diff --git a/.gitattributes b/.gitattributes index 1b3fd332b4..8171cd910f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ # Normalize line endings to LF in the repository * text eol=lf *.png binary +*.gif binary +*.apng binary diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 08def88577..e708ae41b2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,6 +6,7 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) +- [ ] New developer-facing feature (adds functionality for component developers; no end-user configuration change) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change) - [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api) - [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations) @@ -20,6 +21,10 @@ - esphome/esphome.io# +**Pull request in [developers.esphome.io](https://github.com/esphome/developers.esphome.io) with developer documentation (if applicable):** + +- esphome/developers.esphome.io# + ## Test Environment - [ ] ESP32 diff --git a/.github/actions/cache-esp-idf/action.yml b/.github/actions/cache-esp-idf/action.yml index f566ba4c43..b884e1e4c6 100644 --- a/.github/actions/cache-esp-idf/action.yml +++ b/.github/actions/cache-esp-idf/action.yml @@ -3,8 +3,10 @@ description: > Resolve the pinned ESP-IDF version and cache the native ESP-IDF install (toolchains + source) at ~/.esphome-idf. Every job that installs ESP-IDF natively (clang-tidy for IDF/Arduino and the component test batches) shares - one cache, since the install is identical (ESPHOME_IDF_DEFAULT_TARGETS - defaults to "all", so all toolchains are present regardless of the chip). + one cache, since the install is identical: ESPHOME_IDF_DEFAULT_TARGETS + defaults to "all", and _get_configured_targets() in espidf/toolchain.py + skips per-variant narrowing whenever CI is set, so all toolchains are + present regardless of the chip a job builds. Callers must set env ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf and have the Python venv already restored. inputs: diff --git a/.github/actions/cache-sdk-nrf/action.yml b/.github/actions/cache-sdk-nrf/action.yml new file mode 100644 index 0000000000..6cbb87cc66 --- /dev/null +++ b/.github/actions/cache-sdk-nrf/action.yml @@ -0,0 +1,49 @@ +name: Cache nRF Connect SDK +description: > + Resolve the pinned sdk-nrf version and cache the native sdk-nrf install + (west workspace, Zephyr SDK toolchain, python env) at ~/.esphome-sdk-nrf. + Every job that installs sdk-nrf natively (the nrf52 clang-tidy job and, + once the component tests build natively, their batches) shares one cache. + Callers must set env ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf and have + the Python venv already restored. +inputs: + restore-only: + description: > + When "true", only restore -- never save the cache, even on dev. Use from + jobs that may not produce a complete install (e.g. a component batch + that fails mid-install), so a partial install is never written. + default: "false" +runs: + using: composite + steps: + - name: Resolve sdk-nrf and toolchain versions for cache key + # Both versions are pinned in code, not in any file that feeds the + # other cache keys, so resolve them explicitly. Keying on them means + # the cache invalidates when either is bumped (actions/cache never + # overwrites a key). + id: version + shell: bash + run: | + . venv/bin/activate + version=$(python -c ' + from esphome.components.nrf52 import RECOMMENDED_SDK_NRF_VERSION + from esphome.components.nrf52.framework import TOOLCHAIN_VERSION + print(f"{RECOMMENDED_SDK_NRF_VERSION}-{TOOLCHAIN_VERSION}")') + echo "version=$version" >> "$GITHUB_OUTPUT" + # Mirror cache-esp-idf: only dev-branch runs write the shared cache (so it + # lives in the default-branch scope readable by all PRs); PRs are + # restore-only and never push multi-GB artifacts into their own scope. + - name: Cache nRF Connect SDK install (write on dev) + if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.esphome-sdk-nrf + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }} + - name: Cache nRF Connect SDK install (restore-only off dev) + if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.esphome-sdk-nrf + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }} diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 1364e95602..daf041819c 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,7 +17,7 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment @@ -32,9 +32,12 @@ runs: # detects the activated venv via ``VIRTUAL_ENV`` so the venv layout # downstream jobs rely on is preserved. if: steps.cache-venv.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true + # Pull request saves land in per-PR scopes nothing else can + # reuse; dev pushes seed the shared copy instead. + save-cache: ${{ github.event_name != 'pull_request' }} # Pin uv version so the action does not have to fetch the # manifest from raw.githubusercontent.com on every cache # miss; that fetch flakes on Windows runners. diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index 2938fd923c..5e8acb09c9 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -13,6 +13,7 @@ module.exports = { 'merging-to-release', 'merging-to-beta', 'chained-pr', + 'stacked-pr', 'core', 'small-pr', 'medium-pr', @@ -22,11 +23,13 @@ module.exports = { 'has-tests', 'needs-tests', 'needs-docs', + 'needs-developer-docs', 'needs-codeowners', 'too-big', 'labeller-recheck', 'bugfix', 'new-feature', + 'new-feature-developer', 'breaking-change', 'developer-breaking-change', 'undocumented-api-change', @@ -40,5 +43,17 @@ module.exports = { // Keep matching the old esphome-docs name during the transition period /https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/, /esphome\/esphome-docs#\d+/ + ], + + DEVELOPER_DOCS_PR_PATTERNS: [ + /https:\/\/github\.com\/esphome\/developers\.esphome\.io\/pull\/\d+/, + /esphome\/developers\.esphome\.io#\d+/ + ], + + // Files whose developer-facing changes are documented via Python docstrings + // only - developers.esphome.io has no reference page for them yet, so PRs + // touching nothing but these files (and tests/) skip needs-developer-docs. + DEV_DOCS_EXEMPT_FILES: [ + 'esphome/config_validation.py' ] }; diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index 4406370a27..bb85ccd681 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -1,4 +1,4 @@ -const { DOCS_PR_PATTERNS } = require('./constants'); +const { DOCS_PR_PATTERNS, DEVELOPER_DOCS_PR_PATTERNS, DEV_DOCS_EXEMPT_FILES } = require('./constants'); const { COMPONENT_REGEX, detectComponents, @@ -33,8 +33,41 @@ async function fetchPrFileContent(github, context, path) { } } +// Check whether a pull request is part of a GitHub stack. +// +// GitHub's stacked pull request feature adds a `stack` object to the pull +// request resource. It is present on every pull request in the stack - +// including the bottom one, whose base is already `dev` - and is absent +// entirely on standalone pull requests. +// +// The `pull_request_target` webhook payload is not guaranteed to carry this +// field, so fall back to asking the API when it is missing. Guessing wrong +// here is costly: a stacked pull request mistaken for a manually chained one +// gets a label that blocks merging. +async function isStackedPr(github, context) { + const pr = context.payload.pull_request; + if (pr.stack != null) { + return true; + } + + try { + const { owner, repo } = context.repo; + const { data } = await github.rest.pulls.get({ + owner, + repo, + pull_number: pr.number, + }); + return data.stack != null; + } catch (error) { + // Treat an API failure as "not stacked" so a chained pull request still + // gets its blocking label rather than silently slipping through. + console.log('Failed to check stack membership:', error.message); + return false; + } +} + // Strategy: Merge branch detection -async function detectMergeBranch(context) { +async function detectMergeBranch(github, context) { const labels = new Set(); const baseRef = context.payload.pull_request.base.ref; @@ -42,7 +75,11 @@ async function detectMergeBranch(context) { labels.add('merging-to-release'); } else if (baseRef === 'beta') { labels.add('merging-to-beta'); + } else if (await isStackedPr(github, context)) { + // GitHub manages the merge order for a stack, so these are not blocked. + labels.add('stacked-pr'); } else if (baseRef !== 'dev') { + // A chain built by hand: it must not merge until its base branch does. labels.add('chained-pr'); } @@ -245,6 +282,7 @@ async function detectPRTemplateCheckboxes(context) { const checkboxPatterns = [ { pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' }, { pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' }, + { pattern: /- \[x\] New developer-facing feature \(adds functionality for component developers; no end-user configuration change\)/i, label: 'new-feature-developer' }, { pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' }, { pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' }, { pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' }, @@ -355,12 +393,14 @@ async function detectRequirements(allLabels, prFiles, context, hasYamlLoadable) const labels = new Set(); // Check for missing tests - if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) { + if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature') || allLabels.has('new-feature-developer')) && !allLabels.has('has-tests')) { labels.add('needs-tests'); } // Check for missing docs. - // `new-feature` (PR-body checkbox) always counts. `new-component` / `new-platform` + // `new-feature` (PR-body checkbox) always counts. `new-feature-developer` is + // deliberately excluded here: its docs live on developers.esphome.io and are + // checked separately below. `new-component` / `new-platform` // only count when at least one newly added file defines a top-level CONFIG_SCHEMA, // i.e. the new component/platform is actually loadable from YAML. const docsEligible = @@ -376,6 +416,22 @@ async function detectRequirements(allLabels, prFiles, context, hasYamlLoadable) } } + // Check for missing developer docs. `new-feature-developer` requires a + // developers.esphome.io PR link, unless every changed file outside tests/ is + // in DEV_DOCS_EXEMPT_FILES (core validators documented via docstrings only). + if (allLabels.has('new-feature-developer')) { + const prBody = context.payload.pull_request.body || ''; + const nonTestFiles = prFiles + .map(file => file.filename) + .filter(file => !file.startsWith('tests/')); + const onlyExemptFiles = nonTestFiles.every(file => DEV_DOCS_EXEMPT_FILES.includes(file)); + const hasDevDocsLink = DEVELOPER_DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody)); + + if (!onlyExemptFiles && !hasDevDocsLink) { + labels.add('needs-developer-docs'); + } + } + // Check for missing CODEOWNERS if (allLabels.has('new-component')) { const codeownersModified = prFiles.some(file => diff --git a/.github/scripts/auto-label-pr/index.js b/.github/scripts/auto-label-pr/index.js index c8bdcfb2f3..8b0c821503 100644 --- a/.github/scripts/auto-label-pr/index.js +++ b/.github/scripts/auto-label-pr/index.js @@ -88,7 +88,7 @@ module.exports = async ({ github, context }) => { // Early exit for release and beta branches only if (baseRef === 'release' || baseRef === 'beta') { - const branchLabels = await detectMergeBranch(context); + const branchLabels = await detectMergeBranch(github, context); const finalLabels = Array.from(branchLabels); console.log('Computed labels (merge branch only):', finalLabels.join(', ')); @@ -118,7 +118,7 @@ module.exports = async ({ github, context }) => { deprecatedResult, maintainerAccess ] = await Promise.all([ - detectMergeBranch(context), + detectMergeBranch(github, context), detectComponentPlatforms(changedFiles, apiData), detectNewComponents(github, context, prFiles), detectNewPlatforms(github, context, prFiles, apiData), diff --git a/.github/scripts/auto-label-pr/tests/detectors.test.js b/.github/scripts/auto-label-pr/tests/detectors.test.js index aab1827c44..f30ceff8c1 100644 --- a/.github/scripts/auto-label-pr/tests/detectors.test.js +++ b/.github/scripts/auto-label-pr/tests/detectors.test.js @@ -1,6 +1,14 @@ const { describe, it } = require('node:test'); const assert = require('node:assert/strict'); -const { detectNewPlatforms, detectNewComponents, detectPRSize } = require('../detectors'); +const { + detectMergeBranch, + detectNewPlatforms, + detectNewComponents, + detectPRSize, + detectPRTemplateCheckboxes, + detectRequirements, +} = require('../detectors'); +const { MANAGED_LABELS } = require('../constants'); // Minimal GitHub API mock — only repos.getContent is called by detectNewPlatforms/detectNewComponents // to check for CONFIG_SCHEMA in newly added files. @@ -29,6 +37,107 @@ const API_DATA = { const WITH_SCHEMA = 'CONFIG_SCHEMA = cv.Schema({})'; const WITHOUT_SCHEMA = 'CODEOWNERS = ["@esphome/core"]'; +// --------------------------------------------------------------------------- +// detectMergeBranch +// --------------------------------------------------------------------------- + +// Builds a fresh context for detectMergeBranch tests instead of mutating the +// shared CONTEXT fixture above (which other describe blocks rely on). +function makeMergeContext(baseRef, { stack } = {}) { + const pull_request = { number: 1, base: { ref: baseRef } }; + if (stack !== undefined) { + pull_request.stack = stack; + } + return { + repo: { owner: 'esphome', repo: 'esphome' }, + payload: { pull_request } + }; +} + +// A GitHub API mock exposing only rest.pulls.get, with a call counter so +// tests can assert whether the API fallback was actually invoked. +function makeStackGithub({ stack = null, error = null } = {}) { + const state = { calls: 0 }; + const github = { + rest: { + pulls: { + get: async () => { + state.calls++; + if (error) throw error; + return { data: { stack } }; + } + } + } + }; + return { github, state }; +} + +const STACK_INFO = { base: { ref: 'dev' }, id: 71540, number: 17978, position: 3, size: 3 }; + +describe('detectMergeBranch', () => { + it('base ref release adds merging-to-release only and never checks the stack', async () => { + const { github, state } = makeStackGithub({ stack: STACK_INFO }); + const context = makeMergeContext('release', { stack: STACK_INFO }); + const labels = await detectMergeBranch(github, context); + assert.deepEqual(Array.from(labels).sort(), ['merging-to-release']); + assert.equal(state.calls, 0); + }); + + it('base ref beta adds merging-to-beta only and never checks the stack', async () => { + const { github, state } = makeStackGithub({ stack: STACK_INFO }); + const context = makeMergeContext('beta', { stack: STACK_INFO }); + const labels = await detectMergeBranch(github, context); + assert.deepEqual(Array.from(labels).sort(), ['merging-to-beta']); + assert.equal(state.calls, 0); + }); + + it('stack present on the webhook payload adds stacked-pr without calling the API', async () => { + const { github, state } = makeStackGithub(); + const context = makeMergeContext('feature-branch', { stack: STACK_INFO }); + const labels = await detectMergeBranch(github, context); + assert.deepEqual(Array.from(labels).sort(), ['stacked-pr']); + assert.equal(state.calls, 0); + }); + + it('stack absent from payload falls back to the API and adds stacked-pr', async () => { + const { github, state } = makeStackGithub({ stack: STACK_INFO }); + const context = makeMergeContext('feature-branch'); + const labels = await detectMergeBranch(github, context); + assert.deepEqual(Array.from(labels).sort(), ['stacked-pr']); + assert.equal(state.calls, 1); + }); + + it('bottom of a stack (base ref dev, stack present) still adds stacked-pr', async () => { + const { github, state } = makeStackGithub(); + const context = makeMergeContext('dev', { stack: STACK_INFO }); + const labels = await detectMergeBranch(github, context); + assert.deepEqual(Array.from(labels).sort(), ['stacked-pr']); + assert.equal(state.calls, 0); + }); + + it('not stacked, base ref not dev adds chained-pr', async () => { + const { github } = makeStackGithub({ stack: null }); + const context = makeMergeContext('feature-branch'); + const labels = await detectMergeBranch(github, context); + assert.deepEqual(Array.from(labels).sort(), ['chained-pr']); + }); + + it('not stacked, base ref dev adds no labels', async () => { + const { github } = makeStackGithub({ stack: null }); + const context = makeMergeContext('dev'); + const labels = await detectMergeBranch(github, context); + assert.deepEqual(Array.from(labels).sort(), []); + }); + + it('a failed stack lookup falls back to not-stacked, so a feature-branch base adds chained-pr', async () => { + const { github, state } = makeStackGithub({ error: new Error('API unavailable') }); + const context = makeMergeContext('feature-branch'); + const labels = await detectMergeBranch(github, context); + assert.deepEqual(Array.from(labels).sort(), ['chained-pr']); + assert.equal(state.calls, 1); + }); +}); + // --------------------------------------------------------------------------- // detectNewPlatforms // --------------------------------------------------------------------------- @@ -146,6 +255,125 @@ describe('detectNewComponents', () => { }); }); +// --------------------------------------------------------------------------- +// detectPRTemplateCheckboxes +// --------------------------------------------------------------------------- + +const NEW_FEATURE_LINE = '- [x] New feature (non-breaking change which adds functionality)'; +const DEV_FEATURE_LINE = '- [x] New developer-facing feature (adds functionality for component developers; no end-user configuration change)'; +const DEV_FEATURE_LINE_UNTICKED = '- [ ] New developer-facing feature (adds functionality for component developers; no end-user configuration change)'; + +function makeBodyContext(body) { + return { payload: { pull_request: { body } } }; +} + +describe('detectPRTemplateCheckboxes', () => { + it('ticked developer-facing feature checkbox adds new-feature-developer only', async () => { + const labels = await detectPRTemplateCheckboxes(makeBodyContext(DEV_FEATURE_LINE)); + assert.ok(labels.has('new-feature-developer')); + assert.ok(!labels.has('new-feature')); + }); + + it('unticked developer-facing feature checkbox adds no label', async () => { + const labels = await detectPRTemplateCheckboxes(makeBodyContext(DEV_FEATURE_LINE_UNTICKED)); + assert.ok(!labels.has('new-feature-developer')); + }); + + it('ticked new feature checkbox does not add new-feature-developer', async () => { + const labels = await detectPRTemplateCheckboxes(makeBodyContext(NEW_FEATURE_LINE)); + assert.ok(labels.has('new-feature')); + assert.ok(!labels.has('new-feature-developer')); + }); +}); + +// --------------------------------------------------------------------------- +// detectRequirements +// --------------------------------------------------------------------------- + +describe('detectRequirements', () => { + // PR body without any docs-PR link. + const NO_DOCS_CONTEXT = makeBodyContext('Just a description, no docs link.'); + const USER_DOCS_CONTEXT = makeBodyContext('Docs: esphome/esphome.io#1234'); + const DEV_DOCS_CONTEXT = makeBodyContext('Docs: esphome/developers.esphome.io#1234'); + const DEV_DOCS_URL_CONTEXT = makeBodyContext('Docs: https://github.com/esphome/developers.esphome.io/pull/1234'); + + // File sets: a normal source change vs. one confined to the exempt core validators. + const SOURCE_FILES = [ + { filename: 'esphome/components/foo/foo.py' }, + { filename: 'tests/components/foo/common.yaml' }, + ]; + const VALIDATOR_FILES = [ + { filename: 'esphome/config_validation.py' }, + { filename: 'tests/unit_tests/test_config_validation.py' }, + ]; + + it('new-feature-developer without has-tests adds needs-tests but not needs-docs', async () => { + const labels = await detectRequirements(new Set(['new-feature-developer']), SOURCE_FILES, NO_DOCS_CONTEXT, false); + assert.ok(labels.has('needs-tests')); + assert.ok(!labels.has('needs-docs')); + }); + + it('new-feature-developer with has-tests does not add needs-tests', async () => { + const labels = await detectRequirements(new Set(['new-feature-developer', 'has-tests']), SOURCE_FILES, NO_DOCS_CONTEXT, false); + assert.ok(!labels.has('needs-tests')); + }); + + it('new-feature without a docs link still adds needs-docs', async () => { + const labels = await detectRequirements(new Set(['new-feature', 'has-tests']), [], NO_DOCS_CONTEXT, false); + assert.ok(labels.has('needs-docs')); + }); + + it('new-feature-developer without a developer docs link adds needs-developer-docs', async () => { + const labels = await detectRequirements(new Set(['new-feature-developer', 'has-tests']), SOURCE_FILES, NO_DOCS_CONTEXT, false); + assert.ok(labels.has('needs-developer-docs')); + }); + + it('a developers.esphome.io shorthand link satisfies needs-developer-docs', async () => { + const labels = await detectRequirements(new Set(['new-feature-developer', 'has-tests']), SOURCE_FILES, DEV_DOCS_CONTEXT, false); + assert.ok(!labels.has('needs-developer-docs')); + }); + + it('a developers.esphome.io URL link satisfies needs-developer-docs', async () => { + const labels = await detectRequirements(new Set(['new-feature-developer', 'has-tests']), SOURCE_FILES, DEV_DOCS_URL_CONTEXT, false); + assert.ok(!labels.has('needs-developer-docs')); + }); + + it('a user docs (esphome.io) link does not satisfy needs-developer-docs', async () => { + const labels = await detectRequirements(new Set(['new-feature-developer', 'has-tests']), SOURCE_FILES, USER_DOCS_CONTEXT, false); + assert.ok(labels.has('needs-developer-docs')); + }); + + it('a developer docs link does not satisfy needs-docs for new-feature', async () => { + const labels = await detectRequirements(new Set(['new-feature', 'has-tests']), [], DEV_DOCS_CONTEXT, false); + assert.ok(labels.has('needs-docs')); + }); + + it('changes confined to core validator files are exempt from needs-developer-docs', async () => { + const labels = await detectRequirements(new Set(['new-feature-developer', 'has-tests']), VALIDATOR_FILES, NO_DOCS_CONTEXT, false); + assert.ok(!labels.has('needs-developer-docs')); + }); + + it('validator changes mixed with other source files are not exempt', async () => { + const prFiles = [...VALIDATOR_FILES, { filename: 'esphome/components/foo/foo.py' }]; + const labels = await detectRequirements(new Set(['new-feature-developer', 'has-tests']), prFiles, NO_DOCS_CONTEXT, false); + assert.ok(labels.has('needs-developer-docs')); + }); +}); + +// --------------------------------------------------------------------------- +// MANAGED_LABELS +// --------------------------------------------------------------------------- + +describe('MANAGED_LABELS', () => { + it('includes new-feature-developer so the workflow syncs it', () => { + assert.ok(MANAGED_LABELS.includes('new-feature-developer')); + }); + + it('includes needs-developer-docs so the workflow syncs it', () => { + assert.ok(MANAGED_LABELS.includes('needs-developer-docs')); + }); +}); + // --------------------------------------------------------------------------- // detectPRSize // --------------------------------------------------------------------------- diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index d034227ef6..b49c66b976 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -24,7 +24,7 @@ jobs: if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot') steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Generate a token id: generate-token @@ -35,7 +35,7 @@ jobs: # Scope the minted App token to the minimum needed by auto-label-pr/*.js. permission-contents: read # repos.getContent for CODEOWNERS and file lookups in detectors.js permission-issues: write # listLabelsOnIssue, addLabels, removeLabel, list/createComment - permission-pull-requests: write # pulls.listFiles, list/create/update/dismissReview + permission-pull-requests: write # pulls.get, pulls.listFiles, list/create/update/dismissReview - name: Auto Label PR uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 4c0c330a19..820081cc46 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -21,17 +21,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv # ``--system`` (below) installs into the setup-python interpreter; # no venv is created or restored by this workflow. - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true + # Pull-request-only workflow: a save could never be shared and + # would only consume quota. + save-cache: "false" # Pin uv version so the action does not have to fetch the # manifest from raw.githubusercontent.com on every cache # miss; that fetch flakes on Windows runners. diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 07a792df08..30aa511e29 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -61,9 +61,9 @@ jobs: tag: ${{ steps.tag.outputs.tag }} push: ${{ steps.tag.outputs.push }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up Docker Buildx @@ -71,11 +71,13 @@ jobs: - name: Determine tag and whether to push id: tag + env: + HEAD_REF: ${{ github.head_ref || github.ref_name }} run: | # Sanitize the branch name into a valid docker tag: replace invalid # characters, ensure the first character is valid (tags must start # with [A-Za-z0-9_]), and cap the length at 128 characters. - branch="${{ github.head_ref || github.ref_name }}" + branch="$HEAD_REF" tag="${branch//[^a-zA-Z0-9_.-]/-}" case "$tag" in [a-zA-Z0-9_]*) ;; @@ -96,7 +98,7 @@ jobs: - name: Log in to the GitHub container registry if: steps.tag.outputs.push == 'true' - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -145,16 +147,16 @@ jobs: - "ha-addon" - "docker" steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Log in to the GitHub container registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -202,7 +204,7 @@ jobs: - nrf52 - host steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Download image artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/ci-github-scripts.yml b/.github/workflows/ci-github-scripts.yml index 3313ced690..ea039de9b9 100644 --- a/.github/workflows/ci-github-scripts.yml +++ b/.github/workflows/ci-github-scripts.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Run tests working-directory: .github/scripts/auto-label-pr diff --git a/.github/workflows/ci-memory-impact-comment.yml b/.github/workflows/ci-memory-impact-comment.yml index ac0322e2fa..0dc653bd39 100644 --- a/.github/workflows/ci-memory-impact-comment.yml +++ b/.github/workflows/ci-memory-impact-comment.yml @@ -49,7 +49,7 @@ jobs: - name: Check out code from base repository if: steps.pr.outputs.skip != 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Always check out from the base repository (esphome/esphome), never from forks # Use the PR's target branch to ensure we run trusted code from the main repo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9310b45b4a..e020e90018 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,13 +28,13 @@ jobs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Generate cache-key id: cache-key run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment @@ -49,9 +49,12 @@ jobs: # detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs # that ``. venv/bin/activate`` see an identical layout. if: steps.cache-venv.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true + # Pull request saves land in per-PR scopes nothing else can + # reuse; dev pushes seed the shared copy instead. + save-cache: ${{ github.event_name != 'pull_request' }} # Pin uv version so the action does not have to fetch the # manifest from raw.githubusercontent.com on every cache # miss; that fetch flakes on Windows runners. @@ -65,196 +68,6 @@ jobs: uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit uv pip install -e . - pylint: - name: Check pylint - runs-on: ubuntu-24.04 - needs: - - common - - determine-jobs - if: needs.determine-jobs.outputs.python-linters == 'true' - steps: - - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Run pylint - run: | - . venv/bin/activate - pylint -f parseable --persistent=n esphome - - name: Suggested changes - run: script/ci-suggest-changes - if: always() - - ci-custom: - name: Run script/ci-custom - runs-on: ubuntu-24.04 - needs: - - common - - determine-jobs - if: needs.determine-jobs.outputs.core-ci == 'true' - steps: - - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Register matcher - run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json" - - name: Run script/ci-custom - run: | - . venv/bin/activate - script/ci-custom.py - script/build_codeowners.py --check - script/build_language_schema.py --check - script/generate-esp32-boards.py --check - script/generate-rp2040-boards.py --check - script/ci_check_duplicate_test_ids.py - - import-time: - name: Check import esphome.__main__ time - runs-on: ubuntu-24.04 - needs: - - common - - determine-jobs - if: needs.determine-jobs.outputs.import-time == 'true' - steps: - - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Check import time against budget and write waterfall HAR - run: | - . venv/bin/activate - script/check_import_time.py --check --har importtime.har - - name: Upload waterfall HAR - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: import-time-waterfall - path: importtime.har - if-no-files-found: ignore - retention-days: 14 - - device-builder: - name: Test downstream esphome/device-builder - runs-on: ubuntu-24.04 - needs: - - common - - determine-jobs - if: needs.determine-jobs.outputs.device-builder == 'true' - steps: - - name: Check out esphome (this PR) - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - path: esphome - - name: Check out esphome/device-builder - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - repository: esphome/device-builder - ref: main - path: device-builder - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 - with: - python-version: "3.13" - - name: Set up uv - # Mirrors the install shape device-builder's own CI uses - # (esphome/device-builder#192): uv replaces pip for the - # install step (order-of-magnitude faster on cold boots, - # with its own wheel cache). actions/setup-python still - # provides the interpreter. - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - with: - enable-cache: true - # Pin uv version so the action does not have to fetch the - # manifest from raw.githubusercontent.com on every cache - # miss; that fetch flakes on Windows runners. - version: "0.11.15" - - name: Install device-builder + esphome from PR - # Install device-builder with its esphome + test extras - # first so its pinned versions of pytest/etc. land, then - # overlay the PR's esphome so the downstream tests run - # against this PR's Python code. ``--system`` installs into - # the runner's Python instead of a venv. - run: | - uv pip install --system -e './device-builder[esphome,test]' - uv pip install --system -e ./esphome - - name: Run device-builder pytest - # ``-n auto`` runs under pytest-xdist (matches device-builder's - # own CI). No ``--cov`` here -- this is purely a downstream - # smoke check against this PR's esphome code. ``tests/e2e/slow`` - # is excluded: those are real multi-minute toolchain compiles - # (LibreTiny SDK clone, native ESP-IDF install) that device-builder - # runs in its own dedicated jobs, not this smoke check. - working-directory: device-builder - run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks --ignore=tests/e2e/slow - - pytest: - name: Run pytest - strategy: - fail-fast: false - matrix: - python-version: - - "3.12" - - "3.13" - - "3.14" - os: - - ubuntu-latest - - macOS-latest - - windows-latest - exclude: - # Minimize CI resource usage - # by only running the Python version - # version used for docker images on Windows and macOS - - python-version: "3.13" - os: windows-latest - - python-version: "3.13" - os: macOS-latest - runs-on: ${{ matrix.os }} - needs: - - common - - determine-jobs - if: needs.determine-jobs.outputs.core-ci == 'true' - steps: - - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Restore Python - id: restore-python - uses: ./.github/actions/restore-python - with: - python-version: ${{ matrix.python-version }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Register matcher - run: echo "::add-matcher::.github/workflows/matchers/pytest.json" - - name: Run pytest - if: matrix.os == 'windows-latest' - run: | - . ./venv/Scripts/activate.ps1 - pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/ - - name: Run pytest - if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' - run: | - . venv/bin/activate - pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/ - - name: Upload coverage to Codecov - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - - name: Save Python virtual environment cache - if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: venv - key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} - determine-jobs: name: Determine which jobs to run runs-on: ubuntu-24.04 @@ -285,7 +98,7 @@ jobs: benchmarks: ${{ steps.determine.outputs.benchmarks }} steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Fetch enough history to find the merge base fetch-depth: 2 @@ -344,6 +157,204 @@ jobs: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} + ci-custom: + name: Run script/ci-custom + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: needs.determine-jobs.outputs.core-ci == 'true' + steps: + - name: Check out code from GitHub + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Register matcher + run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + - name: Run script/ci-custom + run: | + . venv/bin/activate + script/ci-custom.py + script/build_codeowners.py --check + script/build_language_schema.py --check + script/generate-esp32-boards.py --check + script/generate-rp2-boards.py --check + script/ci_check_duplicate_test_ids.py + script/ci_check_test_fixture_list_form.py + + pylint: + name: Check pylint + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: needs.determine-jobs.outputs.python-linters == 'true' + steps: + - name: Check out code from GitHub + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Run pylint + run: | + . venv/bin/activate + pylint -f parseable --persistent=n esphome + - name: Suggested changes + run: script/ci-suggest-changes + if: always() + + pre-commit-ci-lite: + name: pre-commit.ci lite + runs-on: ubuntu-latest + needs: + - common + - determine-jobs + if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true' + steps: + - name: Check out code from GitHub + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + # Inlined from esphome/pre-commit-action with a restore-only cache + # step: the pre-commit-seed-cache job owns saving this cache, so + # pull request runs never write per-PR copies. + - name: Restore pre-commit cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/pre-commit + # Must match the key pre-commit-seed-cache saves + # yamllint disable-line rule:line-length + key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} + - name: Run pre-commit + env: + SKIP: pylint,ci-custom + run: | + python -m pip install pre-commit + pre-commit run --show-diff-on-failure --color=always --all-files + - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 + if: always() + + pre-commit-seed-cache: + name: Seed pre-commit cache + runs-on: ubuntu-latest + needs: + - common + # Saves a dev-scoped pre-commit cache that pull request runs can + # restore, since pre-commit.ci lite itself never runs on dev pushes. + if: github.event_name == 'push' && github.ref == 'refs/heads/dev' + steps: + - name: Check out code from GitHub + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Cache pre-commit environments + id: cache-pre-commit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/pre-commit + # Must match the restore key in pre-commit-ci-lite + # yamllint disable-line rule:line-length + key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} + - name: Install pre-commit hook environments + if: steps.cache-pre-commit.outputs.cache-hit != 'true' + run: | + python -m pip install pre-commit + pre-commit install-hooks + + pytest: + name: Run pytest + strategy: + fail-fast: false + matrix: + python-version: + - "3.12" + - "3.13" + - "3.14" + os: + - ubuntu-latest + - macOS-latest + - windows-latest + exclude: + # Minimize CI resource usage + # by only running the Python version + # version used for docker images on Windows and macOS + - python-version: "3.13" + os: windows-latest + - python-version: "3.13" + os: macOS-latest + runs-on: ${{ matrix.os }} + needs: + - common + - determine-jobs + if: needs.determine-jobs.outputs.core-ci == 'true' + steps: + - name: Check out code from GitHub + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Restore Python + id: restore-python + uses: ./.github/actions/restore-python + with: + python-version: ${{ matrix.python-version }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Register matcher + run: echo "::add-matcher::.github/workflows/matchers/pytest.json" + - name: Run pytest + if: matrix.os == 'windows-latest' + run: | + . ./venv/Scripts/activate.ps1 + pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/ + - name: Run pytest + if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' + run: | + . venv/bin/activate + pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/ + - name: Upload coverage to Codecov + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + - name: Save Python virtual environment cache + if: github.ref == 'refs/heads/dev' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: venv + key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} + + codecov-empty-upload: + name: Report no coverage to Codecov + runs-on: ubuntu-24.04 + needs: + - determine-jobs + # ``pytest`` is the only job that uploads coverage, and it is skipped when + # every changed file is CI-irrelevant (see ``should_run_core_ci`` in + # ``script/determine-jobs.py``). With no upload Codecov never reports a + # result, so the required ``codecov/patch`` status stays pending forever and + # the pull request can never be merged. Tell Codecov up front that this + # commit has nothing to cover so it publishes a passing status instead. + # + # ``force`` skips Codecov's own check that every changed file is ignorable; + # ``determine-jobs`` has already decided none of these files can affect + # coverage, and Codecov would otherwise fail the status for paths it does + # not recognise as non-testable (``docker/**``, ``.yamllint``). + if: needs.determine-jobs.outputs.core-ci == 'false' + steps: + - name: Check out code from GitHub + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Report empty upload to Codecov + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + run_command: empty-upload + force: true + fail_ci_if_error: true + integration-tests: name: Run integration tests (${{ matrix.bucket.name }}) runs-on: ubuntu-latest @@ -357,10 +368,28 @@ jobs: bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }} steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install ccache + # Speeds up the host compiles: tests in a bucket compile overlapping + # component sets, so later tests reuse earlier tests' objects. + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends ccache + - name: Restore ccache (restore-only) + # esphome stores the PlatformIO ccache under the machine-global cache + # dir (see _ccache_env() in esphome/platformio/toolchain.py). The + # bucket-name prefix prefers a same-bucket seed; the bare prefix falls + # back to any seed when the bucket layout differs from dev. + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/esphome/platformio-ccache + key: integration-ccache-${{ matrix.bucket.name }}-${{ github.sha }} + restore-keys: | + integration-ccache-${{ matrix.bucket.name }}- + integration-ccache- - name: Set up Python 3.13 id: python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Restore Python virtual environment @@ -372,9 +401,12 @@ jobs: - name: Set up uv # Only needed on cache miss to populate the venv. if: steps.cache-venv.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true + # Pull request saves land in per-PR scopes nothing else can + # reuse; dev pushes seed the shared copy instead. + save-cache: ${{ github.event_name != 'pull_request' }} # Pin uv version so the action does not have to fetch the # manifest from raw.githubusercontent.com on every cache # miss; that fetch flakes on Windows runners. @@ -399,33 +431,46 @@ jobs: mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]') echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests" pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}" + - name: Print ccache statistics + # esphome stores the PlatformIO ccache under the machine-global cache + # dir (see _ccache_env() in esphome/platformio/toolchain.py). + run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s + - name: Save ccache + # Pull request saves land in per-PR scopes nothing else can reuse; + # dev pushes seed the shared copy instead. + if: github.event_name != 'pull_request' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/esphome/platformio-ccache + key: integration-ccache-${{ matrix.bucket.name }}-${{ github.sha }} - cpp-unit-tests: - name: Run C++ unit tests + import-time: + name: Check import esphome.__main__ time runs-on: ubuntu-24.04 needs: - common - determine-jobs - if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') + if: needs.determine-jobs.outputs.import-time == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - - name: Run cpp_unit_test.py + - name: Check import time against budget and write waterfall HAR run: | . venv/bin/activate - if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then - script/cpp_unit_test.py --all - else - ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs) - script/cpp_unit_test.py $ARGS - fi + script/check_import_time.py --check --har importtime.har + - name: Upload waterfall HAR + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: import-time-waterfall + path: importtime.har + if-no-files-found: ignore + retention-days: 14 benchmarks: name: Run CodSpeed benchmarks @@ -438,7 +483,7 @@ jobs: (github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true') steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Restore Python uses: ./.github/actions/restore-python @@ -456,7 +501,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1 + uses: CodSpeedHQ/action@88472375d0a4572cf70a9f1fe3a4e0ab8da1b924 # v5.0.1 with: run: | . venv/bin/activate @@ -464,6 +509,33 @@ jobs: pytest tests/benchmarks/python/ --codspeed --no-cov mode: simulation + cpp-unit-tests: + name: Run C++ unit tests + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') + steps: + - name: Check out code from GitHub + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + + - name: Run cpp_unit_test.py + run: | + . venv/bin/activate + if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then + script/cpp_unit_test.py --all + else + ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs) + script/cpp_unit_test.py $ARGS + fi + clang-tidy-single: name: ${{ matrix.name }} runs-on: ubuntu-24.04 @@ -475,6 +547,8 @@ jobs: GH_TOKEN: ${{ github.token }} # esp32-arduino-tidy installs ESP-IDF natively; share the native IDF cache. ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf + # nrf52-tidy installs sdk-nrf natively; pin it to a cacheable path. + ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf strategy: fail-fast: false max-parallel: 2 @@ -491,12 +565,21 @@ jobs: - id: clang-tidy name: Run script/clang-tidy for ZEPHYR options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52 - pio_cache_key: tidy-zephyr + cache_sdk_nrf: true ignore_errors: false + - id: clang-tidy + name: Run script/clang-tidy for RP2 + options: --environment rp2-tidy --grep USE_RP2 + pio_cache_key: tidyrp2 + - id: clang-tidy + name: Run script/clang-tidy for LibreTiny + environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy + options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X + pio_cache_key: tidylibretiny steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -527,6 +610,10 @@ jobs: with: framework: arduino + - name: Cache nRF Connect SDK install + if: matrix.cache_sdk_nrf + uses: ./.github/actions/cache-sdk-nrf + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" @@ -552,10 +639,21 @@ jobs: . venv/bin/activate if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})" - script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} + changed="" else echo "Running clang-tidy on changed files only" - script/clang-tidy --all-headers --fix --changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} + changed="--changed" + fi + if [ -n "${{ matrix.environments }}" ]; then + rc=0 + for env in ${{ matrix.environments }}; do + echo "::group::clang-tidy $env" + script/clang-tidy --all-headers --fix $changed --environment "$env" ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} || rc=1 + echo "::endgroup::" + done + exit $rc + else + script/clang-tidy --all-headers --fix $changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} fi env: # Also cache libdeps, store them in a ~/.platformio subfolder @@ -579,7 +677,7 @@ jobs: ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -659,7 +757,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -729,7 +827,8 @@ jobs: include: - id: clang-tidy name: Run script/clang-tidy for ESP32 S3 - options: --environment esp32s3-idf-tidy --grep USE_ESP32_VARIANT_ESP32S3 + # yamllint disable-line rule:line-length + options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC - id: clang-tidy name: Run script/clang-tidy for ESP32 P4 # P4 has no native Wi-Fi/BLE; those run over the hosted co-processor, @@ -739,11 +838,11 @@ jobs: - id: clang-tidy name: Run script/clang-tidy for ESP32 C6 # yamllint disable-line rule:line-length - options: --environment esp32c6-idf-tidy --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE + options: --environment esp32c6-idf-tidy --grep SOC_LP_I2C_SUPPORTED --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -805,6 +904,9 @@ jobs: # esp32 component builds use the native ESP-IDF toolchain (default), so # share the tidy jobs' install location -- the restore below lands here. ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf + # nrf52 component builds install sdk-nrf natively; pin it to the shared + # cacheable path so the restore below lands where the build looks. + ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf strategy: fail-fast: false max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} @@ -819,14 +921,15 @@ jobs: - name: List components run: echo ${{ matrix.batch.components }} - - name: Cache apt packages - uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3 - with: - packages: libsdl2-dev ccache - version: 1.1 + - name: Install apt packages + # Not cached: this job is pull-request-only, so a cache save could + # never be shared and would only consume quota. + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends libsdl2-dev ccache - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -840,6 +943,14 @@ jobs: uses: ./.github/actions/cache-esp-idf with: restore-only: true + - name: Cache nRF Connect SDK install (restore-only) + # Only batches whose test platforms include nrf52 need the native + # sdk-nrf install; never save -- just reuse the shared install the + # dev nrf52 tidy job cached when present. + if: matrix.batch.needs_nrf + uses: ./.github/actions/cache-sdk-nrf + with: + restore-only: true - name: Validate and compile components with intelligent grouping run: | . venv/bin/activate @@ -963,7 +1074,7 @@ jobs: TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp32-platformio-components }} steps: - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Restore Python uses: ./.github/actions/restore-python @@ -989,26 +1100,62 @@ jobs: # Arduino framework via PlatformIO (only components with an esp32-ard test are built): python3 script/test_build_components.py -e compile -t esp32-ard -c "$TEST_COMPONENTS" -f --toolchain platformio - pre-commit-ci-lite: - name: pre-commit.ci lite - runs-on: ubuntu-latest + device-builder: + name: Test downstream esphome/device-builder + runs-on: ubuntu-24.04 needs: - common - determine-jobs - if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true' + if: needs.determine-jobs.outputs.device-builder == 'true' steps: - - name: Check out code from GitHub - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Restore Python - uses: ./.github/actions/restore-python + - name: Check out esphome (this PR) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache - env: - SKIP: pylint,ci-custom - - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 - if: always() + path: esphome + - name: Check out esphome/device-builder + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: esphome/device-builder + ref: main + path: device-builder + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + - name: Set up uv + # Mirrors the install shape device-builder's own CI uses + # (esphome/device-builder#192): uv replaces pip for the + # install step (order-of-magnitude faster on cold boots, + # with its own wheel cache). actions/setup-python still + # provides the interpreter. + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + enable-cache: true + # Pull request saves land in per-PR scopes nothing else can + # reuse; dev pushes seed the shared copy instead. + save-cache: ${{ github.event_name != 'pull_request' }} + # Pin uv version so the action does not have to fetch the + # manifest from raw.githubusercontent.com on every cache + # miss; that fetch flakes on Windows runners. + version: "0.11.15" + - name: Install device-builder + esphome from PR + # Install device-builder with its esphome + test extras + # first so its pinned versions of pytest/etc. land, then + # overlay the PR's esphome so the downstream tests run + # against this PR's Python code. ``--system`` installs into + # the runner's Python instead of a venv. + run: | + uv pip install --system -e './device-builder[esphome,test]' + uv pip install --system -e ./esphome + - name: Run device-builder pytest + # ``-n auto`` runs under pytest-xdist (matches device-builder's + # own CI). No ``--cov`` here -- this is purely a downstream + # smoke check against this PR's esphome code. ``tests/e2e/slow`` + # is excluded: those are real multi-minute toolchain compiles + # (LibreTiny SDK clone, native ESP-IDF install) that device-builder + # runs in its own dedicated jobs, not this smoke check. + working-directory: device-builder + run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks --ignore=tests/e2e/slow memory-impact-target-branch: name: Build target branch for memory impact @@ -1024,7 +1171,7 @@ jobs: skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }} steps: - name: Check out target branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.base_ref }} @@ -1206,7 +1353,7 @@ jobs: flash_usage: ${{ steps.extract.outputs.flash_usage }} steps: - name: Check out PR branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -1275,7 +1422,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -1309,21 +1456,28 @@ jobs: ci-status: name: CI Status runs-on: ubuntu-24.04 + # Listed in the same order the jobs are defined above. Two jobs are + # deliberately left out: "benchmarks" reports through CodSpeed rather than + # this check, and "pre-commit-seed-cache" only populates a cache on pushes + # to dev. needs: - common + - determine-jobs - ci-custom - pylint + - pre-commit-ci-lite - pytest + - codecov-empty-upload - integration-tests + - import-time + - cpp-unit-tests - clang-tidy-single - clang-tidy-nosplit - clang-tidy-split - clang-tidy-esp32-variants - - determine-jobs - - device-builder - test-build-components-split - test-esp32-platformio - - pre-commit-ci-lite + - device-builder - memory-impact-target-branch - memory-impact-pr-branch - memory-impact-comment diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index 9b1333734e..bb1d1e2d7a 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout base branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.base.sha }} sparse-checkout: | diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index da9c5f63d6..38a4b8ff0e 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout base branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.base.sha }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 610e6ed020..3ffb4a633f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -52,11 +52,11 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -84,6 +84,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 5e70117652..ec736a2002 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -14,4 +14,4 @@ jobs: permissions: issues: write # issues.lock on closed issues pull-requests: write # issues.lock on closed pull requests - uses: esphome/workflows/.github/workflows/lock.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1 + uses: esphome/workflows/.github/workflows/lock.yml@9f6577fd37b5cf773ab1b9be929714a0dcd15661 # 2026.7.0 diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index 2bb6505b74..3a89c26cd3 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -16,7 +16,7 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d00c6523c7..839b805237 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: branch_build: ${{ steps.tag.outputs.branch_build }} deploy_env: ${{ steps.tag.outputs.deploy_env }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Get tag id: tag # yamllint disable rule:line-length @@ -60,9 +60,9 @@ jobs: contents: read # actions/checkout to build the sdist/wheel id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish) steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.x" - name: Build @@ -70,7 +70,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: skip-existing: true @@ -92,9 +92,9 @@ jobs: os: "ubuntu-24.04-arm" steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" @@ -102,12 +102,12 @@ jobs: uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Log in to docker hub - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -168,7 +168,7 @@ jobs: - ghcr - dockerhub steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Download digests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -182,13 +182,13 @@ jobs: - name: Log in to docker hub if: matrix.registry == 'dockerhub' - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry if: matrix.registry == 'ghcr' - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7003f6c482..8ec88238a9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,61 +6,46 @@ on: - cron: "30 0 * * *" workflow_dispatch: -permissions: - issues: write # actions/stale labels, comments on, and closes stale issues - pull-requests: write # actions/stale labels, comments on, and closes stale pull requests - -concurrency: - group: lock +# The reusable workflow authenticates as the ESPHome GitHub App, so GITHUB_TOKEN +# needs no permissions at all. +permissions: {} jobs: stale: if: github.repository_owner == 'esphome' - runs-on: ubuntu-latest - steps: - - name: Stale - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 - with: - debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch - remove-stale-when-updated: true - operations-per-run: 400 + # No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome + # GitHub App token so the labels, comments and closures come from + # esphome[bot] instead of github-actions[bot]. + uses: esphome/workflows/.github/workflows/stale.yml@203cea60ebfd18e2b966e57750750e0417a9feec # main + secrets: + ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + with: + # Live only on dev: a workflow_dispatch from any other branch is a dry run + dry-run: ${{ github.ref != 'refs/heads/dev' }} + days-before-stale: 90 + days-before-close: 7 + stale-label: stale + exempt-label: not-stale + ignored-users: esphbot,codecov-commenter + stale-pr-message: > + There hasn't been any activity on this pull request recently. This + pull request has been automatically marked as stale because of that + and will be closed if no further activity occurs within 7 days. - # The 90 day stale policy for PRs - # - PRs - # - No PRs marked as "not-stale" - # - No Issues (see below) - days-before-pr-stale: 90 - days-before-pr-close: 7 - stale-pr-label: "stale" - exempt-pr-labels: "not-stale" - stale-pr-message: > - There hasn't been any activity on this pull request recently. This - pull request has been automatically marked as stale because of that - and will be closed if no further activity occurs within 7 days. + If you are the author of this PR, please leave a comment if you want + to keep it open. Also, please rebase your PR onto the latest dev + branch to ensure that it's up to date with the latest changes. - If you are the author of this PR, please leave a comment if you want - to keep it open. Also, please rebase your PR onto the latest dev - branch to ensure that it's up to date with the latest changes. + Thank you for your contribution! + stale-issue-message: > + There hasn't been any activity on this issue recently. Due to the + high number of incoming GitHub notifications, we have to clean some + of the old issues, as many of them have already been resolved with + the latest updates. - Thank you for your contribution! + Please make sure to update to the latest ESPHome version and + check if that solves the issue. Let us know if that works for you by + adding a comment 👍 - # The 90 day stale policy for Issues - # - Issues - # - No Issues marked as "not-stale" - # - No PRs (see above) - days-before-issue-stale: 90 - days-before-issue-close: 7 - stale-issue-label: "stale" - exempt-issue-labels: "not-stale" - stale-issue-message: > - There hasn't been any activity on this issue recently. Due to the - high number of incoming GitHub notifications, we have to clean some - of the old issues, as many of them have already been resolved with - the latest updates. - - Please make sure to update to the latest ESPHome version and - check if that solves the issue. Let us know if that works for you by - adding a comment 👍 - - This issue has now been marked as stale and will be closed if no - further activity occurs. Thank you for your contributions. + This issue has now been marked as stale and will be closed if no + further activity occurs. Thank you for your contributions. diff --git a/.github/workflows/status-check-labels.yml b/.github/workflows/status-check-labels.yml index d27cc0cbec..72987c25b1 100644 --- a/.github/workflows/status-check-labels.yml +++ b/.github/workflows/status-check-labels.yml @@ -5,7 +5,7 @@ on: types: [opened, reopened, labeled, unlabeled, synchronize] permissions: - pull-requests: read # issues.listLabelsOnIssue to detect blocking labels (needs-docs, merge-after-release, chained-pr) + pull-requests: read # issues.listLabelsOnIssue to detect blocking labels (needs-docs, needs-developer-docs, merge-after-release, chained-pr) concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} @@ -20,7 +20,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr']; + const blockingLabels = ['needs-docs', 'needs-developer-docs', 'merge-after-release', 'chained-pr']; const { data: labels } = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 0501d6d364..5d250b97eb 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -28,16 +28,16 @@ jobs: permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Checkout Home Assistant - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: home-assistant/core path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" @@ -47,7 +47,7 @@ jobs: # setup-python interpreter so subsequent ``pre-commit`` / # ``script/run-in-env.py`` steps find the deps without a # ``uv run`` prefix. - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true # Pin uv version so the action does not have to fetch the @@ -96,8 +96,8 @@ jobs: uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: commit-message: "Synchronise Device Classes from Home Assistant" - committer: esphomebot - author: esphomebot + committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com> + author: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com> branch: sync/device-classes delete-branch: true title: "Synchronise Device Classes from Home Assistant" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da424f516f..99a4f40201 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.15 + rev: v0.16.0 hooks: # Run the linter. - id: ruff diff --git a/AGENTS.md b/AGENTS.md index 9a01626ee4..b067482d18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -191,11 +191,14 @@ This document provides essential context for AI models interacting with this pro my_component_ns = cg.esphome_ns.namespace("my_component") MyComponent = my_component_ns.class_("MyComponent", cg.Component) - CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(MyComponent), - cv.Required(CONF_KEY): cv.string, - cv.Optional(CONF_PARAM, default=42): cv.int_, - }).extend(cv.COMPONENT_SCHEMA) + CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MyComponent), + cv.Required(CONF_KEY): cv.string, + cv.Optional(CONF_PARAM, default=42): cv.int_, + } + ).extend(cv.COMPONENT_SCHEMA) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -229,7 +232,12 @@ This document provides essential context for AI models interacting with this pro - **Sensor:** ```python from esphome.components import sensor - CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(cv.polling_component_schema("60s")) + + CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend( + cv.polling_component_schema("60s") + ) + + async def to_code(config): var = await sensor.new_sensor(config) await cg.register_component(var, config) @@ -238,7 +246,10 @@ This document provides essential context for AI models interacting with this pro - **Binary Sensor:** ```python from esphome.components import binary_sensor - CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({ ... }) + + CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({...}) + + async def to_code(config): var = await binary_sensor.new_binary_sensor(config) ``` @@ -246,7 +257,10 @@ This document provides essential context for AI models interacting with this pro - **Switch:** ```python from esphome.components import switch - CONFIG_SCHEMA = switch.switch_schema().extend({ ... }) + + CONFIG_SCHEMA = switch.switch_schema().extend({...}) + + async def to_code(config): var = await switch.new_switch(config) ``` @@ -263,10 +277,13 @@ This document provides essential context for AI models interacting with this pro ```python from esphome import automation - CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(MyComponent), - cv.Optional(CONF_ON_STATE): automation.validate_automation({}), - }).extend(cv.COMPONENT_SCHEMA) + CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MyComponent), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + } + ).extend(cv.COMPONENT_SCHEMA) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -316,11 +333,14 @@ This document provides essential context for AI models interacting with this pro ```python TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template()) - CONFIG_SCHEMA = cv.Schema({ - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)} - ), - }) + CONFIG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)} + ), + } + ) + async def to_code(config): for conf in config.get(CONF_ON_TURN_ON, []): @@ -368,7 +388,10 @@ This document provides essential context for AI models interacting with this pro ``` Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`. +* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`. + * **Configuration Validation:** + * **Reuse existing validators:** Before writing a custom validator, check for an existing one in `config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components. For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`. * **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`. * **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`. * **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`, `cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`. @@ -381,6 +404,7 @@ This document provides essential context for AI models interacting with this pro .extend(i2c.i2c_device_schema(0x48)) .extend(spi.spi_device_schema(cs_pin_required=True)) ``` + * **Constants:** `esphome/const.py` is frozen — do not add new `CONF_` constants there. Define a component-local constant in the component's own `.py` (as with `CONF_PARAM` above); for a constant shared by multiple components, add it to `esphome/components/const/__init__.py`. CI (`lint_constants_usage`) fails if the same constant is defined in three or more component files. Constants used in core files (i.e. those not under `esphome/components`) may be added to `esphome/const.py` but will require adjustment to the CI validation check. ## 5. Key Files & Entrypoints @@ -427,13 +451,14 @@ This document provides essential context for AI models interacting with this pro When a PR's only edits to a component are `validate.*.yaml` files (no source changes, no `test.*.yaml` changes, and the component isn't pulled in as a dependency of another changed component), CI skips the compile stage for that component entirely and only runs config validation. This is decided in `script/determine-jobs.py` via `_component_change_is_validate_only` and surfaced as the `validate_only_components` output that the `test-build-components-split` job consumes. - * **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`: + * **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`. + + All includes in test files must go through dict-style `packages:` so that batch grouping works correctly — the grouping scripts only understand dict-style packages. Never use list-style packages (`packages: [- !include ...]`) or top-level merge keys (`<<: !include common.yaml`). Bus packages are keyed by the bus name; the component's `common.yaml` is keyed by the component name (e.g. `cst328: !include common.yaml`): ```yaml - # test.esp32-idf.yaml — use packages for buses + # test.esp32-idf.yaml — everything included via named packages packages: uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml - - <<: !include common.yaml + my_component: !include common.yaml ``` ```yaml # common.yaml — component config only, NO bus definitions @@ -470,7 +495,7 @@ This document provides essential context for AI models interacting with this pro 3. **Test:** Create component tests for all supported platforms and run the full test suite locally. 4. **Lint:** Run `pre-commit` to ensure code is compliant. 5. **Commit:** Commit your changes. There is no strict format for commit messages. - 6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template. + 6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template. * **Documentation Contributions:** * Documentation is hosted in the separate `esphome/esphome.io` repository. @@ -616,6 +641,7 @@ This document provides essential context for AI models interacting with this pro _component_state = [] _use_feature = None + def enable_feature(): global _use_feature _use_feature = True @@ -635,20 +661,24 @@ This document provides essential context for AI models interacting with this pro DOMAIN = "my_component" + @dataclass class MyComponentData: feature_enabled: bool = False item_count: int = 0 items: list[str] = field(default_factory=list) + def _get_data() -> MyComponentData: if DOMAIN not in CORE.data: CORE.data[DOMAIN] = MyComponentData() return CORE.data[DOMAIN] + def request_feature() -> None: _get_data().feature_enabled = True + def add_item(item: str) -> None: _get_data().items.append(item) ``` @@ -703,10 +733,22 @@ This document provides essential context for AI models interacting with this pro ``` * **Deprecation Pattern (Python):** + For a renamed config key, use the shared `cv.rename_key` validator with `removed_in` (and `component` for context) — it warns and auto-migrates: + ```python + CONFIG_SCHEMA = cv.All( + cv.rename_key( + CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component" + ), + cv.Schema({ ... }), + ) + ``` + For other deprecations, warn manually during validation: ```python # Remove before 2026.6.0 if CONF_OLD_KEY in config: - _LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0") + _LOGGER.warning( + f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0" + ) config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate ``` ## 9. English Language diff --git a/CODEOWNERS b/CODEOWNERS index 07f36609d4..cb1be26a61 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -69,11 +69,14 @@ esphome/components/bh1750/* @OttoWinter esphome/components/bh1900nux/* @B48D81EFCC esphome/components/binary_sensor/* @esphome/core esphome/components/bk72xx/* @kuba2k2 +esphome/components/bk72xx_ble/* @Bl00d-B0b +esphome/components/bk72xx_ble_tracker/* @Bl00d-B0b esphome/components/bl0906/* @athom-tech @jesserockz @tarontop esphome/components/bl0939/* @ziceva esphome/components/bl0940/* @dan-s-github @tobias- esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow +esphome/components/ble_device_base/* @Bl00d-B0b esphome/components/ble_nus/* @tomaszduda23 esphome/components/bluetooth_proxy/* @bdraco @jesserockz esphome/components/bm8563/* @abmantis @@ -122,6 +125,7 @@ esphome/components/cover/* @esphome/core esphome/components/cs5460a/* @balrog-kun esphome/components/cse7761/* @berfenger esphome/components/cst226/* @clydebarrow +esphome/components/cst328/* @latonita esphome/components/cst816/* @clydebarrow esphome/components/cst9220/* @clydebarrow esphome/components/ct_clamp/* @jesserockz @@ -144,6 +148,7 @@ esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/ds2484/* @mrk-its +esphome/components/ds248x/* @tomwellnitz esphome/components/dsmr/* @glmnet @PolarGoose esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M @@ -186,6 +191,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi +esphome/components/file/* @esphome/core esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund esphome/components/font/* @clydebarrow @esphome/core esphome/components/fs3000/* @kahrendt @@ -207,6 +213,7 @@ esphome/components/gree/switch/* @nagyrobi esphome/components/grove_gas_mc_v2/* @YorkshireIoT esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte +esphome/components/gsl3670/* @clydebarrow esphome/components/gt911/* @clydebarrow @jesserockz esphome/components/haier/* @paveldn esphome/components/haier/binary_sensor/* @paveldn @@ -288,6 +295,7 @@ esphome/components/light/* @esphome/core esphome/components/lightwaverf/* @max246 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lm75b/* @beormund +esphome/components/ln882h_ble/* @Bl00d-B0b esphome/components/ln882x/* @lamauny esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core @@ -403,6 +411,7 @@ esphome/components/pn7160_i2c/* @jesserockz @kbx81 esphome/components/pn7160_spi/* @jesserockz @kbx81 esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core +esphome/components/provisioning/* @esphome/core esphome/components/psram/* @esphome/core esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston esphome/components/pvvx_mithermometer/* @pasiz @@ -425,7 +434,7 @@ esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/ring_buffer/* @kahrendt esphome/components/router/speaker/* @kahrendt -esphome/components/rp2040/* @jesserockz +esphome/components/rp2/* @jesserockz esphome/components/rp2040_ble/* @bdraco esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pwm/* @jesserockz diff --git a/Doxyfile b/Doxyfile index 9f4e20b977..3bb08e5b06 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.7.0-dev +PROJECT_NUMBER = 2026.8.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/MANIFEST.in b/MANIFEST.in index e426627e8d..1626261fb6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,3 +6,4 @@ recursive-include esphome *.cpp *.h *.tcc *.c recursive-include esphome *.py.script recursive-include esphome *.jinja recursive-include esphome LICENSE.txt +recursive-include esphome requirements.txt diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index a4355a5055..5816f38176 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -79,6 +79,48 @@ These *are* security bugs in this repo, and we want to hear about them privately - Flaws that weaken the device's API encryption (Noise), OTA, or web server auth below their documented guarantees. +## The web server is an open HTTP API by design + +The `web_server` component exposes a plain HTTP interface for viewing and +controlling entities, and, when the `web_server` OTA platform is enabled, for +uploading firmware at `/update`. Its only access controls are the optional +`web_server` `auth:` credentials and the network the device sits on. + +When `auth:` is not configured, every endpoint is reachable by any client that +can reach the device. This is intentional; enabling `web_server` without `auth:` +is choosing an open control surface, in the same way that running native OTA +without a password leaves OTA open. The API is documented and is meant to be +called by other devices, scripts, and pages. + +As defense-in-depth, the web server checks the `Origin` header on browser requests +to its entity control and state endpoints: a request whose `Origin` does not match +the address the device is served on is rejected, and the `allowed_origins` option +widens that list. This blocks the common "confused deputy" (CSRF) case where a page +the operator visits drives the device through their browser. It is **not** an +authentication boundary: it only constrains browsers. Any client that omits the +`Origin` header — `curl`, scripts, or other non-browser callers on the same +network — reaches every endpoint exactly as before. The check also does not cover +the web OTA `/update` endpoint. The device performs no CSRF-token or `Referer` +validation. The following are therefore **not** vulnerabilities in this repository: + +- Requests without an `Origin` header (for example `curl`) reaching the control + endpoints, whether or not `web_server` `auth:` is set. +- Requests from an origin the operator added to `allowed_origins`. +- Cross-origin or CSRF firmware upload through the web OTA endpoint (`/update`) when + web OTA is enabled without `web_server` `auth:`. The `/update` endpoint is not + covered by the `Origin` check; this is the same exposure as running OTA without a + password. + +The supported defenses are `web_server` `auth:`, protecting OTA (a web password or +a native OTA password), and keeping devices on a trusted, segmented network. See +the security best practices guide linked above. + +What remains in scope is bypassing `web_server` `auth:` when it *is* configured, +and any memory-safety or protocol bug in the server reachable without credentials. + +This section documents the current design and scope; it is not a judgment that the +design is optimal or that it will not change. + ## Explicitly out of scope - Local attackers who already have shell access on the host that runs `esphome`. @@ -86,6 +128,10 @@ These *are* security bugs in this repo, and we want to hear about them privately - Operator-supplied hostile YAML (covered above — config authoring is trusted). - Attacks that require an already-authenticated device peer (someone who already holds the API key / OTA / web credentials). +- Access to the device web server or its web OTA endpoint by non-browser clients + (those that send no `Origin` header). The web server is an open HTTP API by + design (see above); browser cross-origin requests are blocked by default, but the + real controls are `web_server` `auth:` and network isolation. - Anything in the dashboard / device-builder — report that in its own repository (linked at the top). - Deployments where the operator removed protections or exposed credentials. See diff --git a/docker/Dockerfile b/docker/Dockerfile index 9dec23db1b..a7aaea6b60 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.0.27 +RUN uv pip install --no-cache-dir esphome-device-builder==1.9.0 RUN \ platformio settings set enable_telemetry No \ diff --git a/esphome/__main__.py b/esphome/__main__.py index 2cc904ff4b..e56b504398 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -16,14 +16,10 @@ import sys import time from typing import Protocol -import argcomplete - # Note: Do not import modules from esphome.components here, as this would # cause them to be loaded before external components are processed, resulting # in the built-in version being used instead of the external component one. from esphome import const -import esphome.codegen as cg -from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( ALLOWED_NAME_CHARS, ARGUMENT_HELP_DEVICE, @@ -355,7 +351,7 @@ def choose_upload_log_host( bootsel_permission_error = False if ( purpose == Purpose.UPLOADING - and CORE.is_rp2040 + and CORE.is_rp2 and (picotool := _find_picotool()) is not None ): bootsel = detect_rp2040_bootsel(picotool) @@ -402,7 +398,7 @@ def choose_upload_log_host( # Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found if ( purpose == Purpose.UPLOADING - and CORE.is_rp2040 + and CORE.is_rp2 and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options) ): if bootsel_permission_error: @@ -704,6 +700,8 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: def _wrap_to_code(name, comp, yaml_util): + import esphome.codegen as cg + coro = coroutine(comp.to_code) @functools.wraps(comp.to_code) @@ -739,6 +737,7 @@ def write_cpp(config: ConfigType) -> int: def generate_cpp_contents(config: ConfigType) -> None: from esphome import yaml_util + from esphome.config import iter_component_configs _LOGGER.info("Generating C++ source...") @@ -776,6 +775,13 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: check_placeholder_credentials(config) + # Keep this here, NOT in codegen: config-hash and --only-generate must keep + # working on machines that cannot run the toolchain. + if CORE.is_esp8266: + from esphome.components.esp8266 import check_rosetta + + check_rosetta() + # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py # If you change this format, update the regex in that script as well _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) @@ -985,7 +991,7 @@ def upload_using_platformio(config: ConfigType, port: str) -> int: # RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for # the upload target, but 'nobuild' skips the build phase that creates it. # Create it here so the upload doesn't fail. - if CORE.is_rp2040: + if CORE.is_rp2: idedata = toolchain.get_idedata(config) build_dir = Path(idedata.firmware_elf_path).parent firmware_bin = build_dir / "firmware.bin" @@ -1173,7 +1179,7 @@ def upload_program( if CORE.is_esp32 or CORE.is_esp8266: file = getattr(args, "file", None) exit_code = upload_using_esptool(config, host, file, args.upload_speed) - elif CORE.is_rp2040 or CORE.is_libretiny: + elif CORE.is_rp2 or CORE.is_libretiny: exit_code = upload_using_platformio(config, host) # else: Unknown target platform, exit_code remains 1 @@ -1457,6 +1463,7 @@ def command_wizard(args: ArgsProtocol) -> int | None: def command_config(args: ArgsProtocol, config: ConfigType) -> int | None: from esphome import yaml_util + from esphome.config import strip_default_ids if getattr(args, "no_defaults", False): user_config = getattr(config, "user_config", None) @@ -1510,10 +1517,18 @@ def _redact_with_legacy_fallback(output: str) -> str: m = _LEGACY_REDACTION_RE.search(line) if m is None: continue + key = m.group("key") if not in_substitutions: - unmarked.add(m.group("key")) + # Public keys (e.g. wireguard's peer_public_key) are not secret; + # redacting them and telling maintainers to mark them cv.sensitive + # would be wrong on both counts. Substitution keys are user-named + # with no schema behind them, so anything secret-shaped there + # (public or not) stays conservatively redacted. + if "public" in key.split("_"): + continue + unmarked.add(key) lines[i] = ( - f"{line[: m.start()]}{m.group('key')}: " + f"{line[: m.start()]}{key}: " f"\\033[8m{m.group('val')}\\033[28m{line[m.end() :]}" ) output = "\n".join(lines) @@ -1647,7 +1662,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: # After BOOTSEL upload, wait for a new serial port to appear # so it shows up in the log chooser - if successful_device is None and CORE.is_rp2040: + if successful_device is None and CORE.is_rp2: _wait_for_serial_port(known_ports=pre_upload_ports) # If exactly one new serial port appeared, use it directly serial_ports = get_serial_ports() @@ -2483,7 +2498,12 @@ def parse_args(argv): # a deprecation warning). arguments = argv[1:] - argcomplete.autocomplete(parser) + # argcomplete only does anything when the shell-completion machinery + # invokes us with _ARGCOMPLETE set; skip the import otherwise. + if "_ARGCOMPLETE" in os.environ: + import argcomplete + + argcomplete.autocomplete(parser) if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS: args, unknown_args = parser.parse_known_args(arguments) @@ -2582,6 +2602,8 @@ def run_esphome(argv): ) if config is None: + from esphome.config import read_config + config = read_config( command_line_substitutions, skip_external_update=skip_external, diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 4fbceb7e5e..ab20e4d076 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -20,6 +20,7 @@ from . import ( RAM_SECTIONS, MemoryAnalyzer, ) +from .toolchain import find_elf_path, find_idedata_path, idedata_candidates if TYPE_CHECKING: from . import ComponentMemory @@ -759,45 +760,25 @@ def main(): print(f"Error: {build_path} is not a directory", file=sys.stderr) sys.exit(1) - # Find firmware.elf - elf_file = None - for elf_candidate in [ - build_path / "firmware.elf", - build_path / ".pioenvs" / build_path.name / "firmware.elf", - ]: - if elf_candidate.exists(): - elf_file = str(elf_candidate) - break - - if not elf_file: - print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr) + elf_path = find_elf_path(build_path) + if not elf_path: + print(f"Error: no firmware ELF found in {build_dir}", file=sys.stderr) sys.exit(1) - - # Find idedata.json - check current directory first, then home - device_name = build_path.name - idedata_candidates = [ - Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json", - Path.home() / ".esphome" / "idedata" / f"{device_name}.json", - ] + elf_file = str(elf_path) idedata = None - for idedata_path in idedata_candidates: - if not idedata_path.exists(): - continue + if idedata_path := find_idedata_path(build_path): try: with idedata_path.open(encoding="utf-8") as f: raw_data = json.load(f) idedata = IDEData(raw_data) print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) - break except (json.JSONDecodeError, OSError) as e: print(f"Warning: Failed to load idedata: {e}", file=sys.stderr) if not idedata: - print( - f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})", - file=sys.stderr, - ) + searched = "\n ".join(str(p) for p in idedata_candidates(build_path)) + print(f"Warning: idedata not found, searched:\n {searched}", file=sys.stderr) analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata) analyzer.analyze() diff --git a/esphome/analyze_memory/ram_strings.py b/esphome/analyze_memory/ram_strings.py index fbcbeeca61..03da86de94 100644 --- a/esphome/analyze_memory/ram_strings.py +++ b/esphome/analyze_memory/ram_strings.py @@ -8,7 +8,7 @@ memory-constrained platforms like ESP8266. from __future__ import annotations from collections import defaultdict -from dataclasses import dataclass +from dataclasses import dataclass, field import logging from pathlib import Path import re @@ -65,6 +65,7 @@ class RamSymbol: size: int section: str demangled: str = "" # Demangled name, set after batch demangling + aliases: list[str] = field(default_factory=list) # Other names at same address class RamStringsAnalyzer: @@ -235,6 +236,11 @@ class RamStringsAnalyzer: except (subprocess.CalledProcessError, FileNotFoundError): return + # Track symbols by address so aliases (multiple names for the same + # object, e.g. the newlib __lock___* mutexes that all alias one + # StaticSemaphore_t) are reported once instead of once per name. + symbols_by_addr: dict[int, RamSymbol] = {} + for line in output.split("\n"): parts = line.split() if len(parts) < 4: @@ -253,6 +259,18 @@ class RamStringsAnalyzer: if sym_type not in DATA_SYMBOL_TYPES: continue + if (existing := symbols_by_addr.get(addr)) is not None: + # Prefer a global (uppercase type) name as the primary so + # nm output order can't hide it behind a local alias. + if sym_type.isupper() and existing.sym_type.islower(): + existing.aliases.append(existing.name) + existing.name = name + existing.sym_type = sym_type + else: + existing.aliases.append(name) + existing.size = max(existing.size, size) + continue + # Check if symbol is in a RAM section for section_name in self.ram_sections: if section_name not in self.sections: @@ -260,15 +278,15 @@ class RamStringsAnalyzer: section = self.sections[section_name] if section.address <= addr < section.address + section.size: - self.ram_symbols.append( - RamSymbol( - name=name, - sym_type=sym_type, - address=addr, - size=size, - section=section_name, - ) + symbol = RamSymbol( + name=name, + sym_type=sym_type, + address=addr, + size=size, + section=section_name, ) + symbols_by_addr[addr] = symbol + self.ram_symbols.append(symbol) break def _demangle_symbols(self) -> None: @@ -436,7 +454,13 @@ class RamStringsAnalyzer: for symbol in largest_symbols: # Use demangled name if available, otherwise raw name display_name = symbol.demangled or symbol.name - name_display = display_name[:49] if len(display_name) > 49 else display_name + # Truncate the name, not the alias note, so merged aliases stay + # visible even for long demangled C++ names. + alias_note = f" (+{len(symbol.aliases)} aliases)" if symbol.aliases else "" + max_name_len = 49 - len(alias_note) + if len(display_name) > max_name_len: + display_name = display_name[:max_name_len] + name_display = display_name + alias_note lines.append( f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}" ) diff --git a/esphome/analyze_memory/toolchain.py b/esphome/analyze_memory/toolchain.py index a724d52f25..19041ac807 100644 --- a/esphome/analyze_memory/toolchain.py +++ b/esphome/analyze_memory/toolchain.py @@ -23,6 +23,78 @@ TOOLCHAIN_PREFIXES = [ ] +def find_elf_path(build_path: Path) -> Path | None: + """Locate the firmware ELF inside an ESPHome build directory. + + The layout depends on the toolchain that produced the build, so try each + known one in turn. + + Args: + build_path: Path to an ESPHome build directory + + Returns: + Path to the ELF file, or None if no known layout matches + """ + name = build_path.name + for candidate in ( + # Native ESP-IDF: idf.py writes build/.elf, which ESPHome copies + # to build/firmware.elf (see espidf.toolchain.create_elf_copy) + build_path / "build" / "firmware.elf", + # PlatformIO + build_path / "firmware.elf", + build_path / ".pioenvs" / name / "firmware.elf", + # LibreTiny uses raw_firmware.elf + build_path / "raw_firmware.elf", + build_path / ".pioenvs" / name / "raw_firmware.elf", + # Zephyr (nRF52); the SDK nests the artifacts one level deeper from 2.9.2 + build_path / ".pioenvs" / name / "zephyr" / "zephyr" / "zephyr.elf", + build_path / ".pioenvs" / name / "zephyr" / "zephyr.elf", + ): + if candidate.is_file(): + return candidate + return None + + +def idedata_candidates(build_path: Path) -> list[Path]: + """Return the idedata locations searched for a build directory, in order. + + Exposed so a caller reporting "not found" can name the paths it tried + without keeping its own copy of the list. + + Args: + build_path: Path to an ESPHome build directory + + Returns: + The candidate idedata JSON paths, most specific first + """ + name = build_path.name + return [ + # In .pioenvs for test builds + build_path / ".pioenvs" / name / "idedata.json", + # Both toolchains cache it in the data dir, which holds this build dir: + # /idedata/.json next to /build/ + build_path.parent.parent / "idedata" / f"{name}.json", + # Regular builds, invoked from the config dir or from anywhere + Path.cwd() / ".esphome" / "idedata" / f"{name}.json", + Path.home() / ".esphome" / "idedata" / f"{name}.json", + ] + + +def find_idedata_path(build_path: Path) -> Path | None: + """Locate the idedata JSON belonging to an ESPHome build directory. + + Args: + build_path: Path to an ESPHome build directory + + Returns: + Path to the idedata JSON, or None if it was not found + """ + for candidate in idedata_candidates(build_path): + if candidate.is_file(): + return candidate + return None + + def _find_in_platformio_packages(tool_name: str) -> str | None: """Search for a tool in PlatformIO package directories. diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index dec6ea04de..cf476555e7 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -6,7 +6,11 @@ from pathlib import Path from esphome.components.esp32 import get_esp32_variant, idf_version import esphome.config_validation as cv from esphome.core import CORE -from esphome.framework_helpers import get_project_compile_flags, get_project_link_flags +from esphome.framework_helpers import ( + get_project_compile_flags, + get_project_cxx_compile_flags, + get_project_link_flags, +) from esphome.helpers import mkdir_p, write_file_if_changed # Replaces the IDF default C++ standard (-std=gnu++2b appended to @@ -91,6 +95,14 @@ def get_project_cmakelists(minimal: bool = False) -> str: for flag in project_compile_opts ) + # Flags registered via cg.add_cxx_build_flag() go on CXX_COMPILE_OPTIONS + # (not COMPILE_OPTIONS) because GCC warns when a C++-only flag such as + # -Wno-volatile is passed on a C compile. + cxx_compile_options = "\n".join( + f'idf_build_set_property(CXX_COMPILE_OPTIONS "{flag}" APPEND)' + for flag in get_project_cxx_compile_flags() + ) + cpp_standard_options = ( CPP_STANDARD_TEMPLATE.format(standard=CORE.cpp_standard) if CORE.cpp_standard @@ -155,6 +167,8 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) {cpp_standard_options} +{cxx_compile_options} + {extra_compile_options} {managed_components_property} @@ -196,15 +210,27 @@ def get_component_cmakelists() -> str: if(CMAKE_SCRIPT_MODE_FILE) file(GLOB_RECURSE app_sources "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp" + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cc" + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cxx" + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.c++" "${{CMAKE_CURRENT_SOURCE_DIR}}/*.c" "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cc" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cxx" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c++" "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c" ) else() file(GLOB_RECURSE app_sources CONFIGURE_DEPENDS "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp" + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cc" + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cxx" + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.c++" "${{CMAKE_CURRENT_SOURCE_DIR}}/*.c" "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cc" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cxx" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c++" "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c" ) endif() diff --git a/esphome/build_gen/platformio.py b/esphome/build_gen/platformio.py index a583279ea7..b63c4b733d 100644 --- a/esphome/build_gen/platformio.py +++ b/esphome/build_gen/platformio.py @@ -108,7 +108,6 @@ Import("env") def write_cxx_flags_script() -> None: path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME) contents = CXX_FLAGS_FILE_CONTENTS - if not CORE.is_host: - contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])' - contents += "\n" + for flag in sorted(CORE.cxx_build_flags): + contents += f'env.Append(CXXFLAGS=["{flag}"])\n' write_file_if_changed(path, contents) diff --git a/esphome/bundle.py b/esphome/bundle.py index d38f68ebfd..dcaea03646 100644 --- a/esphome/bundle.py +++ b/esphome/bundle.py @@ -7,12 +7,12 @@ and compiled directly: ``esphome compile my_device.esphomebundle.tar.gz`` from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import StrEnum import io import json import logging -from pathlib import Path +from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath import re import shutil import tarfile @@ -32,6 +32,8 @@ from esphome.core import CORE, EsphomeError _LOGGER = logging.getLogger(__name__) +DOMAIN = "bundle" + BUNDLE_EXTENSION = ".esphomebundle.tar.gz" MANIFEST_FILENAME = "manifest.json" CURRENT_MANIFEST_VERSION = 1 @@ -49,6 +51,7 @@ class ManifestKey(StrEnum): MANIFEST_VERSION = "manifest_version" ESPHOME_VERSION = "esphome_version" CONFIG_FILENAME = "config_filename" + CONFIG_DIR = "config_dir" FILES = "files" HAS_SECRETS = "has_secrets" @@ -120,6 +123,126 @@ def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]: return keys +@dataclass +class BundleData: + """Files components asked to include, keyed under DOMAIN in CORE.data.""" + + extra_files: list[Path] = field(default_factory=list) + # Original config dir parsed from an extracted bundle's manifest.json, + # kept in the path flavor of the machine the bundle was created on. + # The checked flag makes the manifest lookup happen at most once per run; + # CORE.data is cleared between runs. + original_config_dir: PurePath | None = None + original_config_dir_checked: bool = False + + +def _get_data() -> BundleData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = BundleData() + return CORE.data[DOMAIN] + + +def add_bundle_file(path: Path) -> None: + """Register a file that a bundle must include. + + Bundle discovery walks the validated config, so it only finds files the config + names. Components call this during validation for files it cannot see, such as a + file that is referenced from inside another file. + + A relative path is taken as relative to the config directory. Files outside the + config directory are skipped when the bundle is built. + """ + _get_data().extra_files.append(CORE.relative_config_path(path)) + + +# Windows paths start with a drive letter or contain backslashes; POSIX +# paths do neither in practice, so this is how the flavor of a recorded +# path string is recognized on any host. +_WINDOWS_DRIVE_RE = re.compile(r"^[A-Za-z]:") + + +def _path_flavor(value: str) -> type[PurePath]: + """Pick the pure path class matching the flavor ``value`` was written in.""" + if "\\" in value or _WINDOWS_DRIVE_RE.match(value): + return PureWindowsPath + return PurePosixPath + + +def _load_original_config_dir() -> PurePath | None: + """Read the original config dir from an extracted bundle's manifest. + + Returns None when the current config dir is not an extracted bundle or + the manifest does not record the original config dir. + """ + manifest_path = CORE.config_dir / MANIFEST_FILENAME + try: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + except FileNotFoundError: + # The common case: this config dir is not an extracted bundle. + return None + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as err: + # A manifest.json is present but unreadable or malformed. Say so + # instead of letting it look identical to "not a bundle". + _LOGGER.warning("Bundle: ignoring unreadable %s: %s", manifest_path, err) + return None + if not isinstance(manifest, dict): + return None + # A manifest.json in the config dir does not have to be ours. Only trust + # one that looks like a bundle manifest for exactly this config file. + version = manifest.get(ManifestKey.MANIFEST_VERSION) + if not isinstance(version, int) or version < 1: + return None + if manifest.get(ManifestKey.CONFIG_FILENAME) != CORE.config_path.name: + return None + config_dir = manifest.get(ManifestKey.CONFIG_DIR) + if not isinstance(config_dir, str) or not config_dir: + return None + return _path_flavor(config_dir)(config_dir) + + +def remap_bundle_path(value: str) -> Path | None: + """Remap an absolute path from the machine a bundle was created on. + + A bundled config may reference files by absolute path. The referenced + files ship inside the bundle at their config-relative locations, but the + YAML text is copied verbatim, so after extraction on another machine the + absolute reference points at a path that only existed on the creating + machine. The bundle manifest records that machine's config dir; when + ``value`` names a path that lived under it, return the corresponding + file next to the extracted config. + + ``value`` is the raw path string from the config. It is parsed with the + original machine's path flavor, so a bundle created on Windows remaps on + a POSIX build server and vice versa. + + Returns None when not compiling an extracted bundle, when ``value`` was + not under the original config dir, or when the bundle does not contain + the file. + """ + data = _get_data() + if not data.original_config_dir_checked: + data.original_config_dir_checked = True + data.original_config_dir = _load_original_config_dir() + original_dir = data.original_config_dir + if original_dir is None: + return None + path = type(original_dir)(value) + if not path.is_absolute(): + return None + try: + rel = path.relative_to(original_dir) + except ValueError: + return None + # relative_to is lexical, so ".." segments survive it. Refuse them: the + # remapped file must land strictly inside the extracted config tree. + if ".." in rel.parts: + return None + remapped = CORE.relative_config_path(Path(*rel.parts)) + if not remapped.exists(): + return None + return remapped + + @dataclass class BundleFile: """A file to include in the bundle.""" @@ -146,6 +269,7 @@ class BundleManifest: config_filename: str files: list[str] has_secrets: bool + config_dir: str | None = None class ConfigBundleCreator: @@ -286,13 +410,18 @@ class ConfigBundleCreator: with known file extensions are also resolved and checked. Core ESPHome concepts that use relative paths or directories - are handled explicitly. + are handled explicitly. Files the config does not name at all are + registered by their component with add_bundle_file(). """ config = self._config # Generic walk: find all file paths in the validated config self._walk_config_for_files(config) + # Files registered by components during validation + for extra_file in _get_data().extra_files: + self._add_file(extra_file) + # --- Core ESPHome concepts needing explicit handling --- # esphome.includes / includes_c - can be relative paths and directories @@ -405,6 +534,7 @@ class ConfigBundleCreator: ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION, ManifestKey.ESPHOME_VERSION: const.__version__, ManifestKey.CONFIG_FILENAME: self._config_path.name, + ManifestKey.CONFIG_DIR: str(self._config_dir), ManifestKey.FILES: [f.path for f in files], ManifestKey.HAS_SECRETS: has_secrets, } @@ -489,12 +619,14 @@ def read_bundle_manifest(bundle_path: Path) -> BundleManifest: except tarfile.TarError as err: raise EsphomeError(f"Failed to read bundle: {err}") from err + config_dir = manifest.get(ManifestKey.CONFIG_DIR) return BundleManifest( manifest_version=manifest[ManifestKey.MANIFEST_VERSION], esphome_version=manifest.get(ManifestKey.ESPHOME_VERSION, "unknown"), config_filename=manifest[ManifestKey.CONFIG_FILENAME], files=manifest.get(ManifestKey.FILES, []), has_secrets=manifest.get(ManifestKey.HAS_SECRETS, False), + config_dir=config_dir if isinstance(config_dir, str) else None, ) diff --git a/esphome/codegen.py b/esphome/codegen.py index a5b5abe447..0694eb4d84 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -25,6 +25,7 @@ from esphome.cpp_generator import ( # noqa: F401 add, add_build_flag, add_build_unflag, + add_cxx_build_flag, add_define, add_global, add_library, @@ -52,6 +53,7 @@ from esphome.cpp_helpers import ( # noqa: F401 past_safe_mode, register_component, register_parented, + set_setup_priority, ) from esphome.cpp_types import ( # noqa: F401 NAN, diff --git a/esphome/components/__init__.py b/esphome/components/__init__.py index e69de29bb2..3d7a546253 100644 --- a/esphome/components/__init__.py +++ b/esphome/components/__init__.py @@ -0,0 +1,6 @@ +# Importing `esphome.loader` here installs the component-alias +# ``sys.meta_path`` finder before any submodule lookup runs. Without this, +# `from esphome.components import ` from a fresh interpreter +# can race the finder install and raise ImportError, since the legacy +# alias dir no longer exists on disk. +from esphome import loader as _loader # noqa: F401 diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 96c8334a6d..555d511f6e 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -227,12 +227,12 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { def validate_adc_pin(value): if str(value).upper() == "VCC": - if CORE.is_rp2040: + if CORE.is_rp2: return pins.internal_gpio_input_pin_schema(29) return cv.only_on([PLATFORM_ESP8266])("VCC") if str(value).upper() == "TEMPERATURE": - return cv.only_on_rp2040("TEMPERATURE") + return cv.only_on_rp2("TEMPERATURE") if CORE.is_esp32: conf = pins.internal_gpio_input_pin_schema(value) @@ -261,11 +261,11 @@ def validate_adc_pin(value): raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") return conf - if CORE.is_rp2040: + if CORE.is_rp2: conf = pins.internal_gpio_input_pin_schema(value) number = conf[CONF_NUMBER] if number not in (26, 27, 28, 29): - raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC") + raise cv.Invalid("RP2: Only pins 26, 27, 28 and 29 support ADC") return conf if CORE.is_libretiny: diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 03de6f8b4b..7131898747 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -123,9 +123,9 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v void set_autorange(bool autorange) { this->autorange_ = autorange; } #endif // USE_ESP32 -#ifdef USE_RP2040 +#ifdef USE_RP2 void set_is_temperature() { this->is_temperature_ = true; } -#endif // USE_RP2040 +#endif // USE_RP2 protected: uint8_t sample_count_{1}; @@ -152,9 +152,9 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v static adc_oneshot_unit_handle_t shared_adc_handles[2]; #endif // USE_ESP32 -#ifdef USE_RP2040 +#ifdef USE_RP2 bool is_temperature_{false}; -#endif // USE_RP2040 +#endif // USE_RP2 #ifdef USE_ZEPHYR const struct adc_dt_spec *channel_ = nullptr; diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2.cpp similarity index 97% rename from esphome/components/adc/adc_sensor_rp2040.cpp rename to esphome/components/adc/adc_sensor_rp2.cpp index 894c346588..6cb9ef113f 100644 --- a/esphome/components/adc/adc_sensor_rp2040.cpp +++ b/esphome/components/adc/adc_sensor_rp2.cpp @@ -1,4 +1,4 @@ -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "adc_sensor.h" #include "esphome/core/log.h" @@ -17,7 +17,7 @@ namespace esphome::adc { -static const char *const TAG = "adc.rp2040"; +static const char *const TAG = "adc.rp2"; void ADCSensor::setup() { static bool initialized = false; @@ -102,4 +102,4 @@ float ADCSensor::sample() { } // namespace esphome::adc -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 27442d1837..c5a4288c07 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -211,7 +211,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, - "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "adc_sensor_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, "adc_sensor_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 9c9c7e3871..0df7c56313 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -1,114 +1,36 @@ -import logging +# --------------------------------------------------------------------------- +# Legacy top-level `animation:` deprecation shim -- REMOVE this whole file after +# 2027.1.0. +# +# Animations are now a platform of the `image:` component (`platform: +# animation`); the real schema, actions and codegen live in `image.py`. This +# module only keeps the deprecated top-level `animation:` key working during the +# deprecation window: it reuses that schema/codegen and adds a one-shot +# deprecation warning (with a pasteable migrated `image:` block) at validation +# time. Deleting this file drops the top-level form entirely. +# --------------------------------------------------------------------------- -from esphome import automation -import esphome.codegen as cg -from esphome.components.const import CONF_LOOP import esphome.components.image as espImage import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_REPEAT -_LOGGER = logging.getLogger(__name__) +from .image import ANIMATION_CONFIG_SCHEMA, setup_animation -AUTO_LOAD = ["image"] +AUTO_LOAD = ["image", "file"] CODEOWNERS = ["@syndlex"] DEPENDENCIES = ["display"] MULTI_CONF = True MULTI_CONF_NO_DEFAULT = True -CONF_START_FRAME = "start_frame" -CONF_END_FRAME = "end_frame" -CONF_FRAME = "frame" +DOMAIN = "animation" -animation_ns = cg.esphome_ns.namespace("animation") +LEGACY_REMOVAL_VERSION = "2027.1.0" -Animation_ = animation_ns.class_("Animation", espImage.Image_) - -# Actions -NextFrameAction = animation_ns.class_( - "AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_) -) -PrevFrameAction = animation_ns.class_( - "AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_) -) -SetFrameAction = animation_ns.class_( - "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) +_capture_legacy_entry, _warn_legacy_animation = ( + espImage.legacy_platform_migration_warning(DOMAIN, DOMAIN, LEGACY_REMOVAL_VERSION) ) -CONFIG_SCHEMA = cv.All( - espImage.IMAGE_SCHEMA.extend( - { - cv.Required(CONF_ID): cv.declare_id(Animation_), - cv.Optional(CONF_LOOP): cv.All( - { - cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, - cv.Optional(CONF_END_FRAME): cv.positive_int, - cv.Optional(CONF_REPEAT): cv.positive_int, - } - ), - }, - ), - espImage.validate_settings, -) +CONFIG_SCHEMA = cv.All(_capture_legacy_entry, ANIMATION_CONFIG_SCHEMA) +FINAL_VALIDATE_SCHEMA = _warn_legacy_animation -NEXT_FRAME_SCHEMA = automation.maybe_simple_id( - { - cv.GenerateID(): cv.use_id(Animation_), - } -) -PREV_FRAME_SCHEMA = automation.maybe_simple_id( - { - cv.GenerateID(): cv.use_id(Animation_), - } -) -SET_FRAME_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(Animation_), - cv.Required(CONF_FRAME): cv.uint16_t, - } -) - - -@automation.register_action( - "animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True -) -@automation.register_action( - "animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True -) -@automation.register_action( - "animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True -) -async def animation_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - - if (frame := config.get(CONF_FRAME)) is not None: - template_ = await cg.templatable(frame, args, cg.uint16) - cg.add(var.set_frame(template_)) - return var - - -async def to_code(config): - ( - prog_arr, - width, - height, - image_type, - trans_value, - frame_count, - ) = await espImage.write_image(config, all_frames=True) - - var = cg.new_Pvariable( - config[CONF_ID], - prog_arr, - width, - height, - frame_count, - image_type, - trans_value, - ) - if loop_config := config.get(CONF_LOOP): - start = loop_config[CONF_START_FRAME] - end = loop_config.get(CONF_END_FRAME, frame_count) - count = loop_config.get(CONF_REPEAT, -1) - cg.add(var.set_loop(start, end, count)) +to_code = setup_animation diff --git a/esphome/components/animation/image.py b/esphome/components/animation/image.py new file mode 100644 index 0000000000..95875fe2b0 --- /dev/null +++ b/esphome/components/animation/image.py @@ -0,0 +1,115 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components.const import CONF_LOOP +from esphome.components.file.image import image_schema, write_image +from esphome.components.image import Image_, validate_settings +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_REPEAT +from esphome.types import ConfigType + +CODEOWNERS = ["@syndlex"] +AUTO_LOAD = ["file"] +DEPENDENCIES = ["display"] + +CONF_START_FRAME = "start_frame" +CONF_END_FRAME = "end_frame" +CONF_FRAME = "frame" + +animation_ns = cg.esphome_ns.namespace("animation") + +Animation_ = animation_ns.class_("Animation", Image_) + +# Actions +NextFrameAction = animation_ns.class_( + "AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_) +) +PrevFrameAction = animation_ns.class_( + "AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_) +) +SetFrameAction = animation_ns.class_( + "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) +) + +ANIMATION_SCHEMA = image_schema(Animation_).extend( + { + cv.Optional(CONF_LOOP): cv.All( + { + cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, + cv.Optional(CONF_END_FRAME): cv.positive_int, + cv.Optional(CONF_REPEAT): cv.positive_int, + } + ), + }, +) + +# Shared schema used by both the (deprecated) top-level `animation:` key and the +# `image:` `platform: animation` entry. +ANIMATION_CONFIG_SCHEMA = cv.All(ANIMATION_SCHEMA, validate_settings) + + +NEXT_FRAME_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(Animation_), + } +) +PREV_FRAME_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(Animation_), + } +) +SET_FRAME_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(Animation_), + cv.Required(CONF_FRAME): cv.uint16_t, + } +) + + +@automation.register_action( + "animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True +) +@automation.register_action( + "animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True +) +@automation.register_action( + "animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True +) +async def animation_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + if (frame := config.get(CONF_FRAME)) is not None: + template_ = await cg.templatable(frame, args, cg.uint16) + cg.add(var.set_frame(template_)) + return var + + +async def setup_animation(config: ConfigType) -> None: + ( + prog_arr, + width, + height, + image_type, + trans_value, + frame_count, + ) = await write_image(config, all_frames=True) + + var = cg.new_Pvariable( + config[CONF_ID], + prog_arr, + width, + height, + frame_count, + image_type, + trans_value, + ) + if loop_config := config.get(CONF_LOOP): + start = loop_config[CONF_START_FRAME] + end = loop_config.get(CONF_END_FRAME, frame_count) + count = loop_config.get(CONF_REPEAT, -1) + cg.add(var.set_loop(start, end, count)) + + +CONFIG_SCHEMA = ANIMATION_CONFIG_SCHEMA + +to_code = setup_animation diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 1146b43596..0719cee352 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -112,6 +112,23 @@ CONF_MAX_SEND_QUEUE = "max_send_queue" CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only" +def _register_provisioning_source(config: ConfigType) -> ConfigType: + """Register the API as a provisioning source when encryption is enabled. + + With no ``key`` the device boots unprovisioned and is set up on first + connection; a YAML ``key`` means it is born provisioned. Either way the API + drives the provisioning manager, so it counts as a source for `provisioning:`. + A hardcoded ``key`` is reported so `provisioning:` can warn about it. + """ + if (encryption := config.get(CONF_ENCRYPTION)) is not None: + from esphome.components import provisioning + + provisioning.register_source("api") + if CONF_KEY in encryption: + provisioning.report_hardcoded_credentials("api") + return config + + def validate_encryption_key(value): value = cv.string_strict(value) try: @@ -300,7 +317,7 @@ CONFIG_SCHEMA = cv.All( CONF_LISTEN_BACKLOG, esp8266=1, # Limited RAM (~40KB free), LWIP raw sockets esp32=4, # More RAM (520KB), BSD sockets - rp2040=1, # Limited RAM (264KB), LWIP raw sockets like ESP8266 + rp2=1, # Limited RAM (264KB), LWIP raw sockets like ESP8266 bk72xx=4, # Moderate RAM, BSD-style sockets rtl87xx=4, # Moderate RAM, BSD-style sockets host=4, # Abundant resources @@ -311,7 +328,7 @@ CONFIG_SCHEMA = cv.All( CONF_MAX_CONNECTIONS, esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes esp32=5, # 520KB RAM available - rp2040=4, # 264KB RAM but LWIP constraints + rp2=4, # 264KB RAM but LWIP constraints bk72xx=5, # Moderate RAM rtl87xx=5, # Moderate RAM host=8, # Abundant resources @@ -326,7 +343,7 @@ CONFIG_SCHEMA = cv.All( CONF_MAX_SEND_QUEUE, esp8266=4, # Limited RAM, need to fail fast esp32=8, # More RAM, can buffer more - rp2040=8, # Moderate RAM + rp2=8, # Moderate RAM bk72xx=8, # Moderate RAM nrf52=8, # Moderate RAM rtl87xx=8, # Moderate RAM @@ -337,6 +354,7 @@ CONFIG_SCHEMA = cv.All( ).extend(cv.COMPONENT_SCHEMA), cv.rename_key(CONF_SERVICES, CONF_ACTIONS), _consume_api_sockets, + _register_provisioning_source, ) @@ -470,8 +488,11 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_NOISE_PSK_FROM_YAML") else: # No key provided, but encryption desired - # This will allow a plaintext client to provide a noise key, - # send it to the device, and then switch to noise. + # Until a key is set, the device accepts both Noise connections + # using the well-known all-zeros PSK (preferred: the key travels + # encrypted, protecting against passive sniffing) and plaintext + # connections (deprecated, remove after 2027.2.0) so a client can + # provide a noise key and the device then switches to noise only. # The key will be saved in flash and used for future connections # and plaintext disabled. Only a factory reset can remove it. cg.add_define("USE_API_PLAINTEXT") diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index f4f15c1042..4b3df62ec4 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -158,6 +158,16 @@ message AuthenticationResponse { bool invalid_password = 1; } +// Reason a party is requesting the connection be closed. +enum DisconnectReason { + // No specific reason / not provided (default for older peers). + DISCONNECT_REASON_UNSPECIFIED = 0; + // The device's provisioning window has expired. The device must be reset + // (power-cycled) to reopen the provisioning window before it will accept a + // connection again. + DISCONNECT_REASON_PROVISIONING_CLOSED = 1; +} + // Request to close the connection. // Can be sent by both the client and server message DisconnectRequest { @@ -166,6 +176,10 @@ message DisconnectRequest { option (no_delay) = true; // Do not close the connection before the acknowledgement arrives + + // Optional reason the connection is being closed. Older peers that do not + // send this field will report DISCONNECT_REASON_UNSPECIFIED (0). + DisconnectReason reason = 1; } message DisconnectResponse { @@ -296,6 +310,11 @@ message DeviceInfoResponse { // Serial proxy instance metadata repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"]; + + // Device is unprovisioned and accepts Noise handshakes with the well-known + // all-zeros PSK, so the api encryption key can be provisioned without being + // sent in plaintext (protects against passive sniffing, not active MITM) + bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"]; } message ListEntitiesRequest { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index acdf24e747..3dc2a06c85 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -25,6 +25,9 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/version.h" +#ifdef USE_PROVISIONING +#include "esphome/components/provisioning/provisioning.h" +#endif #ifdef USE_DEEP_SLEEP #include "esphome/components/deep_sleep/deep_sleep_component.h" @@ -195,6 +198,29 @@ APIConnection::~APIConnection() { #endif } +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) +void APIConnection::upgrade_helper_to_noise_() { + // The client opened with a Noise hello while this device has no encryption + // key set. Replace the plaintext helper with a Noise helper so the key can + // be provisioned over an encrypted channel: the noise context PSK is all + // zeros when unprovisioned, and NNpsk0 still runs a fresh ephemeral X25519 + // exchange, so a passive listener cannot read the session. A publicly known + // PSK authenticates nobody; this protects against sniffing only. + auto *plaintext = static_cast(this->helper_.get()); + uint8_t header[3]; + uint8_t header_len = plaintext->get_consumed_header(header); + auto *noise = new APINoiseFrameHelper(plaintext->release_socket_for_switch(), this->parent_->get_noise_ctx()); + // Carry over the peername-based client name (Hello has not arrived yet) + const char *name = plaintext->get_client_name(); + noise->set_client_name(name, strlen(name)); + this->helper_.reset(noise); // destroys the plaintext helper + APIError err = noise->init_from_handoff(header, header_len); + if (err != APIError::OK) { + this->fatal_error_with_log_(LOG_STR("Noise handoff failed"), err); + } +} +#endif // USE_API_NOISE && USE_API_PLAINTEXT + void APIConnection::destroy_active_iterator_() { switch (this->active_iterator_) { case ActiveIterator::LIST_ENTITIES: @@ -253,6 +279,15 @@ void APIConnection::loop() { // No more data available break; } else if (err != APIError::OK) { +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) + // Checked inside the error branch to keep the hot err == OK path + // free of it; this can only fire on the first bytes of a plaintext + // helper on an unprovisioned device + if (err == APIError::PROTOCOL_SWITCH_TO_NOISE) { + this->upgrade_helper_to_noise_(); + return; + } +#endif this->fatal_error_with_log_(LOG_STR("Reading failed"), err); return; } else { @@ -759,6 +794,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); // Current feature flags and other supported parameters msg.feature_flags = traits.get_feature_flags(); + msg.temperature_unit = static_cast(traits.get_temperature_unit()); msg.supported_modes = &traits.get_supported_modes(); msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); @@ -1293,7 +1329,8 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno } } -bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) { +bool APIConnection::send_voice_assistant_get_configuration_response_( + const VoiceAssistantConfigurationRequest & /*msg*/) { VoiceAssistantConfigurationResponse resp; if (!this->check_voice_assistant_api_connection_()) { // send_message encodes synchronously, so this stack local outlives the encode @@ -1313,22 +1350,6 @@ bool APIConnection::send_voice_assistant_get_configuration_response_(const Voice } } - // Filter external wake words - for (auto &wake_word : msg.external_wake_words) { - if (wake_word.model_type != "micro") { - // microWakeWord only - continue; - } - - resp.available_wake_words.emplace_back(); - auto &resp_wake_word = resp.available_wake_words.back(); - resp_wake_word.id = StringRef(wake_word.id); - resp_wake_word.wake_word = StringRef(wake_word.wake_word); - for (const auto &lang : wake_word.trained_languages) { - resp_wake_word.trained_languages.push_back(lang); - } - } - resp.active_wake_words = &config.active_wake_words; resp.max_active_wake_words = config.max_active_wake_words; return this->send_message(resp); @@ -1348,7 +1369,7 @@ void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSet #ifdef USE_ZWAVE_PROXY void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { - zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len); + zwave_proxy::global_zwave_proxy->send_frame(this, msg.data, msg.data_len); } void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { @@ -1433,6 +1454,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec msg.target_temperature_step = traits.get_target_temperature_step(); msg.supported_modes = &traits.get_supported_modes(); msg.supported_features = traits.get_feature_flags(); + msg.temperature_unit = static_cast(traits.get_temperature_unit()); return fill_and_encode_entity_info(wh, msg, conn, remaining_size); } @@ -1521,8 +1543,8 @@ void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigure static_cast(proxies.size())); return; } - proxies[msg.instance]->configure(msg.baudrate, msg.flow_control, static_cast(msg.parity), msg.stop_bits, - msg.data_size); + proxies[msg.instance]->configure(this, msg.baudrate, msg.flow_control, static_cast(msg.parity), + msg.stop_bits, msg.data_size); } void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) { @@ -1531,7 +1553,7 @@ void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } - proxies[msg.instance]->write_from_client(msg.data, msg.data_len); + proxies[msg.instance]->write_from_client(this, msg.data, msg.data_len); } void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) { @@ -1540,7 +1562,7 @@ void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetM ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } - proxies[msg.instance]->set_modem_pins(msg.line_states); + proxies[msg.instance]->set_modem_pins(this, msg.line_states); } void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) { @@ -1711,12 +1733,6 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(), this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); - // TODO: Remove before 2026.8.0 (one version after get_object_id backward compat removal) - if (!this->client_supports_api_version(1, 14)) { - ESP_LOGW(TAG, "'%s' using outdated API %" PRIu16 ".%" PRIu16 ", update to 1.14+", this->helper_->get_client_name(), - this->client_api_version_major_, this->client_api_version_minor_); - } - HelloResponse resp; resp.api_version_major = 1; resp.api_version_minor = 14; @@ -1724,6 +1740,19 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { resp.server_info = ESPHOME_VERSION_REF; resp.name = StringRef(App.get_name()); +#ifdef USE_PROVISIONING + if (provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed()) { + // The provisioning window has closed without the device being provisioned. + // Acknowledge the hello so the client can read the server name, then request + // disconnect with the reason. Authentication is intentionally not completed. + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Provisioning closed; rejecting connection")); + this->send_message(resp); + DisconnectRequest req; + req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED; + return this->send_message(req); + } +#endif + // Auto-authenticate - password auth was removed in ESPHome 2026.1.0 this->complete_authentication_(); @@ -1759,7 +1788,7 @@ bool APIConnection::send_device_info_response_() { // Manufacturer string - define once, handle ESP8266 PROGMEM separately #if defined(USE_ESP8266) || defined(USE_ESP32) #define ESPHOME_MANUFACTURER "Espressif" -#elif defined(USE_RP2040) +#elif defined(USE_RP2) #define ESPHOME_MANUFACTURER "Raspberry Pi" #elif defined(USE_BK72XX) #define ESPHOME_MANUFACTURER "Beken" @@ -1844,6 +1873,12 @@ bool APIConnection::send_device_info_response_() { #endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; +#ifndef USE_API_NOISE_PSK_FROM_YAML + // No key from YAML: while no key is set, the key can be provisioned over a + // zero-PSK Noise connection. Gated on the YAML define (not the plaintext + // one) so this advertisement survives the plaintext removal in 2027.2.0. + resp.api_encryption_provisionable = !this->parent_->get_noise_ctx().has_psk(); +#endif #endif #ifdef USE_DEVICES size_t device_index = 0; @@ -1874,7 +1909,8 @@ void APIConnection::on_hello_request(const HelloRequest &msg) { this->on_fatal_error(); } } -void APIConnection::on_disconnect_request() { +void APIConnection::on_disconnect_request(const DisconnectRequest & /*msg*/) { + // The reason is informational when a client disconnects us; we always ack and close. if (!this->send_disconnect_response_()) { this->on_fatal_error(); } @@ -2002,6 +2038,15 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio NoiseEncryptionSetKeyResponse resp; resp.success = false; +#ifdef USE_PROVISIONING + // Refuse to set a key once the provisioning window has closed (defense in depth; + // such connections are already rejected at hello). + if (provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed()) { + ESP_LOGW(TAG, "Provisioning closed; rejecting key set"); + return this->send_message(resp); + } +#endif + psk_t psk{}; if (msg.key_len == 0) { if (this->parent_->clear_noise_psk(true)) { @@ -2011,10 +2056,21 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio } } else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) { ESP_LOGW(TAG, "Invalid encryption key length"); + } else if (APINoiseContext::is_all_zeros(psk)) { + // Accepting the reserved provisioning PSK would report success without + // enabling encryption (or silently clear an existing key) + ESP_LOGW(TAG, "Rejecting all-zero encryption key"); } else if (!this->parent_->save_noise_psk(psk, true)) { ESP_LOGW(TAG, "Failed to save encryption key"); } else { resp.success = true; +#ifdef USE_API_PLAINTEXT + if (this->helper_->frame_footer_size() == 0) { + // Plaintext transport has no frame footer; Noise always has the MAC footer. + // Remove after 2027.2.0 together with plaintext support on keyless devices. + ESP_LOGW(TAG, "Key received over plaintext; deprecated, will be removed in 2027.2.0"); + } +#endif } return this->send_message(resp); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 92f7065730..7df7ea1429 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -18,8 +18,8 @@ #ifdef USE_ESP32_CRASH_HANDLER #include "esphome/components/esp32/crash_handler.h" #endif -#ifdef USE_RP2040_CRASH_HANDLER -#include "esphome/components/rp2040/crash_handler.h" +#ifdef USE_RP2_CRASH_HANDLER +#include "esphome/components/rp2/crash_handler.h" #endif #ifdef USE_ESP8266_CRASH_HANDLER #include "esphome/components/esp8266/crash_handler.h" @@ -166,10 +166,14 @@ class APIConnection final : public APIServerConnectionBase { #endif bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); #ifdef USE_API_HOMEASSISTANT_SERVICES - void send_homeassistant_action(const HomeassistantActionRequest &call) { + // Returns whether this client has subscribed to Home Assistant actions; the message + // is only handed to the send path when subscribed. A true return does not guarantee + // delivery - it lets the caller warn when no connected client has the subscription. + bool send_homeassistant_action(const HomeassistantActionRequest &call) { if (!this->flags_.service_call_subscription) - return; + return false; this->send_message(call); + return true; } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES void on_homeassistant_action_response(const HomeassistantActionResponse &msg); @@ -259,7 +263,7 @@ class APIConnection final : public APIServerConnectionBase { void on_get_time_response(const GetTimeResponse &value); #endif void on_hello_request(const HelloRequest &msg); - void on_disconnect_request(); + void on_disconnect_request(const DisconnectRequest &msg); void on_ping_request(); void on_device_info_request(); void on_list_entities_request() { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } @@ -279,8 +283,8 @@ class APIConnection final : public APIServerConnectionBase { esp32::crash_handler_log(); esp32::crash_handler_clear(); #endif -#ifdef USE_RP2040_CRASH_HANDLER - rp2040::crash_handler_log(); +#ifdef USE_RP2_CRASH_HANDLER + rp2::crash_handler_log(); #endif #ifdef USE_ESP8266_CRASH_HANDLER esp8266::crash_handler_log(); @@ -626,6 +630,11 @@ class APIConnection final : public APIServerConnectionBase { void destroy_active_iterator_(); void begin_iterator_(ActiveIterator type); void finalize_iterator_sync_(); +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) + // Swap the plaintext helper for a Noise helper after the client opened + // with a Noise hello on an unprovisioned device (zero-PSK provisioning). + void upgrade_helper_to_noise_(); +#endif #ifdef USE_CAMERA std::unique_ptr image_reader_; #endif diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 90353b6402..7425304766 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -97,6 +97,8 @@ const LogString *api_error_to_logstr(APIError err) { return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE"); } #endif + // PROTOCOL_SWITCH_TO_NOISE is intercepted in APIConnection::loop() before + // any logging can happen, so it intentionally has no entry here. return LOG_STR("UNKNOWN"); } diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index f98eca8076..9cae6ba92e 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -88,6 +88,11 @@ enum class APIError : uint16_t { HANDSHAKESTATE_SPLIT_FAILED = 1020, BAD_HANDSHAKE_ERROR_BYTE = 1021, #endif +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) + // Not an error: an unprovisioned device received a Noise client hello on a + // plaintext connection; the caller must hand the socket off to a Noise helper. + PROTOCOL_SWITCH_TO_NOISE = 1023, +#endif }; const LogString *api_error_to_logstr(APIError err); @@ -200,6 +205,12 @@ class APIFrameHelper { // or track that they stopped early and retry without this check. // See Socket::ready() for details. bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) + // Move the socket out of this helper so a replacement helper can take it + // over (plaintext to Noise handoff on unprovisioned devices). The drained + // helper must be destroyed right after. + std::unique_ptr release_socket_for_switch() { return std::move(this->socket_); } +#endif // Release excess memory from internal buffers after initial sync void release_buffers() { // rx_buf_: Safe to clear only if no partial read in progress. diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 6dba64a7f8..225bac51a6 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -109,6 +109,40 @@ APIError APINoiseFrameHelper::init() { state_ = State::CLIENT_HELLO; return APIError::OK; } +#ifdef USE_API_PLAINTEXT +APIError APINoiseFrameHelper::init_from_handoff(const uint8_t *header, uint8_t header_len) { + APIError err = this->init(); + if (err != APIError::OK) { + return err; + } + // Seed the header bytes the plaintext helper consumed before detecting the + // Noise indicator; try_read_frame_ resumes from rx_header_buf_len_. + std::memcpy(this->rx_header_buf_, header, header_len); + this->rx_header_buf_len_ = header_len; + // Pump the handshake without gating on socket_->ready(): on LWIP the + // plaintext helper's partial read can drain rcvevent while the rest of the + // client hello sits in the lastdata cache, so ready() may report false even + // though data is available. + return this->pump_handshake_(); +} +#endif // USE_API_PLAINTEXT + +/// Drive the handshake state machine until DATA, WOULD_BLOCK, or a fatal +/// error. WOULD_BLOCK is not an error: reads stop naturally on EWOULDBLOCK +/// and resume on the next loop(). +APIError APINoiseFrameHelper::pump_handshake_() { + while (this->state_ != State::DATA) { + APIError err = this->state_action_(); + if (err == APIError::WOULD_BLOCK) { + break; + } + if (err != APIError::OK) { + return err; + } + } + return APIError::OK; +} + // Helper for handling handshake frame errors APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { if (aerr == APIError::BAD_INDICATOR) { @@ -131,16 +165,13 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { - // Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once - // the rx buffer is consumed. Re-checking each iteration would block handshake writes - // that must follow reads, deadlocking the handshake. state_action() will return - // WOULD_BLOCK when no more data is available to read. - bool socket_ready = this->socket_->ready(); - while (state_ != State::DATA && socket_ready) { - APIError err = state_action_(); - if (err == APIError::WOULD_BLOCK) { - break; - } + // Check ready() once, not per state transition. On ESP8266 LWIP raw TCP, + // ready() returns false once the rx buffer is consumed. Re-checking each + // iteration would block handshake writes that must follow reads, + // deadlocking the handshake. pump_handshake_() stops on WOULD_BLOCK when + // no more data is available to read. + if (state_ != State::DATA && this->socket_->ready()) { + APIError err = this->pump_handshake_(); if (err != APIError::OK) { return err; } diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 0676eab78d..b0ba9fd01c 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -22,12 +22,20 @@ class APINoiseFrameHelper final : public APIFrameHelper { } ~APINoiseFrameHelper() override; APIError init() override; +#ifdef USE_API_PLAINTEXT + // Take over a connection whose first bytes were consumed by a plaintext + // helper on an unprovisioned device (see APIError::PROTOCOL_SWITCH_TO_NOISE). + // Seeds the already-read header bytes and pumps the handshake state machine + // until it would block. + APIError init_from_handoff(const uint8_t *header, uint8_t header_len); +#endif APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) override; protected: + APIError pump_handshake_(); APIError state_action_(); APIError state_action_client_hello_(); APIError state_action_server_hello_(); diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index fa611a6e33..9359f568fb 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -89,6 +89,17 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { // If this was the first read, validate the indicator byte if (rx_header_buf_pos_ == 0 && received > 0) { if (rx_header_buf_[0] != 0x00) { +#ifdef USE_API_NOISE + // Dual build (encryption supported but no key set): a 0x01 first byte + // is a Noise client hello. Hand the connection off to a Noise helper + // running the all-zeros provisioning PSK so the encryption key can be + // set without crossing the wire in plaintext. Preserve the bytes we + // already consumed; they are the start of the Noise 3-byte header. + if (rx_header_buf_[0] == 0x01) { + rx_header_buf_pos_ = static_cast(received); + return APIError::PROTOCOL_SWITCH_TO_NOISE; + } +#endif state_ = State::FAILED; HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); return APIError::BAD_INDICATOR; diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 8314754715..ea3f6d7280 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -23,6 +23,15 @@ class APIPlaintextFrameHelper final : public APIFrameHelper { APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) override; +#ifdef USE_API_NOISE + // After try_read_frame_ returned PROTOCOL_SWITCH_TO_NOISE: copy out the + // header bytes already consumed from the socket (at most 3, the size of the + // Noise fixed header) so the replacement Noise helper can be seeded with them. + uint8_t get_consumed_header(uint8_t out[3]) const { + memcpy(out, this->rx_header_buf_, this->rx_header_buf_pos_); + return this->rx_header_buf_pos_; + } +#endif protected: APIError try_read_frame_(); diff --git a/esphome/components/api/api_noise_context.h b/esphome/components/api/api_noise_context.h index b5f7016689..44484ffa2c 100644 --- a/esphome/components/api/api_noise_context.h +++ b/esphome/components/api/api_noise_context.h @@ -10,13 +10,20 @@ using psk_t = std::array; class APINoiseContext { public: + // The all-zeros PSK is reserved: it marks the device as unprovisioned and + // doubles as the well-known provisioning PSK that unprovisioned devices + // accept for Noise handshakes (passive-sniffing protection only, no + // authentication). It is never a valid real key. + static bool is_all_zeros(const psk_t &psk) { + uint8_t acc = 0; + for (uint8_t b : psk) { + acc |= b; + } + return acc == 0; + } void set_psk(psk_t psk) { this->psk_ = psk; - bool has_psk = false; - for (auto i : psk) { - has_psk |= i; - } - this->has_psk_ = has_psk; + this->has_psk_ = !is_all_zeros(psk); } const psk_t &get_psk() const { return this->psk_; } bool has_psk() const { return this->has_psk_; } diff --git a/esphome/components/api/api_overflow_buffer.cpp b/esphome/components/api/api_overflow_buffer.cpp index e242d4553e..a57a2fb1bb 100644 --- a/esphome/components/api/api_overflow_buffer.cpp +++ b/esphome/components/api/api_overflow_buffer.cpp @@ -12,6 +12,22 @@ APIOverflowBuffer::~APIOverflowBuffer() { } ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) { + // socket->write() can re-enter this function: a log message emitted from an + // lwip callback during the write goes out over the API and lands back in the + // frame helper's write/drain path. If a nested drain ran here it would send + // and free the entry the outer drain is still holding, causing a double free. + // Report "no progress" instead; the outer drain keeps draining, and the + // nested send is enqueued behind the existing backlog. + if (this->draining_) + return 0; + + // RAII so the flag is cleared on every return path + struct DrainGuard { + explicit DrainGuard(bool &flag) : flag_(flag) { flag_ = true; } + ~DrainGuard() { this->flag_ = false; } + bool &flag_; + } guard(this->draining_); + while (this->count_ > 0) { Entry *front = this->queue_[this->head_]; @@ -29,11 +45,12 @@ ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) { return sent; } - // Entry fully sent — free it and advance - Entry::destroy(front); + // Entry fully sent — unlink it before freeing so a freed pointer is never + // reachable from the queue this->queue_[this->head_] = nullptr; this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE; this->count_--; + Entry::destroy(front); } return 0; // All drained diff --git a/esphome/components/api/api_overflow_buffer.h b/esphome/components/api/api_overflow_buffer.h index 19aae680f0..1227e83126 100644 --- a/esphome/components/api/api_overflow_buffer.h +++ b/esphome/components/api/api_overflow_buffer.h @@ -69,6 +69,10 @@ class APIOverflowBuffer { uint8_t head_{0}; uint8_t tail_{0}; uint8_t count_{0}; + // Guards against re-entrant drains: socket->write() can re-enter the API + // send path (e.g. a log message emitted from an lwip callback), and a nested + // drain would free the entry the outer drain is still holding. + bool draining_{false}; }; } // namespace esphome::api diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index c711ef167c..190bd32425 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -47,6 +47,26 @@ uint32_t HelloResponse::calculate_size() const { size += 2 + this->name.size(); return size; } +bool DisconnectRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->reason = static_cast(value); + break; + default: + return false; + } + return true; +} +uint8_t *DisconnectRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast(this->reason)); + return pos; +} +uint32_t DisconnectRequest::calculate_size() const { + uint32_t size = 0; + size += this->reason ? 2 : 0; + return size; +} #ifdef USE_AREAS uint8_t *AreaInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); @@ -150,6 +170,9 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_ for (const auto &it : this->serial_proxies) { ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 25, it); } +#endif +#ifdef USE_API_NOISE + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable); #endif return pos; } @@ -212,6 +235,9 @@ uint32_t DeviceInfoResponse::calculate_size() const { for (const auto &it : this->serial_proxies) { size += ProtoSize::calc_message_force(2, it.calculate_size()); } +#endif +#ifdef USE_API_NOISE + size += ProtoSize::calc_bool(2, this->api_encryption_provisionable); #endif return size; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 7e926ee0d4..4d5866da0b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -11,6 +11,10 @@ namespace esphome::api { namespace enums { +enum DisconnectReason : uint32_t { + DISCONNECT_REASON_UNSPECIFIED = 0, + DISCONNECT_REASON_PROVISIONING_CLOSED = 1, +}; enum SerialProxyPortType : uint32_t { SERIAL_PROXY_PORT_TYPE_TTL = 0, SERIAL_PROXY_PORT_TYPE_RS232 = 1, @@ -427,18 +431,22 @@ class HelloResponse final : public ProtoMessage { protected: }; -class DisconnectRequest final : public ProtoMessage { +class DisconnectRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 5; - static constexpr uint8_t ESTIMATED_SIZE = 0; + static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("disconnect_request"); } #endif + enums::DisconnectReason reason{}; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif protected: + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class DisconnectResponse final : public ProtoMessage { public: @@ -525,7 +533,7 @@ class SerialProxyInfo final : public ProtoMessage { class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint16_t ESTIMATED_SIZE = 309; + static constexpr uint16_t ESTIMATED_SIZE = 312; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("device_info_response"); } #endif @@ -580,6 +588,9 @@ class DeviceInfoResponse final : public ProtoMessage { #endif #ifdef USE_SERIAL_PROXY std::array serial_proxies{}; +#endif +#ifdef USE_API_NOISE + bool api_encryption_provisionable{false}; #endif uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 850ad37bc9..09570b09e4 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -125,6 +125,16 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint } #pragma GCC diagnostic pop +template<> const char *proto_enum_to_string(enums::DisconnectReason value) { + switch (value) { + case enums::DISCONNECT_REASON_UNSPECIFIED: + return ESPHOME_PSTR("DISCONNECT_REASON_UNSPECIFIED"); + case enums::DISCONNECT_REASON_PROVISIONING_CLOSED: + return ESPHOME_PSTR("DISCONNECT_REASON_PROVISIONING_CLOSED"); + default: + return ESPHOME_PSTR("UNKNOWN"); + } +} template<> const char *proto_enum_to_string(enums::SerialProxyPortType value) { switch (value) { case enums::SERIAL_PROXY_PORT_TYPE_TTL: @@ -864,7 +874,8 @@ const char *HelloResponse::dump_to(DumpBuffer &out) const { return out.c_str(); } const char *DisconnectRequest::dump_to(DumpBuffer &out) const { - out.append_p(ESPHOME_PSTR("DisconnectRequest {}")); + MessageDumpHelper helper(out, ESPHOME_PSTR("DisconnectRequest")); + dump_field(out, ESPHOME_PSTR("reason"), static_cast(this->reason)); return out.c_str(); } const char *DisconnectResponse::dump_to(DumpBuffer &out) const { @@ -971,6 +982,9 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { it.dump_to(out); out.append("\n"); } +#endif +#ifdef USE_API_NOISE + dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable); #endif return out.c_str(); } diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 0ba2961a13..5c9df433dd 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -51,10 +51,12 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui break; } case DisconnectRequest::MESSAGE_TYPE: { + DisconnectRequest msg; + msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_disconnect_request")); + this->log_receive_message_(LOG_STR("on_disconnect_request"), msg); #endif - this->on_disconnect_request(); + this->on_disconnect_request(msg); break; } case DisconnectResponse::MESSAGE_TYPE: { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index aca42ca303..d1b51f4846 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -21,7 +21,7 @@ class APIServerConnectionBase { void on_hello_request(const HelloRequest &value){}; - void on_disconnect_request(){}; + void on_disconnect_request(const DisconnectRequest &value){}; void on_disconnect_response(){}; void on_ping_request(){}; void on_ping_response(){}; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ddd03ace4a..6e3448121c 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -107,8 +107,30 @@ void APIServer::setup() { // Initialize last_connected_ for reboot timeout tracking this->last_connected_ = App.get_loop_component_start_time(); - // Set warning status if reboot timeout is enabled - if (this->reboot_timeout_ != 0) { +#if defined(USE_PROVISIONING) && defined(USE_API_NOISE) + // Register with the provisioning manager (provisioning:) as a source and + // report our current state (provisioned == an encryption key is set). When the + // window closes, disconnect any client still attempting to provision so it learns + // the reason. The manager owns the timeout, window state and on_timeout automation. + if (provisioning::global_provisioning_manager != nullptr) { + this->provisioning_source_ = provisioning::global_provisioning_manager->register_source(); + provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, + this->noise_ctx_.has_psk()); + provisioning::global_provisioning_manager->add_on_closed_callback([this]() { + for (auto &c : this->active_clients()) { + DisconnectRequest req; + req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED; + // Best-effort: if the send buffer is full the reason is dropped, but the + // client still learns the window is closed when it reconnects (rejected at + // hello) or via the socket close. + c->send_message(req); + } + }); + } +#endif + // Set warning status if reboot timeout is enabled (suppressed while provisioning + // is pending so the device waits to be onboarded instead of rebooting). + if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) { this->status_set_warning(LOG_STR("waiting for client connection")); } } @@ -121,8 +143,10 @@ void APIServer::loop() { if (this->api_connection_count_ == 0) { // Check reboot timeout - done in loop to avoid scheduler heap churn - // (cancelled scheduler items sit in heap memory until their scheduled time) - if (this->reboot_timeout_ != 0) { + // (cancelled scheduler items sit in heap memory until their scheduled time). + // Suppressed while a provisioning window is pending so the device waits to be + // onboarded / reset instead of rebooting itself; resumes once provisioned. + if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) { const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_connected_ > this->reboot_timeout_) { ESP_LOGE(TAG, "No clients; rebooting"); @@ -194,7 +218,8 @@ void APIServer::remove_client_(uint8_t client_index) { this->clients_[last_index].reset(); // Last client disconnected - set warning and start tracking for reboot timeout - if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) { + // (suppressed while provisioning is pending - see loop()). + if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) { this->status_set_warning(LOG_STR("waiting for client connection")); this->last_connected_ = App.get_loop_component_start_time(); } @@ -232,7 +257,7 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() { conn->start(); // First client connected - clear warning and update timestamp - if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) { + if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) { this->status_clear_warning(); this->last_connected_ = App.get_loop_component_start_time(); } @@ -240,12 +265,13 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() { } void APIServer::dump_config() { + char addr_buf[network::USE_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "Server:\n" " Address: %s:%u\n" " Listen backlog: %u\n" " Max connections: %u", - network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS); + network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS); #ifdef USE_API_NOISE ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk())); if (!this->noise_ctx_.has_psk()) { @@ -400,8 +426,16 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat #ifdef USE_API_HOMEASSISTANT_SERVICES void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) { + bool has_subscriber = false; for (auto &client : this->active_clients()) { - client->send_homeassistant_action(call); + has_subscriber |= client->send_homeassistant_action(call); + } + if (!has_subscriber) { + // Home Assistant subscribes to actions shortly *after* authenticating, so actions + // fired right at connection time (on_client_connected, on_time_sync, ...) can + // arrive before the subscription and are lost - warn instead of failing silently. + ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", call.is_event ? "event" : "action", call.service.c_str(), + this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected"); } } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -571,8 +605,16 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { } SavedNoisePsk new_saved_psk{psk}; - return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), - make_active); + bool result = this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), + make_active); +#ifdef USE_PROVISIONING + // The device now has a key; report provisioned so the provisioning window is + // satisfied and the reboot timeout resumes normal operation. + if (result && provisioning::global_provisioning_manager != nullptr) { + provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, true); + } +#endif + return result; #endif } bool APIServer::clear_noise_psk(bool make_active) { @@ -583,8 +625,16 @@ bool APIServer::clear_noise_psk(bool make_active) { return false; #else SavedNoisePsk empty_psk{}; - return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), - make_active); + bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), + make_active); +#ifdef USE_PROVISIONING + // The key was cleared; report unprovisioned so a subsequent reboot reopens the + // provisioning window. + if (result && provisioning::global_provisioning_manager != nullptr) { + provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, false); + } +#endif + return result; #endif } #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 16b5762f68..248b83a0ff 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -14,6 +14,9 @@ #include "esphome/core/controller.h" #include "esphome/core/log.h" #include "esphome/core/string_ref.h" +#ifdef USE_PROVISIONING +#include "esphome/components/provisioning/provisioning.h" +#endif #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -255,6 +258,19 @@ class APIServer final : public Component, // Remove a disconnected client by index. Swaps with the last populated slot and resets it. void __attribute__((noinline)) remove_client_(uint8_t client_index); +#ifdef USE_PROVISIONING + // True while a configured provisioning window is still pending (the device is + // unprovisioned). Suppresses the reboot timeout and its warning so the device is + // not auto-rebooted while waiting to be provisioned. False when no provisioning + // window is configured. + bool provisioning_pending_() const { + return provisioning::global_provisioning_manager != nullptr && + provisioning::global_provisioning_manager->window_pending(); + } +#else + bool provisioning_pending_() const { return false; } +#endif + #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool make_active); @@ -332,7 +348,10 @@ class APIServer final : public Component, uint8_t listen_backlog_{4}; bool shutting_down_ = false; uint8_t api_connection_count_{0}; - // 7 bytes used, 1 byte padding +#if defined(USE_PROVISIONING) && defined(USE_API_NOISE) + // Index assigned by the provisioning manager for reporting this transport's state. + uint8_t provisioning_source_{0}; +#endif #ifdef USE_API_NOISE APINoiseContext noise_ctx_; diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 44edc035f9..7f07146dba 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -18,7 +18,7 @@ with warnings.catch_warnings(): import contextlib from esphome.const import CONF_KEY, CONF_PORT, __version__ -from esphome.core import CORE, EsphomeError +from esphome.core import CORE from esphome.util import safe_print from . import CONF_ENCRYPTION @@ -36,15 +36,17 @@ class _LogLineProcessor: """Feeds incoming log lines to the stack-trace decoder. Two responsibilities beyond just calling the decoder: - 1. Catch EsphomeError. on_log runs inside an asyncio protocol - callback; if an exception escapes, the loop tears the transport - down with "Fatal error: protocol.data_received() call failed." - and ReconnectLogic immediately reconnects, the device replays - the same crash trace, and we loop forever. - 2. Disable decoding after the first failure. _decode_pc shells out - to PlatformIO via _run_idedata, which is expensive; a single - crash dump can contain many PC/BT lines and we don't want to - retry the failing subprocess for each one. + 1. Catch everything the decoder can raise. aioesphomeapi isolates + exceptions raised by log handlers, so an escaping one no longer + kills the session, but it does log a full traceback per line. A + crash dump carries a PC line plus one per backtrace frame, so the + tracebacks bury the dump the user is trying to read. Decoding is a + diagnostic nicety; nothing it raises is worth that noise. + 2. Disable decoding after the first failure. _decode_pc shells out to + the toolchain to resolve addr2line, which is expensive; a single + crash dump can contain many PC/BT lines and we don't want to retry + the failing subprocess for each one. This only works if every + failure is caught, which is why 1 is not narrowed to EsphomeError. """ def __init__(self, config: dict[str, Any], platform_handler: Any | None) -> None: @@ -61,12 +63,13 @@ class _LogLineProcessor: self.backtrace_state = self._platform_handler( self._config, raw_line, self.backtrace_state ) - except EsphomeError as exc: + except Exception as exc: # noqa: BLE001 # pylint: disable=broad-except self._decode_enabled = False self.backtrace_state = False # _run_idedata raises EsphomeError with no message; fall back # to a generic explanation when str(exc) is empty. detail = str(exc) or "build artifacts not found locally" + _LOGGER.debug("Stack-trace decoding failed", exc_info=True) _LOGGER.warning( "Crash trace decoding unavailable: %s. " "Run 'esphome compile' for this device to enable PC decoding.", @@ -152,6 +155,9 @@ async def async_run_logs( name=name, subscribe_states=subscribe_states, allow_plaintext_fallback=True, + # A top-level ``deep_sleep:`` block means the device is only awake + # briefly; cap the reconnect backoff so a wake window is not missed. + deep_sleep="deep_sleep" in config, ) try: await asyncio.Event().wait() diff --git a/esphome/components/aqi/__init__.py b/esphome/components/aqi/__init__.py index 4b979ab406..17d434294a 100644 --- a/esphome/components/aqi/__init__.py +++ b/esphome/components/aqi/__init__.py @@ -7,6 +7,7 @@ AQICalculatorType = aqi_ns.enum("AQICalculatorType") CONF_AQI = "aqi" CONF_CALCULATION_TYPE = "calculation_type" +CONF_EXTENDED_RANGE = "extended_range" AQI_CALCULATION_TYPE = { "CAQI": AQICalculatorType.CAQI_TYPE, diff --git a/esphome/components/aqi/abstract_aqi_calculator.h b/esphome/components/aqi/abstract_aqi_calculator.h index 299962fa17..6b4c9c5e04 100644 --- a/esphome/components/aqi/abstract_aqi_calculator.h +++ b/esphome/components/aqi/abstract_aqi_calculator.h @@ -6,7 +6,7 @@ namespace esphome::aqi { class AbstractAQICalculator { public: - virtual uint16_t get_aqi(float pm2_5_value, float pm10_0_value) = 0; + virtual uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool extended_range) = 0; }; } // namespace esphome::aqi diff --git a/esphome/components/aqi/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h index bb8e402280..56b6069118 100644 --- a/esphome/components/aqi/aqi_calculator.h +++ b/esphome/components/aqi/aqi_calculator.h @@ -11,10 +11,12 @@ namespace esphome::aqi { class AQICalculator : public AbstractAQICalculator { public: - uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override { - float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); - float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); + uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool extended_range) override { + float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID, extended_range); + float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID, extended_range); float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f}); + // extended_range lets the index run past the standard maximum, so clamp to the sensor's range. + aqi = std::min(aqi, static_cast(std::numeric_limits::max())); return static_cast(std::lround(aqi)); } @@ -30,7 +32,7 @@ class AQICalculator : public AbstractAQICalculator { {35.5f, 55.5f}, {55.5f, 125.5f}, {125.5f, 225.5f}, - {225.5f, std::numeric_limits::max()} + {225.5f, 500.4f} // EPA 2024: AQI 301-500 maps to PM2.5 225.5-500.4 ug/m3 // clang-format on }; @@ -41,11 +43,11 @@ class AQICalculator : public AbstractAQICalculator { {155.0f, 255.0f}, {255.0f, 355.0f}, {355.0f, 425.0f}, - {425.0f, std::numeric_limits::max()} + {425.0f, 604.0f} // EPA: AQI 301-500 maps to PM10 425-604 ug/m3 (top of the 401-500 band) // clang-format on }; - static float calculate_index(float value, const float array[NUM_LEVELS][2]) { + static float calculate_index(float value, const float array[NUM_LEVELS][2], bool extended_range) { int grid_index = get_grid_index(value, array); if (grid_index == -1) { return -1.0f; @@ -55,14 +57,22 @@ class AQICalculator : public AbstractAQICalculator { float conc_lo = array[grid_index][0]; float conc_hi = array[grid_index][1]; - return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; + float index = (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; + + // Concentrations above the highest breakpoint run the linear fit past aqi_hi. By default we + // clamp to the standard maximum; with extended_range we keep the extrapolated "over-range" + // value so heavy pollution reports numbers beyond what the standard defines. + if (grid_index == NUM_LEVELS - 1 && !extended_range && index > aqi_hi) { + return aqi_hi; + } + return index; } static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - const bool in_range = - (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive - : (value < array[i][1])); // others exclusive on hi + // The top band is open-ended: any value at or above its lower breakpoint falls into it, + // and calculate_index() decides whether to clamp or extrapolate. + const bool in_range = (value >= array[i][0]) && (i == NUM_LEVELS - 1 || value < array[i][1]); if (in_range) { return i; } diff --git a/esphome/components/aqi/aqi_sensor.cpp b/esphome/components/aqi/aqi_sensor.cpp index 2d8a780cc7..4bb964d5ee 100644 --- a/esphome/components/aqi/aqi_sensor.cpp +++ b/esphome/components/aqi/aqi_sensor.cpp @@ -24,6 +24,7 @@ void AQISensor::setup() { void AQISensor::dump_config() { ESP_LOGCONFIG(TAG, "AQI Sensor:"); ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI"); + ESP_LOGCONFIG(TAG, " Extended Range: %s", this->extended_range_ ? "enabled" : "disabled"); if (this->pm_2_5_sensor_ != nullptr) { ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str()); } @@ -44,7 +45,7 @@ void AQISensor::calculate_aqi_() { return; } - uint16_t aqi = calculator->get_aqi(this->pm_2_5_value_, this->pm_10_0_value_); + uint16_t aqi = calculator->get_aqi(this->pm_2_5_value_, this->pm_10_0_value_, this->extended_range_); this->publish_state(aqi); } diff --git a/esphome/components/aqi/aqi_sensor.h b/esphome/components/aqi/aqi_sensor.h index aa64fa5a4d..464c088188 100644 --- a/esphome/components/aqi/aqi_sensor.h +++ b/esphome/components/aqi/aqi_sensor.h @@ -14,6 +14,7 @@ class AQISensor final : public sensor::Sensor, public Component { void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; } void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; } void set_aqi_calculation_type(AQICalculatorType type) { this->aqi_calc_type_ = type; } + void set_extended_range(bool extended_range) { this->extended_range_ = extended_range; } protected: void calculate_aqi_(); @@ -21,6 +22,7 @@ class AQISensor final : public sensor::Sensor, public Component { sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_10_0_sensor_{nullptr}; AQICalculatorType aqi_calc_type_{AQI_TYPE}; + bool extended_range_{false}; AQICalculatorFactory aqi_calculator_factory_; float pm_2_5_value_{NAN}; diff --git a/esphome/components/aqi/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h index 3f6da45aa9..56a98682d9 100644 --- a/esphome/components/aqi/caqi_calculator.h +++ b/esphome/components/aqi/caqi_calculator.h @@ -9,25 +9,28 @@ namespace esphome::aqi { class CAQICalculator : public AbstractAQICalculator { public: - uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override { + // The CAQI (CITEAIR) scale defines no maximum: its top "Very high" class is simply ">100". We + // therefore always extrapolate the top band past 100 without limit, so the extended_range flag + // (which lifts the AQI calculator's fixed 500 cap) has no meaning here and is ignored. + uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool /*extended_range*/) override { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f}); + aqi = std::min(aqi, static_cast(std::numeric_limits::max())); return static_cast(std::lround(aqi)); } protected: - static constexpr int NUM_LEVELS = 5; + static constexpr int NUM_LEVELS = 4; - static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; + static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}}; static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { // clang-format off {0.0f, 15.1f}, {15.1f, 30.1f}, {30.1f, 55.1f}, - {55.1f, 110.1f}, - {110.1f, std::numeric_limits::max()} + {55.1f, 110.1f} // clang-format on }; @@ -36,8 +39,7 @@ class CAQICalculator : public AbstractAQICalculator { {0.0f, 25.1f}, {25.1f, 50.1f}, {50.1f, 90.1f}, - {90.1f, 180.1f}, - {180.1f, std::numeric_limits::max()} + {90.1f, 180.1f} // clang-format on }; @@ -52,14 +54,15 @@ class CAQICalculator : public AbstractAQICalculator { float conc_lo = array[grid_index][0]; float conc_hi = array[grid_index][1]; + // The top band is open-ended (see get_grid_index), so for concentrations above the last + // breakpoint this linear fit extrapolates past 100 unbounded, matching CAQI's open ">100" class. return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; } static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - const bool in_range = - (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive - : (value < array[i][1])); // others exclusive on hi + // The top band is open-ended: any value at or above its lower breakpoint falls into it. + const bool in_range = (value >= array[i][0]) && (i == NUM_LEVELS - 1 || value < array[i][1]); if (in_range) { return i; } diff --git a/esphome/components/aqi/sensor.py b/esphome/components/aqi/sensor.py index 5842aea88c..9c361560df 100644 --- a/esphome/components/aqi/sensor.py +++ b/esphome/components/aqi/sensor.py @@ -8,14 +8,25 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, ) -from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns +from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, CONF_EXTENDED_RANGE, aqi_ns CODEOWNERS = ["@jasstrong"] DEPENDENCIES = ["sensor"] AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = ( + +def _validate_extended_range(config): + if CONF_EXTENDED_RANGE in config and config[CONF_CALCULATION_TYPE] == "CAQI": + raise cv.Invalid( + f"'{CONF_EXTENDED_RANGE}' is not supported with 'calculation_type: CAQI'. " + "CAQI has no maximum value by specification, so it is always reported unbounded.", + [CONF_EXTENDED_RANGE], + ) + return config + + +CONFIG_SCHEMA = cv.All( sensor.sensor_schema( AQISensor, accuracy_decimals=0, @@ -29,9 +40,11 @@ CONFIG_SCHEMA = ( cv.Required(CONF_CALCULATION_TYPE): cv.enum( AQI_CALCULATION_TYPE, upper=True ), + cv.Optional(CONF_EXTENDED_RANGE): cv.boolean, } ) - .extend(cv.COMPONENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + _validate_extended_range, ) @@ -46,3 +59,4 @@ async def to_code(config): cg.add(var.set_pm_10_0_sensor(pm_10_0_sensor)) cg.add(var.set_aqi_calculation_type(config[CONF_CALCULATION_TYPE])) + cg.add(var.set_extended_range(config.get(CONF_EXTENDED_RANGE, False))) diff --git a/esphome/components/as3935_i2c/as3935_i2c.cpp b/esphome/components/as3935_i2c/as3935_i2c.cpp index 4c1020daa7..b3d015114f 100644 --- a/esphome/components/as3935_i2c/as3935_i2c.cpp +++ b/esphome/components/as3935_i2c/as3935_i2c.cpp @@ -24,11 +24,7 @@ void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t I2CAS3935Component::read_register(uint8_t reg) { uint8_t value; - if (write(®, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Writing register failed!"); - return 0; - } - if (read(&value, 1) != i2c::ERROR_OK) { + if (!this->read_byte(reg, &value)) { ESP_LOGW(TAG, "Reading register failed!"); return 0; } diff --git a/esphome/components/as7341/sensor.py b/esphome/components/as7341/sensor.py index fa51a1cdfa..8b6cf61028 100644 --- a/esphome/components/as7341/sensor.py +++ b/esphome/components/as7341/sensor.py @@ -5,7 +5,7 @@ from esphome.const import ( CONF_CLEAR, CONF_GAIN, CONF_ID, - DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_EMPTY, ICON_BRIGHTNESS_5, STATE_CLASS_MEASUREMENT, ) @@ -54,7 +54,7 @@ SENSOR_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 2a07903b68..22d544ba37 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -13,7 +13,7 @@ def AUTO_LOAD() -> list[str]: if ( not CORE.is_esp32 and not CORE.is_esp8266 - and not CORE.is_rp2040 + and not CORE.is_rp2 and not CORE.is_libretiny ): return ["socket"] @@ -37,7 +37,7 @@ async def to_code(config): elif CORE.is_esp8266: # https://github.com/ESP32Async/ESPAsyncTCP cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") - elif CORE.is_rp2040: + elif CORE.is_rp2: # https://github.com/ayushsharma82/RPAsyncTCP # RPAsyncTCP is a drop-in replacement for AsyncTCP_RP2040W with better # ESPAsyncWebServer compatibility @@ -47,6 +47,6 @@ async def to_code(config): def FILTER_SOURCE_FILES() -> list[str]: # Exclude socket implementation for platforms that use AsyncTCP libraries - if CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny: + if CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2 or CORE.is_libretiny: return ["async_tcp_socket.cpp"] return [] diff --git a/esphome/components/async_tcp/async_tcp.h b/esphome/components/async_tcp/async_tcp.h index 21fcfe239f..0906a07844 100644 --- a/esphome/components/async_tcp/async_tcp.h +++ b/esphome/components/async_tcp/async_tcp.h @@ -7,7 +7,7 @@ #elif defined(USE_ESP8266) // Use ESPAsyncTCP library for ESP8266 (always Arduino) #include -#elif defined(USE_RP2040) +#elif defined(USE_RP2) // Use RPAsyncTCP library for RP2040 #include #else diff --git a/esphome/components/async_tcp/async_tcp_socket.cpp b/esphome/components/async_tcp/async_tcp_socket.cpp index e8c0f163b3..10cbc981c7 100644 --- a/esphome/components/async_tcp/async_tcp_socket.cpp +++ b/esphome/components/async_tcp/async_tcp_socket.cpp @@ -1,6 +1,6 @@ #include "async_tcp_socket.h" -#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \ +#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2) && !defined(USE_LIBRETINY) && \ (defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)) #include "esphome/components/network/util.h" diff --git a/esphome/components/async_tcp/async_tcp_socket.h b/esphome/components/async_tcp/async_tcp_socket.h index 28714a7752..3b17fe14df 100644 --- a/esphome/components/async_tcp/async_tcp_socket.h +++ b/esphome/components/async_tcp/async_tcp_socket.h @@ -2,7 +2,7 @@ #include "esphome/core/defines.h" -#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \ +#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2) && !defined(USE_LIBRETINY) && \ (defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)) #include "esphome/components/socket/socket.h" diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp index f8bbd9d55e..7b5cdcfa20 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.cpp +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -65,12 +65,11 @@ optional ATCMiThermometer::parse_header_(const esp32_ble_tracker::S return {}; } - static uint8_t last_frame_count = 0; - if (last_frame_count == raw[12]) { - ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count); + if (this->last_frame_count_ == raw[12]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", this->last_frame_count_); return {}; } - last_frame_count = raw[12]; + this->last_frame_count_ = raw[12]; return result; } diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index 3dde5f1868..0f472c11b9 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -38,6 +38,8 @@ class ATCMiThermometer final : public Component, public esp32_ble_tracker::ESPBT sensor::Sensor *battery_voltage_{nullptr}; sensor::Sensor *signal_strength_{nullptr}; + uint8_t last_frame_count_{0}; + optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector &message, ParseResult &result); bool report_results_(const optional &result, const char *address); diff --git a/esphome/components/audio_file/__init__.py b/esphome/components/audio_file/__init__.py index 53193c8008..d59ed7411a 100644 --- a/esphome/components/audio_file/__init__.py +++ b/esphome/components/audio_file/__init__.py @@ -113,7 +113,9 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]: media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] if file_type == "wav": media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"] - elif file_type in ("mp3", "mpeg", "mpga"): + elif file_type in ("mp1", "mp2", "mp3", "mpeg", "mpga"): + # With puremagic >=2.0 this can cause some MP3 (Layer III) files to be labeled as "mp1"/"mp2". + # Treat those labels as MP3 so we still pick the MP3 decoder. media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] elif file_type == "flac": media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index 4e22489844..9e14615d7a 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -37,15 +37,15 @@ namespace esphome::beken_spi_led_strip { static const char *const TAG = "beken_spi_led_strip"; -struct spi_data_t { +struct SpiData { SemaphoreHandle_t dma_tx_semaphore; volatile bool tx_in_progress; bool first_run; }; -static spi_data_t *spi_data = nullptr; +static SpiData *spi_data = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static void set_spi_ctrl_register(unsigned long bit, bool val) { +static void set_spi_ctrl_register(uint32_t bit, bool val) { uint32_t value = REG_READ(SPI_CTRL); if (val == 0) { value &= ~bit; @@ -55,7 +55,7 @@ static void set_spi_ctrl_register(unsigned long bit, bool val) { REG_WRITE(SPI_CTRL, value); } -static void set_spi_config_register(unsigned long bit, bool val) { +static void set_spi_config_register(uint32_t bit, bool val) { uint32_t value = REG_READ(SPI_CONFIG); if (val == 0) { value &= ~bit; @@ -67,7 +67,7 @@ static void set_spi_config_register(unsigned long bit, bool val) { void spi_dma_tx_enable(bool enable) { GDMA_CFG_ST en_cfg; - set_spi_config_register(SPI_TX_EN, enable ? 1 : 0); + set_spi_config_register(SPI_TX_EN, enable); en_cfg.channel = SPI_TX_DMA_CHANNEL; en_cfg.param = enable ? 1 : 0; sddev_control(GDMA_DEV_NAME, CMD_GDMA_SET_DMA_ENABLE, &en_cfg); @@ -110,13 +110,13 @@ static void spi_set_clock(uint32_t max_hz) { param &= ~(SPI_CKR_MASK << SPI_CKR_POSI); param |= (div << SPI_CKR_POSI); REG_WRITE(SPI_CTRL, param); - ESP_LOGD(TAG, "target frequency: %d, actual frequency: %d", max_hz, source_clk / 2 / div); + ESP_LOGD(TAG, "target frequency: %" PRIu32 ", actual frequency: %d", max_hz, source_clk / 2 / div); } void spi_dma_tx_finish_callback(unsigned int param) { spi_data->tx_in_progress = false; xSemaphoreGive(spi_data->dma_tx_semaphore); - spi_dma_tx_enable(0); + spi_dma_tx_enable(false); } void BekenSPILEDStripLightOutput::setup() { @@ -161,7 +161,7 @@ void BekenSPILEDStripLightOutput::setup() { return; } - spi_data = (spi_data_t *) calloc(1, sizeof(spi_data_t)); + spi_data = (SpiData *) calloc(1, sizeof(SpiData)); // NOLINT(cppcoreguidelines-no-malloc) if (spi_data == nullptr) { ESP_LOGE(TAG, "Cannot allocate spi_data!"); this->mark_failed(); @@ -177,20 +177,20 @@ void BekenSPILEDStripLightOutput::setup() { spi_data->first_run = true; - set_spi_ctrl_register(MSTEN, 0); - set_spi_ctrl_register(BIT_WDTH, 0); + set_spi_ctrl_register(MSTEN, false); + set_spi_ctrl_register(BIT_WDTH, false); spi_set_clock(this->spi_frequency_); - set_spi_ctrl_register(CKPOL, 0); - set_spi_ctrl_register(CKPHA, 0); - set_spi_ctrl_register(MSTEN, 1); - set_spi_ctrl_register(SPIEN, 1); + set_spi_ctrl_register(CKPOL, false); + set_spi_ctrl_register(CKPHA, false); + set_spi_ctrl_register(MSTEN, true); + set_spi_ctrl_register(SPIEN, true); - set_spi_ctrl_register(TXINT_EN, 0); - set_spi_ctrl_register(RXINT_EN, 0); - set_spi_config_register(SPI_TX_FINISH_EN, 1); - set_spi_config_register(SPI_RX_FINISH_EN, 1); - set_spi_ctrl_register(RXOVR_EN, 0); - set_spi_ctrl_register(TXOVR_EN, 0); + set_spi_ctrl_register(TXINT_EN, false); + set_spi_ctrl_register(RXINT_EN, false); + set_spi_config_register(SPI_TX_FINISH_EN, true); + set_spi_config_register(SPI_RX_FINISH_EN, true); + set_spi_ctrl_register(RXOVR_EN, false); + set_spi_ctrl_register(TXOVR_EN, false); value = REG_READ(SPI_CTRL); value &= ~CTRL_NSSMD_3; @@ -199,7 +199,7 @@ void BekenSPILEDStripLightOutput::setup() { value = GFUNC_MODE_SPI_DMA; sddev_control(GPIO_DEV_NAME, CMD_GPIO_ENABLE_SECOND, &value); - set_spi_ctrl_register(SPI_S_CS_UP_INT_EN, 0); + set_spi_ctrl_register(SPI_S_CS_UP_INT_EN, false); GDMA_CFG_ST en_cfg; GDMACFG_TPYES_ST init_cfg; @@ -210,7 +210,7 @@ void BekenSPILEDStripLightOutput::setup() { init_cfg.dstptr_incr = 0; init_cfg.srcptr_incr = 1; init_cfg.src_start_addr = this->dma_buf_; - init_cfg.dst_start_addr = (void *) SPI_DAT; // SPI_DMA_REG4_TXFIFO + init_cfg.dst_start_addr = (void *) SPI_DAT; // NOLINT(performance-no-int-to-ptr) SPI_DMA_REG4_TXFIFO init_cfg.channel = SPI_TX_DMA_CHANNEL; init_cfg.prio = 0; // 10 init_cfg.u.type4.src_loop_start_addr = this->dma_buf_; @@ -230,7 +230,7 @@ void BekenSPILEDStripLightOutput::setup() { en_cfg.param = 0; sddev_control(GDMA_DEV_NAME, CMD_GDMA_CFG_SRCADDR_LOOP, &en_cfg); - spi_dma_tx_enable(0); + spi_dma_tx_enable(false); value = REG_READ(SPI_CONFIG); value &= ~(0xFFF << 8); @@ -247,7 +247,8 @@ void BekenSPILEDStripLightOutput::set_led_params(uint8_t bit0, uint8_t bit1, uin void BekenSPILEDStripLightOutput::write_state(light::LightState *state) { // protect from refreshing too often uint32_t now = micros(); - if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + if (this->max_refresh_rate_.has_value() && *this->max_refresh_rate_ != 0 && + (now - this->last_refresh_) < *this->max_refresh_rate_) { // try again next loop iteration, so that this change won't get lost this->schedule_show(); return; @@ -293,7 +294,7 @@ void BekenSPILEDStripLightOutput::write_state(light::LightState *state) { } spi_data->first_run = false; - spi_dma_tx_enable(1); + spi_dma_tx_enable(true); this->status_clear_warning(); } @@ -376,7 +377,7 @@ void BekenSPILEDStripLightOutput::dump_config() { " RGB Order: %s\n" " Max refresh rate: %" PRIu32 "\n" " Number of LEDs: %u", - rgb_order, *this->max_refresh_rate_, this->num_leds_); + rgb_order, this->max_refresh_rate_.value_or(0), this->num_leds_); } float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index a9a09363fc..5800e0bd9e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -448,7 +448,9 @@ _BINARY_SENSOR_SCHEMA = ( cv.Exclusive( CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE ): cv.boolean, - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}), diff --git a/esphome/components/bk72xx_ble/__init__.py b/esphome/components/bk72xx_ble/__init__.py new file mode 100644 index 0000000000..29e2a6b13d --- /dev/null +++ b/esphome/components/bk72xx_ble/__init__.py @@ -0,0 +1,94 @@ +"""BK72xx BLE — BLE controller support for the BLE-5.x LibreTiny Beken chips. + +The platform analog of esp32_ble / rp2040_ble: owns the Beken BDK BLE stack +bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build +on this component and contain no SDK calls of their own. + +Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253 +(BLE 5.2), and any future BLE-5.x SoC. Capability is detected at compile time, +not by a chip list: the C++ guards on `__has_include("ble_api.h")` — the Beken +BLE 5.x public API header, which the LibreTiny beken-72xx builder ships only +for BLE-5.x SoCs. BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) fail +with a clear #error. + +No framework patch is needed: the LibreTiny beken-72xx builder already compiles +and links the BLE 5.x stack (CFG_SUPPORT_BLE=1 + CFG_BLE_VERSION=BLE_VERSION_5_x; +prebuilt libble_.a per SoC). This component only calls into it via the +public ble_api.h. +""" + +import logging + +import esphome.codegen as cg +from esphome.components import libretiny +from esphome.components.libretiny.const import FAMILY_BK7231N, FAMILY_BK7238 +import esphome.config_validation as cv +from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID +from esphome.types import ConfigType + +DEPENDENCIES = ["bk72xx"] +CODEOWNERS = ["@Bl00d-B0b"] + +_LOGGER = logging.getLogger(__name__) + +bk72xx_ble_ns = cg.esphome_ns.namespace("bk72xx_ble") +BK72xxBLE = bk72xx_ble_ns.class_("BK72xxBLE", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BK72xxBLE), + # Default off: on the single-core BK72xx, bringing the BLE stack up during + # boot competes with the WiFi connection handshake. Consumers enable the + # stack lazily on first use (e.g. the tracker's first scan start). + cv.Optional(CONF_ENABLE_ON_BOOT, default=False): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + + # Enable the BLE stack in the build (the '#h' maps to sys_config.h; the value + # is a list). ESPHome's libretiny platform normally appends CFG_SUPPORT_BLE=0 + # on BK7231N/BK7238 (saves ~21KB RAM/~200KB Flash when BLE is unused) to this + # SAME key — and add_platformio_option appends list values, it never replaces. + # The platform therefore skips its disable when this component is configured, + # so this =1 is the single CFG_SUPPORT_BLE define emitted. + cg.add_platformio_option("custom_options.sys_config#h", ["CFG_SUPPORT_BLE=1"]) + + # Pin the Beken BDK release the BLE 5.x stack is validated against. The + # bundled 3.0.33 has an older BLE header/library layout — and with + # CFG_SUPPORT_BLE=1 the SDK runs its BLE init unconditionally during boot + # (the reason the libretiny platform sets =0 when BLE is unused), so a + # mismatched BDK can crash the device before WiFi comes up regardless of + # enable_on_boot. Pinning here makes a plain config build against the + # validated BDK without any manual platformio_options. + _LOGGER.warning( + "bk72xx_ble builds with beken-bdk 3.0.78 instead of the platform's bundled " + "default: the default's older BLE layout can crash the device at boot when " + "BLE is compiled in" + ) + cg.add_platformio_option("custom_versions.beken-bdk", "3.0.78") + + # The BDK exposes the controller's BLE address as `common_default_bdaddr` on + # BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is + # derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++ + # which path is available so it doesn't reference a missing symbol. + family = libretiny.get_libretiny_family() + if family == FAMILY_BK7231N: + cg.add_define("BK72XX_BLE_HAS_COMMON_BDADDR") + elif family == FAMILY_BK7238: + # ESPHome's LibreTiny disables BLE on BK7238 because the SDK can hang at + # WiFi STA startup when BLE init runs. This component re-enables BLE, so + # warn loudly: BK7238 is accepted but not hardware-verified and may be + # WiFi-unstable with BLE on. + _LOGGER.warning( + "bk72xx_ble on BK7238: enabling BLE is known to risk a WiFi STA startup " + "hang on this family and is not yet hardware-verified. Expect possible " + "instability." + ) + + cg.add_define("USE_BK72XX_BLE") diff --git a/esphome/components/bk72xx_ble/bk72xx_ble.cpp b/esphome/components/bk72xx_ble/bk72xx_ble.cpp new file mode 100644 index 0000000000..a5ecaf4abb --- /dev/null +++ b/esphome/components/bk72xx_ble/bk72xx_ble.cpp @@ -0,0 +1,292 @@ +// bk72xx_ble.cpp +// +// BLE controller support for the BK72xx BLE-5.x chips (LibreTiny beken-72xx +// family) — the platform analog of esp32_ble / rp2040_ble. Owns everything that +// talks to the Beken BDK BLE stack: +// - one-time stack bring-up (ble_set_notice_cb() + ble_entry()), +// - the controller BLE address, +// - the raw controller scan primitives (bk_ble_scan_start/stop), +// - the scan-report ring: the BDK notice callback (BLE task) takes a report +// from a fixed pool and pushes it on a lock-free SPSC queue; loop() drains, +// dispatches on the main task and returns reports to the pool — the same +// EventPool + LockFreeQueue handoff esp32_ble uses, zero allocation at +// steady state. +// Consumers contain no SDK calls of their own. +// +// NOTE: the Beken BDK BLE 5.x stack is compiled and linked by the LibreTiny +// beken-72xx builder itself (prebuilt libble_.a + ble_5_x sources, gated +// on CFG_SUPPORT_BLE / CFG_BLE_VERSION in sys_config.h). This component only +// calls into it via the public ble_api.h — no framework patch is required. + +#include "bk72xx_ble.h" // pulls esphome/core/defines.h for USE_BK72XX_BLE + +#ifdef USE_BK72XX_BLE + +#include + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" // get_mac_address_raw() +#include "esphome/core/log.h" + +// --------------------------------------------------------------------------- +// SDK-capability gate (not a chip allowlist). +// This component drives the Beken BLE *5.x* controller via its public API, +// `ble_api.h`, which the LibreTiny beken-72xx builder ships only for the +// BLE-5.x SoCs (it selects the `ble_pub` 5.x stack from CFG_BLE_VERSION; the +// 4.2 SoCs build a different, older API with no ble_api.h). Gate on the header +// itself so any BLE-5.x Beken chip — present or future — is supported without a +// hard-coded list, and a non-5.x build fails here with a clear message instead +// of a cryptic "ble_api.h: No such file or directory". +// --------------------------------------------------------------------------- +#if defined(CLANG_TIDY) +// The clang-tidy environment does not carry the full Beken BDK BLE 5.x API +// (its ble_api.h variant lacks parts of the 5.x surface), so there is nothing +// accurate to analyze the SDK calls against — skip the file under analysis. +#define BK72XX_BLE_NO_SDK +#elif !__has_include("ble_api.h") +#error \ + "bk72xx_ble requires a BLE 5.x Beken SDK (ble_api.h). Supported SoCs: BK7231N/BK7236 (BLE 5.1) and BK7238/BK7252N/BK7253 (BLE 5.2). BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) are not supported." +#endif + +#ifndef BK72XX_BLE_NO_SDK + +// --------------------------------------------------------------------------- +// Beken BDK BLE 5.x SDK — public API. +// Exposed on the include path by the LibreTiny beken-72xx builder +// (cores/.../ble_5_x_rw + driver/include). Wrapped in extern "C" because these +// are C headers consumed from C++ (a standard C-header-from-C++ pattern). +// --------------------------------------------------------------------------- +extern "C" { +#include "ble_api.h" // bk_ble_scan_start/stop, ble_entry, ble_set_notice_cb, + // app_ble_get_idle_actv_idx_handle, struct scan_param, + // recv_adv_t, ble_notice_t, BLE_5_REPORT_ADV, SCAN_ACTV +#ifdef BK72XX_BLE_HAS_COMMON_BDADDR +#include "common_bt_defines.h" // struct bd_addr +// The controller's public BLE address, populated by the BDK during ble_entry(). +// Present on BK7231N; the other BLE-5.x chips' stacks have no such symbol — there the +// address is derived from the WiFi MAC instead (matching the BDK's own fallback). +extern struct bd_addr common_default_bdaddr; +#endif +// ble_entry() brings up the BDK BLE stack; it is not declared in ble_api.h, so +// declare it here. +void ble_entry(void); +} + +namespace esphome::bk72xx_ble { + +static const char *const TAG = "bk72xx_ble"; + +// The BDK notice callback is a plain C function pointer with no user argument, +// so it reaches the (single) component instance through a file-static pointer. +static BK72xxBLE *s_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +// --------------------------------------------------------------------------- +// BLE notice callback — runs in the BDK BLE task context. +// The BK controller reports every advertisement as a BLE_5_REPORT_ADV notice +// carrying a recv_adv_t. Copy it into the queue and return; all dispatch +// happens in loop() on the main task. +// --------------------------------------------------------------------------- +static void ble_notice_callback(ble_notice_t notice, void *param) { + if (s_ble == nullptr || param == nullptr) + return; + if (notice != BLE_5_REPORT_ADV) + return; + + const recv_adv_t *info = reinterpret_cast(param); + // rssi is a signed dBm carried in a uint8_t; cast through int8_t (standard for + // a signed dBm value packed in a uint8_t). + s_ble->enqueue_scan_report(info->adv_addr, static_cast(info->rssi), info->adv_addr_type, info->data, + info->data_len); +} + +void BK72xxBLE::enqueue_scan_report(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, + uint16_t data_len) { + BLEScanReport *report = this->report_pool_.allocate(); + if (report == nullptr) { + // Pool exhausted — the queue is full; count and drop. + this->report_queue_.increment_dropped_count(); + return; + } + memcpy(report->mac, mac, 6); + report->rssi = rssi; + report->addr_type = addr_type; + report->data_len = + (data_len <= sizeof(report->data)) ? static_cast(data_len) : static_cast(sizeof(report->data)); + memcpy(report->data, data, report->data_len); + // Cannot fail: the pool is sized to the queue capacity. + this->report_queue_.push(report); +} + +// --------------------------------------------------------------------------- +// Component lifecycle +// --------------------------------------------------------------------------- + +void BK72xxBLE::setup() { + s_ble = this; + // Resolve the MAC early so get_mac_lsb_first() is valid for consumers before + // the stack is up (it is re-read once ble_entry() has run). + this->resolve_mac_(); + if (this->enable_on_boot_) { + this->enable(); + } +} + +// AFTER_WIFI, not BLUETOOTH: replicates the proven pre-split timing — the BDK +// is first touched only once WiFi is up (single-core WiFi/BLE bring-up order). +float BK72xxBLE::get_setup_priority() const { return setup_priority::AFTER_WIFI; } + +void BK72xxBLE::enable() { + if (this->state_ != BLEComponentState::STATE_OFF) + return; + this->state_ = BLEComponentState::ENABLING; + + // One-time BLE stack init: register the notice callback, then bring up the + // BDK BLE stack. The BDK has no teardown path — init happens at most once. + ble_set_notice_cb(ble_notice_callback); + ble_entry(); + + delay(100); // NOLINT — one-time BLE stack init; the SDK needs this settle time + + // Re-read the BLE MAC now that the controller is up (common_default_bdaddr is + // populated by ble_entry()); resolve_mac_() may have fallen back earlier. + this->resolve_mac_(); + +#ifdef BK72XX_BLE_HAS_COMMON_BDADDR + // Liveness heuristic (BK7231N): a healthy ble_entry() populates + // common_default_bdaddr during init, so all-zero after the settle delay + // suggests the stack did not come up. The BDK entry point returns void — no + // return code exists — so warn rather than fail: scan starts against a dead + // stack already fail cleanly downstream (no idle activity handle). + bool bdaddr_live = false; + for (uint8_t b : common_default_bdaddr.addr) { + if (b != 0) { + bdaddr_live = true; + break; + } + } + if (!bdaddr_live) + ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started"); +#endif + + this->state_ = BLEComponentState::ACTIVE; + ESP_LOGD(TAG, "BLE stack initialised"); +} + +void BK72xxBLE::loop() { + // Drain the lock-free ring filled by the BLE task; all per-report work runs + // here on the main task, then the report returns to the pool. + BLEScanReport *report = this->report_queue_.pop(); + if (report == nullptr) + return; + do { + for (auto *listener : this->scan_listeners_) + listener->on_scan_report(*report); + this->report_pool_.release(report); + } while ((report = this->report_queue_.pop()) != nullptr); + + // Log dropped reports — only reachable when reports were processed; drops can + // only occur while the queue is full, and only this loop drains it. + uint16_t dropped = this->report_queue_.get_and_reset_dropped_count(); + if (dropped > 0) + ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped); +} + +void BK72xxBLE::get_mac_lsb_first(uint8_t out[6]) const { + for (int i = 0; i < 6; i++) + out[i] = this->ble_mac_[i]; +} + +void BK72xxBLE::dump_config() { + // ble_mac_ is stored LSB-first (BLE convention); print [5..0] for the + // MSB-first order Home Assistant shows. + ESP_LOGCONFIG(TAG, + "BK72xx BLE:\n" + " MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n" + " Active: %s", + this->ble_mac_[5], this->ble_mac_[4], this->ble_mac_[3], this->ble_mac_[2], this->ble_mac_[1], + this->ble_mac_[0], YESNO(this->is_active())); +} + +// --------------------------------------------------------------------------- +// MAC resolution +// --------------------------------------------------------------------------- + +void BK72xxBLE::resolve_mac_() { +#ifdef BK72XX_BLE_HAS_COMMON_BDADDR + // BK7231N: the BDK populates common_default_bdaddr (LSB-first, BLE convention) + // during ble_entry(). It may still be zero before the stack is up; if so, fall + // through to the WiFi-derived MAC below. + bool nonzero = false; + for (uint8_t b : common_default_bdaddr.addr) { + if (b != 0) { + nonzero = true; + break; + } + } + if (nonzero) { + memcpy(this->ble_mac_, common_default_bdaddr.addr, 6); + return; + } +#endif + // Chips whose BLE stack does not export common_default_bdaddr (BK7238 and the other + // BLE-5.x SoCs), or BK7231N before the stack is up: derive the BLE MAC exactly as the + // Beken BDK does in bdaddr_env_init() — the WiFi STA MAC with only its last byte + // incremented (sta_mac[5] += 1, a plain byte increment with no carry into the next + // byte), OUI unchanged. This reproduces the address the controller advertises with + // (verified against the BK7231N BLE-5.1 and BK7252N/BK7238 BLE-5.2 SDK sources), so it + // matches on every device, including the last-byte == 0xFF edge that a 24-bit increment + // would carry differently. + uint8_t wifi_mac[6]; + get_mac_address_raw(wifi_mac); // MSB-first + const uint8_t ble[6] = {wifi_mac[0], wifi_mac[1], wifi_mac[2], + wifi_mac[3], wifi_mac[4], static_cast(wifi_mac[5] + 1)}; + // Store LSB-first to match recv_adv_t adv_addr ordering. + for (int i = 0; i < 6; i++) + this->ble_mac_[i] = ble[5 - i]; +} + +// --------------------------------------------------------------------------- +// Controller scan primitives +// --------------------------------------------------------------------------- + +bool BK72xxBLE::scan_start(uint16_t interval, uint16_t window) { + if (!this->is_active()) + this->enable(); + + if (this->scan_actv_idx_ != 0xFF) { + // Already scanning — stop first so this call cleanly restarts with the new + // parameters (the BDK cannot start a second scan on a busy activity). + this->scan_stop(); + } + + struct scan_param sp; + memset(&sp, 0, sizeof(sp)); + sp.channel_map = 7; // advertising channels 37/38/39 + sp.interval = interval; + sp.window = window; + + this->scan_actv_idx_ = app_ble_get_idle_actv_idx_handle(SCAN_ACTV); + if (this->scan_actv_idx_ == 0xFF) { + ESP_LOGE(TAG, "Scan start failed: no idle activity handle"); + return false; + } + ble_err_t ret = bk_ble_scan_start(this->scan_actv_idx_, &sp, nullptr); + if (ret != ERR_SUCCESS) { + ESP_LOGE(TAG, "Scan start failed (err %d)", static_cast(ret)); + this->scan_actv_idx_ = 0xFF; + return false; + } + return true; +} + +void BK72xxBLE::scan_stop() { + if (this->scan_actv_idx_ != 0xFF) { + bk_ble_scan_stop(this->scan_actv_idx_, nullptr); + this->scan_actv_idx_ = 0xFF; + } +} + +} // namespace esphome::bk72xx_ble + +#endif // BK72XX_BLE_NO_SDK +#endif // USE_BK72XX_BLE diff --git a/esphome/components/bk72xx_ble/bk72xx_ble.h b/esphome/components/bk72xx_ble/bk72xx_ble.h new file mode 100644 index 0000000000..2654f4e68e --- /dev/null +++ b/esphome/components/bk72xx_ble/bk72xx_ble.h @@ -0,0 +1,98 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_BK72XX_BLE + +#include "esphome/core/component.h" +#include "esphome/core/event_pool.h" +#include "esphome/core/lock_free_queue.h" + +#include +#include + +namespace esphome::bk72xx_ble { + +enum class BLEComponentState : uint8_t { + STATE_OFF = 0, + ENABLING, + ACTIVE, +}; + +/// One advertisement report from the controller. +struct BLEScanReport { + uint8_t mac[6]; // LSB-first, as the controller delivers it + int8_t rssi; // signed dBm + uint8_t addr_type; + uint8_t data_len; // bytes valid in data[] + uint8_t data[62]; // legacy advertisement (31) + scan response (31) + + // EventPool contract: nothing is heap-allocated inside a report. + void release() {} +}; + +/// Consumer interface for controller scan reports. on_scan_report() always runs +/// on the ESPHome main task: reports are queued from the BDK BLE task and +/// drained by the controller's loop(), so consumers never deal with cross-task +/// state (the esp32_ble event-queue pattern). +class BLEScanListener { + public: + virtual void on_scan_report(const BLEScanReport &report) = 0; + + protected: + ~BLEScanListener() = default; // deletion via this interface is not part of the contract +}; + +// Maximum reports buffered between the BLE task and loop(). +static constexpr uint8_t MAX_SCAN_REPORT_QUEUE_SIZE = 64; + +class BK72xxBLE final : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + /// Bring up the BDK BLE stack (one-time; the BDK has no teardown path). + void enable(); + bool is_active() const { return this->state_ == BLEComponentState::ACTIVE; } + + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + + /// Controller BLE address, least-significant octet first (BLE convention). + void get_mac_lsb_first(uint8_t out[6]) const; + + /// Register a consumer for scan reports (delivered on the main task via loop()). + void register_scan_listener(BLEScanListener *listener) { this->scan_listeners_.push_back(listener); } + + /// Start the controller scan. Interval/window are in BLE units (0.625 ms). + /// Enables the stack first if needed. Returns false on controller failure. + bool scan_start(uint16_t interval, uint16_t window); + /// Stop the controller scan (no-op when not scanning). + void scan_stop(); + + /// Internal: buffer one controller report (BDK notice callback, BLE task + /// context — bounded copy under the scheduler lock, nothing else). + void enqueue_scan_report(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint16_t data_len); + + protected: + void resolve_mac_(); + + std::vector scan_listeners_; + // Report ring: the BDK notice callback (BLE task) allocates a report from the + // pool, fills it and pushes the pointer; loop() pops, dispatches and releases. + // Lock-free SPSC, zero allocation at steady state — the esp32_ble pattern. + esphome::LockFreeQueue report_queue_; + // Pool sized to queue capacity (SIZE-1): the ring reserves one slot, so + // allocate() returns nullptr before push() can fail. This prevents leaking a + // pool slot on a failed push and keeps release() off the producer path. + esphome::EventPool report_pool_; + uint8_t ble_mac_[6]{0}; // LSB-first (BLE convention) + uint8_t scan_actv_idx_{0xFF}; + BLEComponentState state_{BLEComponentState::STATE_OFF}; + bool enable_on_boot_{false}; +}; + +} // namespace esphome::bk72xx_ble + +#endif // USE_BK72XX_BLE diff --git a/esphome/components/bk72xx_ble_tracker/__init__.py b/esphome/components/bk72xx_ble_tracker/__init__.py new file mode 100644 index 0000000000..39c9ada731 --- /dev/null +++ b/esphome/components/bk72xx_ble_tracker/__init__.py @@ -0,0 +1,151 @@ +"""BK72xx BLE Tracker — ESPHome BLE 5.x scanner for the BLE-5.x-capable +LibreTiny Beken chips (beken-72xx family). + +Builds on the bk72xx_ble controller component (stack bring-up, BLE address, +scan primitives) and implements the platform-neutral ble_device_base BLEHub +contract: the shared BLE sensors (ble_presence, ble_rssi, ble_scanner, +bthome_mithermometer, xiaomi_*, …) bind to this tracker through +cv.use_id(BLEHub) with no BK-specific code. + +Scan modes: + continuous: true — scan runs forever; never stops automatically. + Use this when the radio is dedicated to BLE. + continuous: false — a started scan runs for `duration` ms, then stops. The + FIRST start is external too: nothing in this component + starts a non-continuous scan on boot, so until the + automation actions land (follow-up PR) the radio stays + idle. start_scan() is called from code (e.g. an api + client-connected automation) so the single-core radio + can service WiFi in between scans. +""" + +import esphome.codegen as cg +from esphome.components import bk72xx_ble, ble_device_base, ota +from esphome.components.const import CONF_SCAN_PARAMETERS, CONF_WINDOW +import esphome.config_validation as cv +from esphome.const import CONF_CONTINUOUS, CONF_DURATION, CONF_ID, CONF_INTERVAL +from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.types import ConfigType + +CONF_BK72XX_BLE_ID = "bk72xx_ble_id" + +DEPENDENCIES = ["bk72xx"] +AUTO_LOAD = ["ble_device_base", "bk72xx_ble"] +CODEOWNERS = ["@Bl00d-B0b"] + +bk72xx_ble_tracker_ns = cg.esphome_ns.namespace("bk72xx_ble_tracker") +BK72xxBLETracker = bk72xx_ble_tracker_ns.class_( + "BK72xxBLETracker", ble_device_base.BLEHub, cg.Component +) + + +def to_ble_units(value: cv.TimePeriod) -> int: + """Convert a scan time to the controller's 0.625 ms units. + + Used by both validation and codegen so what is validated is exactly what is + programmed — the truncation here is what makes the duty-cycle check below + meaningful. + """ + return value.total_microseconds // 625 + + +def validate_scan_parameters(config: ConfigType) -> ConfigType: + """Reject impossible window/interval/duration combinations at config time. + + Mirrors esp32_ble_tracker: the controller cannot scan for longer than the + interval, and a too-short duration would end the scan period almost + immediately. Catching it here gives a clear error instead of a runtime + controller failure and the 1/sec retry loop. + """ + duration = config[CONF_DURATION] + interval = config[CONF_INTERVAL] + window = config[CONF_WINDOW] + + if window > interval: + raise cv.Invalid( + f"Scan window ({window}) needs to be smaller than scan interval ({interval})" + ) + + # BLE scan interval/window are programmed in 0.625 ms units as a 16-bit value; the + # controller only accepts 2.5 ms .. 10240 ms (0x0004 .. 0x4000). Reject out-of-range + # values here instead of letting the unit conversion silently overflow. + for name, value in (("interval", interval), ("window", window)): + if value.total_microseconds < 2500 or value.total_microseconds > 10_240_000: + raise cv.Invalid( + f"Scan {name} ({value}) must be between 2.5 ms and 10240 ms" + ) + + # Validate what actually reaches the controller: both values are truncated to + # whole 0.625 ms units, so a window/interval pair that differs by less than one + # unit collapses to the same value — silently programming a 100 % duty cycle + # (radio permanently on) from a config that asked for less. + interval_units = to_ble_units(interval) + window_units = to_ble_units(window) + if window_units == interval_units and window < interval: + raise cv.Invalid( + f"Scan window ({window}) and interval ({interval}) both round to " + f"{interval_units} x 0.625 ms, which the controller scans at a 100 % duty " + f"cycle. Separate them by at least 0.625 ms." + ) + + if interval.total_microseconds * 3 > duration.total_microseconds: + raise cv.Invalid( + f"Scan duration ({duration}) must cover at least three scan intervals " + f"({interval}): the scanner listens on one of the three BLE advertising " + f"channels per interval, so a shorter duration can miss devices entirely." + ) + + return config + + +SCAN_PARAMETERS_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_DURATION, default="5min"): cv.positive_time_period_seconds, + # interval/window default to the BK reference scan rate — 100 ms / 30 ms, + # a 30 % duty cycle. Converted to the controller's 0.625 ms BLE units in + # to_code(). (LN882H's SDK recommends a different 100 / 50 ms = 50 %.) + cv.Optional(CONF_INTERVAL, default="100ms"): cv.positive_time_period, + cv.Optional(CONF_WINDOW, default="30ms"): cv.positive_time_period, + cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean, + } + ), + validate_scan_parameters, +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BK72xxBLETracker), + cv.GenerateID(CONF_BK72XX_BLE_ID): cv.use_id(bk72xx_ble.BK72xxBLE), + cv.Optional(CONF_SCAN_PARAMETERS, default={}): SCAN_PARAMETERS_SCHEMA, + } +).extend(cv.COMPONENT_SCHEMA) + + +# Runs at FINAL priority so every BLE sensor has registered through +# ble_device_base (and any tracker-owned listeners have been counted) before +# the StaticVector size is emitted. Same pattern as esp32_ble_tracker. +@coroutine_with_priority(CoroPriority.FINAL) +async def _emit_listener_count() -> None: + count = ble_device_base.get_listener_count() + if count > 0: + cg.add_define("ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT", count) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + parent = await cg.get_variable(config[CONF_BK72XX_BLE_ID]) + cg.add(var.set_parent(parent)) + + # Get notified when an OTA update starts, to pause scanning (esp32_ble_tracker parity) + ota.request_ota_state_listeners() + + scan = config[CONF_SCAN_PARAMETERS] + cg.add(var.set_scan_interval(to_ble_units(scan[CONF_INTERVAL]))) + cg.add(var.set_scan_window(to_ble_units(scan[CONF_WINDOW]))) + cg.add(var.set_scan_duration(scan[CONF_DURATION].total_milliseconds)) + cg.add(var.set_scan_continuous(scan[CONF_CONTINUOUS])) + + CORE.add_job(_emit_listener_count) diff --git a/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.cpp b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.cpp new file mode 100644 index 0000000000..634285d530 --- /dev/null +++ b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.cpp @@ -0,0 +1,211 @@ +// bk72xx_ble_tracker.cpp +// +// BLE scan policy for the BK72xx BLE-5.x chips: parameters, duration/period +// timers and the rate-limited start retry. All controller access (stack +// bring-up, scan primitives, the BLE-task → main-task report queue) goes +// through the bk72xx_ble component — no SDK calls and no cross-task state here. + +#ifdef USE_LIBRETINY + +#include "bk72xx_ble_tracker.h" + +#include +#include + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::bk72xx_ble_tracker { + +static const char *const TAG = "bk72xx_ble_tracker"; + +// Minimum interval between scan (re)start attempts, so a failing controller start +// cannot be retried every main-loop iteration (single-core CPU starvation). The +// interval doubles with consecutive failed starts (1 s up to 64 s) so a controller +// that never comes up — the controller logs each failure at ERROR — settles into a +// slow, quiet poll instead of an error line every second for the rest of uptime; +// a single WARN is emitted when the retry interval first saturates. +static constexpr uint32_t SCAN_START_RETRY_MS = 1000; +static constexpr uint8_t SCAN_START_RETRY_MAX_DOUBLINGS = 6; // 1 s << 6 = 64 s + +// --------------------------------------------------------------------------- +// Component lifecycle +// --------------------------------------------------------------------------- + +void BK72xxBLETracker::setup() { + // Receive the controller's scan reports; the controller queues them from the + // BLE task and delivers here on the main task. + this->parent_->register_scan_listener(this); +#ifdef USE_OTA_STATE_LISTENER + // Pause scanning while an OTA update is in flight — on the single-core BK72xx the + // BLE scan competes with the OTA flash writes. Mirrors esp32_ble_tracker. + ota::get_global_ota_callback()->add_global_state_listener(this); +#endif +} + +#ifdef USE_OTA_STATE_LISTENER +void BK72xxBLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, + ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + this->scan_continuous_before_ota_ = this->scan_continuous_; + this->stop_scan(); + } else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) { + // On success the device reboots, so restore only on a failed/aborted update; + // loop() restarts the scan on its next iteration (continuous idle branch). + this->scan_continuous_before_ota_ = false; + this->scan_continuous_ = true; + } +} +#endif // USE_OTA_STATE_LISTENER + +void BK72xxBLETracker::loop() { + const uint32_t now = millis(); + if (this->scan_continuous_) { + if (!this->scan_running_) { + // Rate-limit (re)start attempts. The controller start can fail (no idle activity + // handle, WiFi/BLE coexistence) and leave scan_running_ false; retrying every + // main-loop iteration would spin the single-core CPU and starve WiFi (device + // becomes unresponsive). The interval backs off with consecutive failures so a + // controller that never comes up polls slowly and quietly. + const uint8_t doublings = std::min(this->failed_start_count_, SCAN_START_RETRY_MAX_DOUBLINGS); + if (now - this->last_scan_start_attempt_ >= (SCAN_START_RETRY_MS << doublings)) { + this->last_scan_start_attempt_ = now; + this->start_scan_(); + if (this->scan_running_) { + this->failed_start_count_ = 0; + } else if (this->failed_start_count_ < SCAN_START_RETRY_MAX_DOUBLINGS) { + ++this->failed_start_count_; + if (this->failed_start_count_ == SCAN_START_RETRY_MAX_DOUBLINGS) { + ESP_LOGW(TAG, "Scan start keeps failing; retrying every %" PRIu32 " s", + (SCAN_START_RETRY_MS << SCAN_START_RETRY_MAX_DOUBLINGS) / 1000); + } + } + } + } + // Period timer: fire on_scan_end() once per scan_duration_ window, mirroring + // esp32_ble_tracker::cleanup_scan_state_(). Gated on scan_started_once_ so a scan + // that never came up (start kept failing) does not fire spurious on_scan_end events. + if (this->scan_started_once_ && now - this->scan_period_start_ >= this->scan_duration_) { +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + for (auto *listener : this->listeners_) + listener->on_scan_end(); + this->discovered_log_.clear(); // reset per-scan "Found device" dedup (esp32_ble_tracker parity) +#endif + this->scan_period_start_ = now; + } + return; + } + + // Non-continuous mode: run for scan_duration_ ms, then stop and fire on_scan_end. + // Restart is driven externally (e.g. api: on_client_connected:). + if (this->scan_running_ && now - this->scan_start_time_ >= this->scan_duration_) { + this->stop_scan_(); + } +} + +void BK72xxBLETracker::dump_config() { + ESP_LOGCONFIG(TAG, + "BK72xx BLE Tracker:\n" + " Scan Duration: %" PRIu32 " s\n" + " Scan Interval: %.0f ms (%" PRIu32 " BLE units)\n" + " Scan Window: %.0f ms (%" PRIu32 " BLE units)\n" + " Scan Type: PASSIVE\n" + " Continuous Scanning: %s", + this->scan_duration_ / 1000, this->scan_interval_ * 0.625f, this->scan_interval_, + this->scan_window_ * 0.625f, this->scan_window_, YESNO(this->scan_continuous_)); +} + +// --------------------------------------------------------------------------- +// Scan report — delivered by the controller's loop() on the ESPHome main task +// (the controller queues reports from the BLE task), so publish_state() and +// listener dispatch run in main-loop context with no cross-task handling here. +// --------------------------------------------------------------------------- + +void BK72xxBLETracker::on_scan_report(const bk72xx_ble::BLEScanReport &report) { + // Raw callback (the raw-advertisement path). + if (this->raw_advertisement_callback_.is_set()) { + const ble_device_base::RawAdvertisement adv{.mac = report.mac, + .data = report.data, + .data_len = report.data_len, + .rssi = report.rssi, + .addr_type = report.addr_type}; + this->raw_advertisement_callback_.invoke(adv); + } + +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + ble_device_base::ESPBTDevice device; + device.from_scan_result(report.mac, report.rssi, report.addr_type, report.data, report.data_len); + bool found = false; + for (auto *listener : this->listeners_) + if (listener->parse_device(device)) + found = true; + // Mirror esp32_ble_tracker: log a newly-seen device only when nothing claimed + // it and the scan is one-shot (continuous scans would spam). + if (!found && !this->scan_continuous_) + this->discovered_log_.log_device(TAG, device); +#endif // ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT +} + +// --------------------------------------------------------------------------- +// Public scan control +// --------------------------------------------------------------------------- + +void BK72xxBLETracker::start_scan() { + // Mirrors esp32_ble_tracker::start_scan(): caller sets scan_continuous_ via + // set_scan_continuous() first, then calls start_scan() to begin scanning. + if (!this->scan_running_) { + this->start_scan_(); + } +} + +void BK72xxBLETracker::stop_scan() { + this->scan_continuous_ = false; + this->stop_scan_(); +} + +// --------------------------------------------------------------------------- +// Internal scan start / stop +// --------------------------------------------------------------------------- + +void BK72xxBLETracker::start_scan_() { + if (this->scan_running_) + return; + + if (!this->parent_->scan_start(static_cast(this->scan_interval_), + static_cast(this->scan_window_))) + return; + + const uint32_t now = millis(); + this->scan_running_ = true; + this->scan_start_time_ = now; + // Log every explicit start at DEBUG — stop_scan_() logs every stop at DEBUG, and + // in non-continuous mode each period is an explicit start, so asymmetric logging + // would read as the scanner failing to come back up. + ESP_LOGD(TAG, "Scan started (passive, window=%.0fms, interval=%.0fms)", this->scan_window_ * 0.625f, + this->scan_interval_ * 0.625f); + // Re-anchor the on_scan_end period to every successful start — first start (so the + // period counts from the scan, not from boot) and every restart after a stop (so + // resuming after longer than scan_duration, e.g. a failed OTA restoring continuous + // mode 10 minutes later, does not fire on_scan_end before an advertisement can + // arrive). scan_started_once_ purely gates the period timer. + this->scan_period_start_ = now; + this->scan_started_once_ = true; +} + +void BK72xxBLETracker::stop_scan_() { + if (!this->scan_running_) + return; + this->parent_->scan_stop(); + this->scan_running_ = false; + ESP_LOGD(TAG, "Scan stopped"); +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + for (auto *listener : this->listeners_) + listener->on_scan_end(); + this->discovered_log_.clear(); // reset per-scan "Found device" dedup (esp32_ble_tracker parity) +#endif + this->scan_period_start_ = millis(); // reset period clock so on_scan_end does not double-fire +} + +} // namespace esphome::bk72xx_ble_tracker + +#endif // USE_LIBRETINY diff --git a/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h new file mode 100644 index 0000000000..9c70246f05 --- /dev/null +++ b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h @@ -0,0 +1,150 @@ +// bk72xx_ble_tracker.h +// +// ESPHome BLE scanner for the BK72xx BLE-5.x chips (LibreTiny beken-72xx family). +// Implements the platform-neutral ble_device_base::BLEHub contract on top of the +// bk72xx_ble controller component: parsed ESPBTDevice objects go to registered +// listeners (bthome_mithermometer, ble_presence, …) and every raw frame to the +// hub's raw-advertisement callback. +// +// This component contains no Beken SDK calls and no cross-task state: the +// controller (stack bring-up, BLE address, scan primitives, and the BLE-task → +// main-task report queue) is owned by bk72xx_ble, which delivers every scan +// report on the ESPHome main task. The tracker owns scan policy — parameters, +// duration/period timers and the rate-limited start retry. +// +// YAML config (values shown are the defaults; interval/window are a 30 % duty +// cycle, the BK reference scan rate): +// +// bk72xx_ble_tracker: +// scan_parameters: +// interval: 100ms +// window: 30ms +// duration: 5min +// continuous: true + +#pragma once + +#ifdef USE_LIBRETINY + +#include "esphome/components/bk72xx_ble/bk72xx_ble.h" +#include "esphome/components/ble_device_base/ble_device.h" +#include "esphome/components/ble_device_base/ble_hub.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include + +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + +namespace esphome::bk72xx_ble_tracker { + +// --------------------------------------------------------------------------- +// BK72xxBLETracker +// --------------------------------------------------------------------------- + +class BK72xxBLETracker : public Component, + public ble_device_base::BLEHub, + public bk72xx_ble::BLEScanListener, + public Parented +#ifdef USE_OTA_STATE_LISTENER + , + public ota::OTAGlobalStateListener +#endif +{ + public: + // ---- ESPHome Component ---- + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + +#ifdef USE_OTA_STATE_LISTENER + // Pause scanning while an OTA update runs (single-core WiFi/BLE/flash contention); + // mirrors esp32_ble_tracker. + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + + // ---- YAML configuration setters ---- + void set_scan_interval(uint32_t scan_interval) { this->scan_interval_ = scan_interval; } + void set_scan_window(uint32_t scan_window) { this->scan_window_ = scan_window; } + void set_scan_duration(uint32_t scan_duration) { this->scan_duration_ = scan_duration; } + void set_scan_continuous(bool scan_continuous) { this->scan_continuous_ = scan_continuous; } + + // ---- Public scan control ---- + // Mirrors esp32_ble_tracker: set_scan_continuous() + start_scan() / stop_scan(). + void start_scan(); + void stop_scan(); + + // ---- ble_device_base::BLEHub contract ---- + void register_listener(ble_device_base::ESPBTDeviceListener *listener) override { +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + this->listeners_.push_back(listener); +#endif + } + void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override { + this->raw_advertisement_callback_ = callback; + } + ble_device_base::HubCapabilities get_capabilities() const override { + // The Beken BDK exposes no active-scan path (passive scanning only), so the + // controller never solicits scan responses and never merges them; consumers + // relying on scan-response fields (device names) get them only where the + // receiver merges per address (Home Assistant does). No GATT client either. + return {.active_scan = false, .merges_scan_response = false, .gatt = false}; + } + // The controller stores the address LSB-first (BLE convention); the contract + // wants printable (MSB-first) order. + void get_adapter_mac(uint8_t out[6]) override { + uint8_t mac[6]; + this->parent_->get_mac_lsb_first(mac); + for (int i = 0; i < 6; i++) + out[i] = mac[5 - i]; + } + bool scan_running() override { return this->scan_running_; } + bool scan_active() override { return false; } // BK72xx scan is passive-only + + // ---- bk72xx_ble::BLEScanListener ---- + // Delivered by the controller's loop() on the ESPHome main task — the + // BLE-task → main-task handoff already happened in the controller's queue. + void on_scan_report(const bk72xx_ble::BLEScanReport &report) override; + + protected: + void start_scan_(); + void stop_scan_(); + + bool scan_running_{false}; + // Defaults: the BK reference — 30 % duty cycle + // (interval 100 ms / window 30 ms), in 0.625 ms BLE units. + uint32_t scan_interval_{160}; // 160 × 0.625 ms = 100 ms + uint32_t scan_window_{48}; // 48 × 0.625 ms = 30 ms (30/100 = 30 %) + uint32_t scan_duration_{300000}; + bool scan_continuous_{true}; +#ifdef USE_OTA_STATE_LISTENER + bool scan_continuous_before_ota_{false}; // continuous mode saved at OTA start, restored on OTA failure +#endif + uint32_t scan_start_time_{0}; + + uint32_t last_scan_start_attempt_{0}; // millis() of last start_scan_() attempt; rate-limits retries + uint8_t failed_start_count_{0}; // consecutive failed starts; drives the retry backoff (reset on success) + uint32_t scan_period_start_{0}; // millis() at start of current scan period; used to rate-limit on_scan_end() + bool scan_started_once_{false}; // true after first successful scan start; gates the period timer + + ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{}; +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + // Parsed-advertisement consumers registered through ble_device_base. + // Codegen-sized: no heap allocation, no std::vector template instantiations. + StaticVector listeners_; +#endif + +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + // Per-period "Found device" DEBUG log with MAC dedup — shared implementation + // in ble_device_base, identical output on every tracker backend. Guarded like + // its only writer so a no-listener build does not carry an unused vector. + ble_device_base::DiscoveredDeviceLog discovered_log_{}; +#endif +}; + +} // namespace esphome::bk72xx_ble_tracker + +#endif // USE_LIBRETINY diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index d41fb17961..25001c8f74 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -51,7 +51,9 @@ bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es for (auto *node : this->nodes_) node->gattc_event_handler(event, esp_gattc_if, param); - if (!this->services_.empty() && this->all_nodes_established_()) { + // The release frees the GATT cache that BLEClientBase's CCCD lookup still needs. + // The last REG_FOR_NOTIFY event clears the counter before node dispatch, so the release still runs here. + if (!this->services_.empty() && !this->notify_registration_pending() && this->all_nodes_established_()) { this->release_services(); ESP_LOGD(TAG, "All clients established, services released"); } diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index f27bef332b..f20df31816 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -34,6 +34,11 @@ class BLEClientNode { // This should be transitioned to Established once the node no longer needs // the services/descriptors/characteristics of the parent client. This will // allow some memory to be freed. + // The parent frees the peer's GATT cache once every node reports Established. + // Never report Established while an operation that reads that cache is outstanding. + // - esp_ble_gattc_register_for_notify() completes asynchronously. + // - Register from ESP_GATTC_SEARCH_CMPL_EVT, then set this from ESP_GATTC_REG_FOR_NOTIFY_EVT. + // - BLEClientBase::register_for_notify() holds the release until the registration completes. espbt::ClientState node_state; BLEClient *parent() { return this->parent_; } diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 4bd871dc81..60992f282e 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -77,8 +77,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga this->handle = descr->handle; } if (this->notify_) { - auto status = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), - this->parent()->get_remote_bda(), chr->handle); + auto status = this->parent()->register_for_notify(chr->handle); if (status) { ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); } diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index 7eaa6af076..6f09281922 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -77,8 +77,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->handle = descr->handle; } if (this->notify_) { - auto status = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), - this->parent()->get_remote_bda(), chr->handle); + auto status = this->parent()->register_for_notify(chr->handle); if (status) { ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); } diff --git a/esphome/components/ble_device_base/__init__.py b/esphome/components/ble_device_base/__init__.py new file mode 100644 index 0000000000..d9789b0e9f --- /dev/null +++ b/esphome/components/ble_device_base/__init__.py @@ -0,0 +1,134 @@ +""" +ble_device_base — the platform-neutral BLE layer. + +Owns the shared advertisement types (ESPBTUUID / ESPBTDevice / ServiceData / +ESPBLEiBeacon / ESPBTDeviceListener, in ble_device.h) and the tracker contract +(BLEHub, in ble_hub.h) on every platform. + +BLE consumers (sensor components, bluetooth_proxy) bind to whichever tracker the +configuration declares via `cv.use_id(BLEHub)` — ESPHome resolves any declared +subclass, so there is no platform table here and no dependency in either +direction. A sensor appends inject_ble_hub to its CONFIG_SCHEMA (via cv.All) and +calls register_ble_device() in to_code; a tracker component subclasses BLEHub +(C++ and codegen class). Adding a new BLE chip requires only a new tracker +component. + +AES-CCM decryption for encrypted advertisements is provided portably in +ble_aes_ccm.h. +""" + +import re + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.core import CORE +from esphome.types import ConfigType + +CODEOWNERS = ["@Bl00d-B0b"] + +CONF_BLE_HUB_ID = "ble_hub_id" + +# CORE.data key: number of parsed-advertisement listeners registered in this +# build. Trackers whose codegen sizes storage at compile time (esp32's +# StaticVector count define) read it in their final coroutine. +KEY_BLE_LISTENER_COUNT = "ble_device_base_listener_count" + +ble_device_base_ns = cg.esphome_ns.namespace("ble_device_base") + +# The neutral tracker contract. Every tracker's codegen class declares this as a +# parent, which is what lets cv.use_id(BLEHub) resolve any of them. +BLEHub = ble_device_base_ns.class_("BLEHub") + +# The neutral listener base (C++: ble_device_base::ESPBTDeviceListener). +ESPBTDeviceListener = ble_device_base_ns.class_("ESPBTDeviceListener") + + +def inject_ble_hub(config: ConfigType) -> ConfigType: + """Validator: auto-resolve the configured BLE tracker into the config. + + Append via cv.All to a BLE consumer's CONFIG_SCHEMA. Uses cv.GenerateID + + cv.use_id(BLEHub): an omitted id resolves to the single declared tracker on + any platform; multiple trackers can be disambiguated with an explicit + ble_hub_id. + """ + return cv.Schema( + {cv.GenerateID(CONF_BLE_HUB_ID): cv.use_id(BLEHub)}, extra=cv.ALLOW_EXTRA + )(config) + + +def request_irk_support() -> None: + """Compile in resolve_irk()'s software-AES path. Called by sensors with an + irk: option so builds without IRK do not carry the resolution code.""" + cg.add_define("USE_BLE_DEVICE_IRK") + + +def get_listener_count() -> int: + """Number of parsed listeners registered so far (for tracker codegen).""" + return CORE.data.get(KEY_BLE_LISTENER_COUNT, 0) + + +async def register_ble_device(var: cg.MockObj, config: ConfigType) -> cg.MockObj: + """Register `var` as a parsed-advertisement listener on the configured hub.""" + hub = await cg.get_variable(config[CONF_BLE_HUB_ID]) + cg.add(hub.register_listener(var)) + CORE.data[KEY_BLE_LISTENER_COUNT] = CORE.data.get(KEY_BLE_LISTENER_COUNT, 0) + 1 + return var + + +# ---- shared validation / codegen helpers (platform-neutral) ---- +BT_UUID16_FORMAT = "XXXX" +BT_UUID32_FORMAT = "XXXXXXXX" +BT_UUID128_FORMAT = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + +_BT_UUID16_RE = re.compile("^[A-F0-9]{4,}$") +_BT_UUID32_RE = re.compile("^[A-F0-9]{8,}$") +_BT_UUID128_RE = re.compile( + "^[A-F0-9]{8,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{12,}$" +) + + +# Validator table keyed by input length: (compiled pattern, label used in errors). +_BT_UUID_FORMATS = { + len(BT_UUID16_FORMAT): (_BT_UUID16_RE, "16 bit"), + len(BT_UUID32_FORMAT): (_BT_UUID32_RE, "32 bit"), + len(BT_UUID128_FORMAT): (_BT_UUID128_RE, "128"), +} + + +def bt_uuid(value: str) -> str: + in_value = cv.string_strict(value) + value = in_value.upper() + + fmt = _BT_UUID_FORMATS.get(len(value)) + if fmt is None: + raise cv.Invalid( + f"Bluetooth UUID must be in 16 bit '{BT_UUID16_FORMAT}', 32 bit '{BT_UUID32_FORMAT}', or 128 bit '{BT_UUID128_FORMAT}' format" + ) + pattern, label = fmt + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for {label} UUID format: '{in_value}'" + ) + return value + + +def as_hex(value: str) -> cg.RawExpression: + return cg.RawExpression(f"0x{value}ULL") + + +def _hex_array_expression(value: str, reverse: bool) -> cg.RawExpression: + value = value.replace("-", "") + cpp_array = [ + f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] + ] + if reverse: + cpp_array.reverse() + return cg.RawExpression(f"(uint8_t*)(const uint8_t[16]){{{','.join(cpp_array)}}}") + + +def as_hex_array(value: str) -> cg.RawExpression: + return _hex_array_expression(value, reverse=False) + + +def as_reversed_hex_array(value: str) -> cg.RawExpression: + return _hex_array_expression(value, reverse=True) diff --git a/esphome/components/ble_device_base/ble_aes_ccm.cpp b/esphome/components/ble_device_base/ble_aes_ccm.cpp new file mode 100644 index 0000000000..3ff34acc34 --- /dev/null +++ b/esphome/components/ble_device_base/ble_aes_ccm.cpp @@ -0,0 +1,202 @@ +#include "ble_aes_ccm.h" + +#include +#include + +namespace esphome::ble_device_base { + +namespace { + +// AES-128 forward cipher only — CCM uses the block cipher in the encrypt +// direction for both the CTR keystream and the CBC-MAC. +const uint8_t SBOX[256] = { + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, // + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, // + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, // + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, // + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, // + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, // + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, // + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, // + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, // + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, // + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, // + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, // + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, // + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, // + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, // + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, // +}; + +const uint8_t RCON[11] = {0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36}; + +inline uint8_t xtime(uint8_t x) { return static_cast((x << 1) ^ ((x & 0x80) ? 0x1b : 0x00)); } + +// AES-128 forward cipher with on-the-fly key schedule. +class Aes128 { + public: + explicit Aes128(const uint8_t key[16]) { + memcpy(this->rk_, key, 16); + for (size_t i = 16; i < 176; i += 4) { + uint8_t t[4] = {this->rk_[i - 4], this->rk_[i - 3], this->rk_[i - 2], this->rk_[i - 1]}; + if (i % 16 == 0) { + const uint8_t tmp = t[0]; + t[0] = static_cast(SBOX[t[1]] ^ RCON[i / 16]); + t[1] = SBOX[t[2]]; + t[2] = SBOX[t[3]]; + t[3] = SBOX[tmp]; + } + for (size_t j = 0; j < 4; j++) + this->rk_[i + j] = static_cast(this->rk_[i - 16 + j] ^ t[j]); + } + } + + void encrypt(const uint8_t in[16], uint8_t out[16]) const { + uint8_t s[16]; + memcpy(s, in, 16); + for (size_t i = 0; i < 16; i++) + s[i] ^= this->rk_[i]; + + for (size_t round = 1; round < 10; round++) { + for (uint8_t &b : s) + b = SBOX[b]; + shift_rows(s); + for (size_t c = 0; c < 4; c++) { + uint8_t *col = s + c * 4; + const uint8_t a0 = col[0], a1 = col[1], a2 = col[2], a3 = col[3]; + const uint8_t h = static_cast(a0 ^ a1 ^ a2 ^ a3); + col[0] ^= static_cast(h ^ xtime(static_cast(a0 ^ a1))); + col[1] ^= static_cast(h ^ xtime(static_cast(a1 ^ a2))); + col[2] ^= static_cast(h ^ xtime(static_cast(a2 ^ a3))); + col[3] ^= static_cast(h ^ xtime(static_cast(a3 ^ a0))); + } + for (size_t i = 0; i < 16; i++) + s[i] ^= this->rk_[round * 16 + i]; + } + + for (uint8_t &b : s) + b = SBOX[b]; + shift_rows(s); + for (size_t i = 0; i < 16; i++) + s[i] ^= this->rk_[160 + i]; + memcpy(out, s, 16); + } + + protected: + static void shift_rows(uint8_t s[16]) { + uint8_t t = s[1]; + s[1] = s[5]; + s[5] = s[9]; + s[9] = s[13]; + s[13] = t; + t = s[2]; + s[2] = s[10]; + s[10] = t; + t = s[6]; + s[6] = s[14]; + s[14] = t; + t = s[3]; + s[3] = s[15]; + s[15] = s[11]; + s[11] = s[7]; + s[7] = t; + } + + uint8_t rk_[176]; +}; + +} // namespace + +void aes128_encrypt_block(const uint8_t key[16], const uint8_t in[16], uint8_t out[16]) { + Aes128 aes(key); + aes.encrypt(in, out); +} + +bool aes_ccm_auth_decrypt(const uint8_t key[16], const uint8_t *nonce, size_t nonce_len, const uint8_t *aad, + size_t aad_len, const uint8_t *ciphertext, size_t ct_len, uint8_t *plaintext, + const uint8_t *tag, size_t tag_len) { + // CCM length field width L and tag width M (RFC 3610 §2.2). For a 13-byte + // nonce L = 2; BTHome uses M = 4. + if (nonce_len < 7 || nonce_len > 13 || tag_len < 4 || tag_len > 16) + return false; + const size_t l = 15 - nonce_len; + const size_t m = tag_len; + + const Aes128 aes(key); + + // Build CTR block A_i = [L-1] | nonce | counter(L bytes, big-endian). + uint8_t a[16]; + auto build_ctr = [&](uint32_t counter) { + a[0] = static_cast(l - 1); + memcpy(a + 1, nonce, nonce_len); + memset(a + 1 + nonce_len, 0, l); + for (size_t i = 0; i < l; i++) + a[15 - i] = static_cast((counter >> (8 * i)) & 0xff); + }; + + // S_0 = E(A_0); its first m bytes mask the transmitted tag. + uint8_t s0[16]; + build_ctr(0); + aes.encrypt(a, s0); + + // CTR-decrypt ciphertext into plaintext using S_1, S_2, ... + uint8_t ks[16]; + for (size_t off = 0; off < ct_len; off += 16) { + build_ctr(static_cast(off / 16) + 1); + aes.encrypt(a, ks); + const size_t n = std::min(static_cast(16), ct_len - off); + for (size_t i = 0; i < n; i++) + plaintext[off + i] = static_cast(ciphertext[off + i] ^ ks[i]); + } + + // CBC-MAC over B_0 | (formatted AAD) | plaintext. + uint8_t x[16]; + uint8_t b0[16]; + const uint8_t flags = static_cast((aad_len > 0 ? 0x40 : 0x00) | (((m - 2) / 2) << 3) | (l - 1)); + b0[0] = flags; + memcpy(b0 + 1, nonce, nonce_len); + memset(b0 + 1 + nonce_len, 0, l); + for (size_t i = 0; i < l; i++) + b0[15 - i] = static_cast((ct_len >> (8 * i)) & 0xff); + aes.encrypt(b0, x); // X_1 = E(B_0) + + if (aad_len > 0) { + // Only the < 2^16-2^8 encoding is needed for BLE-sized AAD. + uint8_t blk[16] = {0}; + blk[0] = static_cast((aad_len >> 8) & 0xff); + blk[1] = static_cast(aad_len & 0xff); + size_t ai = 0; + size_t pos = 2; + while (pos < 16 && ai < aad_len) + blk[pos++] = aad[ai++]; + for (size_t i = 0; i < 16; i++) + x[i] ^= blk[i]; + aes.encrypt(x, x); + while (ai < aad_len) { + memset(blk, 0, 16); + const size_t n = std::min(static_cast(16), aad_len - ai); + memcpy(blk, aad + ai, n); + ai += n; + for (size_t i = 0; i < 16; i++) + x[i] ^= blk[i]; + aes.encrypt(x, x); + } + } + + for (size_t off = 0; off < ct_len; off += 16) { + uint8_t blk[16] = {0}; + const size_t n = std::min(static_cast(16), ct_len - off); + memcpy(blk, plaintext + off, n); + for (size_t i = 0; i < 16; i++) + x[i] ^= blk[i]; + aes.encrypt(x, x); + } + + // Expected tag U = T XOR S_0[0..m). Constant-time compare with the received tag. + uint8_t diff = 0; + for (size_t i = 0; i < m; i++) + diff |= static_cast((x[i] ^ s0[i]) ^ tag[i]); + return diff == 0; +} + +} // namespace esphome::ble_device_base diff --git a/esphome/components/ble_device_base/ble_aes_ccm.h b/esphome/components/ble_device_base/ble_aes_ccm.h new file mode 100644 index 0000000000..d337ad8ecd --- /dev/null +++ b/esphome/components/ble_device_base/ble_aes_ccm.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace esphome::ble_device_base { + +// Self-contained AES-128-CCM authenticated decryption (RFC 3610). +// +// Encrypted BLE advertisements (BTHome, several Xiaomi/ATC variants) use +// AES-128-CCM. The platform crypto that provides it is inconsistent across BLE +// targets: ESP-IDF exposes PSA/mbedtls, but a LibreTiny SDK may keep its mbedtls +// internal (e.g. the beken-72xx SDK ships mbedtls with CCM enabled but does not +// put it on the application include path), so a sensor cannot rely on +// being available. This software implementation makes +// encrypted-advertisement decryption work on every BLE platform without a +// per-chip crypto dependency. Decryption volume is tiny (one short block per +// matching advertisement), so software AES is not a meaningful cost. +// +// Verifies the CCM authentication tag and, on success, writes `ct_len` decrypted +// bytes to `plaintext` and returns true. Returns false when authentication fails +// (the caller must then discard `plaintext`). The CCM parameters follow the +// caller (BTHome: 13-byte nonce, 4-byte tag, no associated data); `aad` may be +// null when `aad_len` is 0. +/// AES-128 single-block encrypt (the same software cipher CCM uses). Used by +/// ESPBTDevice::resolve_irk() for the Bluetooth "ah" RPA hash, so IRK matching +/// works identically on every platform with no chip crypto dependency. +void aes128_encrypt_block(const uint8_t key[16], const uint8_t in[16], uint8_t out[16]); + +bool aes_ccm_auth_decrypt(const uint8_t key[16], const uint8_t *nonce, size_t nonce_len, const uint8_t *aad, + size_t aad_len, const uint8_t *ciphertext, size_t ct_len, uint8_t *plaintext, + const uint8_t *tag, size_t tag_len); + +} // namespace esphome::ble_device_base diff --git a/esphome/components/ble_device_base/ble_device.cpp b/esphome/components/ble_device_base/ble_device.cpp new file mode 100644 index 0000000000..9c3e1d4397 --- /dev/null +++ b/esphome/components/ble_device_base/ble_device.cpp @@ -0,0 +1,530 @@ +// ble_device.cpp +// +// Platform-neutral implementation of the shared BLE advertisement types. +// Parses raw BLE advertisement data into ESPBTDevice. + +#include "ble_device.h" + +#include "ble_aes_ccm.h" + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::ble_device_base { + +static const char *const TAG = "ble_device_base"; + +// Longest advertisement payload worth hex-dumping at VERY_VERBOSE +// (legacy advertising: 31-byte adv + 31-byte scan response). +static constexpr size_t BLE_ADV_MAX_LOG_BYTES = 62; + +// --------------------------------------------------------------------------- +// ESPBTUUID +// --------------------------------------------------------------------------- + +ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { + ESPBTUUID ret; + ret.type_ = Type::UUID16; + ret.uuid_.uuid16 = uuid; + return ret; +} + +ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { + ESPBTUUID ret; + ret.type_ = Type::UUID32; + ret.uuid_.uuid32 = uuid; + return ret; +} + +ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { + ESPBTUUID ret; + ret.type_ = Type::UUID128; + memcpy(ret.uuid_.uuid128, data, 16); + return ret; +} + +ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) { + ESPBTUUID ret; + ret.type_ = Type::UUID128; + for (int i = 0; i < 16; i++) + ret.uuid_.uuid128[i] = data[15 - i]; + return ret; +} + +ESPBTUUID ESPBTUUID::from_raw(const char *data, size_t length) { + // Same text-parsing semantics as the historical esp32_ble::ESPBTUUID::from_raw. + ESPBTUUID ret; + if (length == 4) { + // 16-bit UUID as 4-character hex string + auto parsed = parse_hex(data, length); + if (parsed.has_value()) { + ret.type_ = Type::UUID16; + ret.uuid_.uuid16 = parsed.value(); + } + } else if (length == 8) { + // 32-bit UUID as 8-character hex string + auto parsed = parse_hex(data, length); + if (parsed.has_value()) { + ret.type_ = Type::UUID32; + ret.uuid_.uuid32 = parsed.value(); + } + } else if (length == 16) { + // 16 raw bytes (little-endian 128-bit UUID) + ret.type_ = Type::UUID128; + memcpy(ret.uuid_.uuid128, reinterpret_cast(data), 16); + } else if (length == 36) { + // Dashed text form XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + ret.type_ = Type::UUID128; + int n = 0; + for (size_t i = 0; i < length; i += 2) { + if (data[i] == '-') + i++; + uint8_t msb = data[i]; + uint8_t lsb = data[i + 1]; + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F); + } + } else { + ESP_LOGE(TAG, "ERROR: UUID value not 4, 8, 16 or 36 bytes - %s", data); + } + return ret; +} + +#ifdef USE_ESP32 +ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { + if (uuid.len == ESP_UUID_LEN_16) + return ESPBTUUID::from_uint16(uuid.uuid.uuid16); + if (uuid.len == ESP_UUID_LEN_32) + return ESPBTUUID::from_uint32(uuid.uuid.uuid32); + return ESPBTUUID::from_raw(uuid.uuid.uuid128); +} + +esp_bt_uuid_t ESPBTUUID::get_uuid() const { + esp_bt_uuid_t ret; + switch (this->type_) { + case Type::UUID16: + ret.len = ESP_UUID_LEN_16; + ret.uuid.uuid16 = this->uuid_.uuid16; + break; + case Type::UUID32: + ret.len = ESP_UUID_LEN_32; + ret.uuid.uuid32 = this->uuid_.uuid32; + break; + default: + case Type::UUID128: + ret.len = ESP_UUID_LEN_128; + memcpy(ret.uuid.uuid128, this->uuid_.uuid128, ESP_UUID_LEN_128); + break; + } + return ret; +} + +void ESPBTDevice::parse_scan_rst(const esp32_ble::BLEScanResult &scan_result) { + this->scan_result_ = &scan_result; + // BLEScanResult's bda is most-significant octet first; the neutral ingest + // takes the BLE controller (LSB-first) order, so reverse — address_uint64()/ + // address_str() then produce exactly the historical esp32 values. + uint8_t mac_lsb_first[6]; + for (uint8_t i = 0; i < 6; i++) + mac_lsb_first[i] = scan_result.bda[5 - i]; + this->from_scan_result(mac_lsb_first, scan_result.rssi, scan_result.ble_addr_type, scan_result.ble_adv, + scan_result.adv_data_len + scan_result.scan_rsp_len); +} +#endif // USE_ESP32 + +ESPBTUUID ESPBTUUID::as_128bit() const { + if (this->type_ == Type::UUID128) + return *this; + uint8_t data[16]; + this->to_128bit_(data); + return ESPBTUUID::from_raw(data); +} + +bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { + // Adjacent byte-pair search — identical semantics to esp32_ble::ESPBTUUID::contains. + switch (this->type_) { + case Type::UUID16: + return (this->uuid_.uuid16 >> 8) == data2 && (this->uuid_.uuid16 & 0xFF) == data1; + case Type::UUID32: + for (uint8_t i = 0; i < 3; i++) { + bool a = ((this->uuid_.uuid32 >> i * 8) & 0xFF) == data1; + bool b = ((this->uuid_.uuid32 >> (i + 1) * 8) & 0xFF) == data2; + if (a && b) + return true; + } + return false; + case Type::UUID128: + for (uint8_t i = 0; i < 15; i++) { + if (this->uuid_.uuid128[i] == data1 && this->uuid_.uuid128[i + 1] == data2) + return true; + } + return false; + } + return false; +} + +const char *ESPBTUUID::to_str(char *buf) const { + // Identical output format to esp32_ble::ESPBTUUID::to_str. + char *pos = buf; + switch (this->type_) { + case Type::UUID16: + *pos++ = '0'; + *pos++ = 'x'; + *pos++ = format_hex_pretty_char(this->uuid_.uuid16 >> 12); + *pos++ = format_hex_pretty_char((this->uuid_.uuid16 >> 8) & 0x0F); + *pos++ = format_hex_pretty_char((this->uuid_.uuid16 >> 4) & 0x0F); + *pos++ = format_hex_pretty_char(this->uuid_.uuid16 & 0x0F); + *pos = 0; // NUL-terminate + return buf; + case Type::UUID32: + *pos++ = '0'; + *pos++ = 'x'; + for (int shift = 28; shift >= 0; shift -= 4) + *pos++ = format_hex_pretty_char((this->uuid_.uuid32 >> shift) & 0x0F); + *pos = 0; // NUL-terminate + return buf; + default: + case Type::UUID128: + // Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + for (int8_t i = 15; i >= 0; i--) { + uint8_t byte = this->uuid_.uuid128[i]; + *pos++ = format_hex_pretty_char(byte >> 4); + *pos++ = format_hex_pretty_char(byte & 0x0F); + if (i == 12 || i == 10 || i == 8 || i == 6) + *pos++ = '-'; + } + *pos = 0; // NUL-terminate + return buf; + } +} + +void ESPBTUUID::to_128bit_(uint8_t out[16]) const { + // Bluetooth Base UUID 00000000-0000-1000-8000-00805F9B34FB (LSB-first), with the 16/32-bit + // value placed at bytes 12..; identical expansion to esp32_ble::ESPBTUUID::as_128bit(). + static const uint8_t BASE[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + if (this->type_ == Type::UUID128) { + memcpy(out, this->uuid_.uuid128, 16); + return; + } + memcpy(out, BASE, 16); + const uint32_t value = (this->type_ == Type::UUID32) ? this->uuid_.uuid32 : this->uuid_.uuid16; + const size_t len = (this->type_ == Type::UUID32) ? 4 : 2; + for (size_t i = 0; i < len; i++) + out[12 + i] = (value >> (i * 8)) & 0xFF; +} + +bool ESPBTUUID::operator==(const ESPBTUUID &other) const { + if (this->type_ == other.type_) { + switch (this->type_) { + case Type::UUID16: + return this->uuid_.uuid16 == other.uuid_.uuid16; + case Type::UUID32: + return this->uuid_.uuid32 == other.uuid_.uuid32; + case Type::UUID128: + return memcmp(this->uuid_.uuid128, other.uuid_.uuid128, 16) == 0; + } + return false; + } + // Different widths: expand both to the 128-bit Bluetooth Base UUID form and compare, so a + // configured 16/32-bit UUID matches the equivalent 128-bit advertisement (esp32 parity). + uint8_t a[16]; + uint8_t b[16]; + this->to_128bit_(a); + other.to_128bit_(b); + return memcmp(a, b, 16) == 0; +} + +// --------------------------------------------------------------------------- +// ESPBLEiBeacon +// --------------------------------------------------------------------------- + +ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(this->beacon_data_)); } + +optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { + // iBeacon manufacturer specific data (after company-ID bytes have been stripped): + // [0x02][0x15][16-byte UUID][2-byte major][2-byte minor][1-byte power] = exactly 23 bytes + // Parity with esp32_ble_tracker: gate on the Apple company ID and length only. + // (Checking the 0x02/0x15 sub-type prefix would be stricter, but is a behavior + // change; it belongs to a follow-up, not this refactor.) + if (!data.uuid.contains(0x4C, 0x00)) // Apple company ID 0x004C + return {}; + if (data.data.size() != 23) + return {}; + return ESPBLEiBeacon(data.data.data()); +} + +// --------------------------------------------------------------------------- +// ESPBTDevice +// --------------------------------------------------------------------------- + +const char *ESPBTDevice::address_type_str() const { + switch (this->address_type_) { + case BLE_ADDR_TYPE_PUBLIC: + return "PUBLIC"; + case BLE_ADDR_TYPE_RANDOM: + return "RANDOM"; + case BLE_ADDR_TYPE_RPA_PUBLIC: + return "RPA_PUBLIC"; + case BLE_ADDR_TYPE_RPA_RANDOM: + return "RPA_RANDOM"; + default: + return "UNKNOWN"; + } +} + +void ESPBTDevice::from_scan_result(const uint8_t *mac, int rssi, uint8_t addr_type, const uint8_t *data, + uint16_t data_len) { + // Ingest is BLE controller order (LSB-first); store in printable (MSB-first) + // order so the raw address() accessor matches the historical esp32 layout. + for (uint8_t i = 0; i < 6; i++) + this->address_[i] = mac[5 - i]; + this->address_type_ = addr_type; + this->rssi_ = rssi; + this->name_.clear(); + this->service_uuids_.clear(); + this->manufacturer_datas_.clear(); + this->service_datas_.clear(); + this->tx_powers_.clear(); + this->appearance_.reset(); + this->ad_flag_.reset(); + this->parse_adv_(data, data_len); + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGVV(TAG, + "Parse Result:\n" + " Address: %s (%s)\n" + " RSSI: %d\n" + " Name: '%s'", + this->address_str_to(addr_buf), this->address_type_str(), this->rssi_, this->name_.c_str()); + for (auto &it : this->tx_powers_) { + ESP_LOGVV(TAG, " TX Power: %d", it); + } + if (this->appearance_.has_value()) { + ESP_LOGVV(TAG, " Appearance: %u", *this->appearance_); + } + if (this->ad_flag_.has_value()) { + ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_); + } + char uuid_buf[UUID_STR_LEN]; + for (auto &uuid : this->service_uuids_) { + ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_str(uuid_buf)); + } + char hex_buf[format_hex_pretty_size(BLE_ADV_MAX_LOG_BYTES)]; + for (auto &mfg_data : this->manufacturer_datas_) { + auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(mfg_data); + if (ibeacon.has_value()) { + ESP_LOGVV(TAG, + " Manufacturer iBeacon:\n" + " UUID: %s\n" + " Major: %u\n" + " Minor: %u\n" + " TXPower: %d", + ibeacon.value().get_uuid().to_str(uuid_buf), ibeacon.value().get_major(), ibeacon.value().get_minor(), + ibeacon.value().get_signal_power()); + } else { + ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", mfg_data.uuid.to_str(uuid_buf), + format_hex_pretty_to(hex_buf, mfg_data.data.data(), mfg_data.data.size())); + } + } + for (auto &svc_data : this->service_datas_) { + ESP_LOGVV(TAG, + " Service data:\n" + " UUID: %s\n" + " Data: %s", + svc_data.uuid.to_str(uuid_buf), + format_hex_pretty_to(hex_buf, svc_data.data.data(), svc_data.data.size())); + } + ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty_to(hex_buf, data, data_len)); +#endif // ESPHOME_LOG_HAS_VERY_VERBOSE +} + +std::string ESPBTDevice::address_str() const { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(this->address_str_to(buf)); +} + +const char *ESPBTDevice::address_str_to(char *buf) const { + // address_ is stored in printable (MSB-first) order. + format_mac_addr_upper(this->address_, buf); + return buf; +} + +uint64_t ESPBTDevice::address_uint64() const { + // address_ is MSB-first; byte 0 of the result is the LSB (esp32 semantics). + uint64_t addr = 0; + for (int i = 0; i < 6; i++) + addr |= static_cast(this->address_[i]) << ((5 - i) * 8); + return addr; +} + +bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { +#ifdef USE_BLE_DEVICE_IRK + // Bluetooth Core 5.x "ah" function: hash = e(IRK, padding | prand)[low 24 bits]. + // The resolvable private address is prand (top 3 bytes) | hash (bottom 3 bytes). + // Uses the portable software AES-128 shared with the CCM decryptor, so IRK + // matching behaves identically on every platform (volume is one block per + // advertisement from a matching RPA device — software AES is not a cost). + uint8_t ecb_plaintext[16] = {0}; + uint8_t ecb_ciphertext[16]; + const uint64_t addr64 = this->address_uint64(); + ecb_plaintext[13] = (addr64 >> 40) & 0xff; + ecb_plaintext[14] = (addr64 >> 32) & 0xff; + ecb_plaintext[15] = (addr64 >> 24) & 0xff; + aes128_encrypt_block(irk, ecb_plaintext, ecb_ciphertext); + return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && + ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); +#else + // No sensor configured an irk: in this build; the AES core is compiled out. + (void) irk; + return false; +#endif +} + +void ESPBTDevice::parse_adv_(const uint8_t *payload, uint16_t len) { + // BLE AD structure TLV: [length][type][value...] + // length includes the type byte. + uint16_t offset = 0; + while (offset < len) { + uint8_t ad_len = payload[offset++]; + if (ad_len == 0) + continue; // possible zero-padded advertisement data (esp32_ble_tracker skips these too) + if (offset + ad_len > len) + break; + uint8_t ad_type = payload[offset]; + const uint8_t *ad_data = &payload[offset + 1]; + uint8_t ad_data_len = ad_len - 1; + offset += ad_len; + + switch (ad_type) { + case 0x01: // Flags + if (ad_data_len >= 1) + this->ad_flag_ = ad_data[0]; + break; + + case 0x08: // Shortened Local Name + case 0x09: // Complete Local Name + // Keep the longest name seen — a merged adv + scan-response frame may carry both the + // shortened and the complete name, and the shortened form must never replace the + // complete one (same rule as esp32_ble_tracker's parse_adv_). + if (ad_data_len > this->name_.length()) + this->name_.assign(reinterpret_cast(ad_data), ad_data_len); + break; + + case 0x0A: // TX Power Level + if (ad_data_len >= 1) + this->tx_powers_.push_back(static_cast(ad_data[0])); + break; + + case 0x19: // Appearance + if (ad_data_len >= 2) + this->appearance_ = static_cast(ad_data[0]) | (static_cast(ad_data[1]) << 8); + break; + + case 0x02: // Incomplete List of 16-bit Service UUIDs + case 0x03: // Complete List of 16-bit Service UUIDs + for (uint8_t i = 0; (i + 1) < ad_data_len; i += 2) { + uint16_t uuid = (static_cast(ad_data[i + 1]) << 8) | ad_data[i]; + this->service_uuids_.push_back(ESPBTUUID::from_uint16(uuid)); + } + break; + + case 0x04: // Incomplete List of 32-bit Service UUIDs + case 0x05: // Complete List of 32-bit Service UUIDs + for (uint8_t i = 0; (i + 3) < ad_data_len; i += 4) { + uint32_t uuid = (static_cast(ad_data[i + 3]) << 24) | + (static_cast(ad_data[i + 2]) << 16) | (static_cast(ad_data[i + 1]) << 8) | + ad_data[i]; + this->service_uuids_.push_back(ESPBTUUID::from_uint32(uuid)); + } + break; + + case 0x06: // Incomplete List of 128-bit Service UUIDs + case 0x07: // Complete List of 128-bit Service UUIDs + for (uint8_t i = 0; (i + 15) < ad_data_len; i += 16) + this->service_uuids_.push_back(ESPBTUUID::from_raw(&ad_data[i])); + break; + + case 0xFF: // Manufacturer Specific Data + if (ad_data_len >= 2) { + uint16_t company_id = (static_cast(ad_data[1]) << 8) | ad_data[0]; + ServiceData sd; + sd.uuid = ESPBTUUID::from_uint16(company_id); + sd.data.assign(ad_data + 2, ad_data + ad_data_len); + this->manufacturer_datas_.push_back(std::move(sd)); + } + break; + + case 0x16: // Service Data — 16-bit UUID + if (ad_data_len >= 2) { + uint16_t uuid = (static_cast(ad_data[1]) << 8) | ad_data[0]; + ServiceData sd; + sd.uuid = ESPBTUUID::from_uint16(uuid); + sd.data.assign(ad_data + 2, ad_data + ad_data_len); + this->service_datas_.push_back(std::move(sd)); + } + break; + + case 0x20: // Service Data — 32-bit UUID + if (ad_data_len >= 4) { + uint32_t uuid = (static_cast(ad_data[3]) << 24) | (static_cast(ad_data[2]) << 16) | + (static_cast(ad_data[1]) << 8) | ad_data[0]; + ServiceData sd; + sd.uuid = ESPBTUUID::from_uint32(uuid); + sd.data.assign(ad_data + 4, ad_data + ad_data_len); + this->service_datas_.push_back(std::move(sd)); + } + break; + + case 0x21: // Service Data — 128-bit UUID + if (ad_data_len >= 16) { + ServiceData sd; + sd.uuid = ESPBTUUID::from_raw(ad_data); + sd.data.assign(ad_data + 16, ad_data + ad_data_len); + this->service_datas_.push_back(std::move(sd)); + } + break; + + default: + break; + } + } +} + +// --------------------------------------------------------------------------- +// DiscoveredDeviceLog +// --------------------------------------------------------------------------- + +void DiscoveredDeviceLog::log_device(const char *tag, const ESPBTDevice &device) { +#ifdef ESPHOME_LOG_HAS_DEBUG + // Everything here feeds ESP_LOGD: below DEBUG the whole body (including the + // dedup vector growth) would be pure overhead, so compile it out entirely. + const uint64_t address = device.address_uint64(); + for (auto &disc : this->already_discovered_) { + if (disc == address) + return; + } + this->already_discovered_.push_back(address); + + char addr_buf[ESPBTDevice::MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGD(tag, + "Found device %s RSSI=%d\n" + " Address Type: %s", + device.address_str_to(addr_buf), device.get_rssi(), device.address_type_str()); + if (!device.get_name().empty()) { + ESP_LOGD(tag, " Name: '%s'", device.get_name().c_str()); + } + for (auto &tx_power : device.get_tx_powers()) { + ESP_LOGD(tag, " TX Power: %d", tx_power); + } +#endif // ESPHOME_LOG_HAS_DEBUG +} + +} // namespace esphome::ble_device_base diff --git a/esphome/components/ble_device_base/ble_device.h b/esphome/components/ble_device_base/ble_device.h new file mode 100644 index 0000000000..2d2cb5796b --- /dev/null +++ b/esphome/components/ble_device_base/ble_device.h @@ -0,0 +1,274 @@ +// ble_device.h +// +// Platform-neutral BLE advertisement types — the generic base every BLE consumer +// (sensor components, bluetooth_proxy, automation triggers) builds against: +// ESPBTUUID / ServiceData / ESPBLEiBeacon / ESPBTDevice / ESPBTDeviceListener +// +// These types are owned here on EVERY platform, with no chip-SDK types in their +// public surface. Platform trackers produce them: +// - esp32_ble_tracker adapts ESP-IDF scan results into ESPBTDevice and +// re-exports these names (esp32 only) for backward compatibility; +// - the LibreTiny trackers (bk72xx / ln882h) feed from_scan_result() directly. + +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#include +#include +#include +#include +#include + +#if defined(__cpp_lib_span) +#include +#endif + +#ifdef USE_ESP32 +// Historical esp32_ble API surface (below, under the same define) uses the +// ESP-IDF UUID/address/scan-result types directly; never referenced off-esp32. +#include "esphome/components/esp32_ble/ble_scan_result.h" +#include +#endif + +namespace esphome::ble_device_base { + +using adv_data_t = std::vector; + +// Bluetooth Core address types (spec values; matches ESP-IDF's esp_ble_addr_type_t). +static constexpr uint8_t BLE_ADDR_TYPE_PUBLIC = 0; +static constexpr uint8_t BLE_ADDR_TYPE_RANDOM = 1; +static constexpr uint8_t BLE_ADDR_TYPE_RPA_PUBLIC = 2; +static constexpr uint8_t BLE_ADDR_TYPE_RPA_RANDOM = 3; + +/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0" +static constexpr size_t UUID_STR_LEN = 37; + +// --------------------------------------------------------------------------- +// ESPBTUUID — 16/32/128-bit Bluetooth UUID value type. +// API-compatible with the historical esp32_ble::ESPBTUUID; the esp_bt_uuid_t +// conversions live in esp32_ble (esp32-only adapters), not here. +// --------------------------------------------------------------------------- + +class ESPBTUUID { + public: + ESPBTUUID() = default; + + static ESPBTUUID from_uint16(uint16_t uuid); + static ESPBTUUID from_uint32(uint32_t uuid); + /// Construct from raw 16-byte little-endian UUID. + static ESPBTUUID from_raw(const uint8_t *data); + /// Construct from raw 16-byte big-endian UUID (reversed on store). + static ESPBTUUID from_raw_reversed(const uint8_t *data); + /// Parse from text: 4 hex chars (16-bit), 8 hex chars (32-bit), 16 raw bytes, + /// or the 36-char dashed UUID form. Same semantics as esp32_ble historically. + static ESPBTUUID from_raw(const char *data, size_t length); + static ESPBTUUID from_raw(const char *data) { return from_raw(data, strlen(data)); } + static ESPBTUUID from_raw(const std::string &data) { return from_raw(data.c_str(), data.length()); } + static ESPBTUUID from_raw(std::initializer_list data) { + return from_raw(reinterpret_cast(data.begin()), data.size()); + } + +#ifdef USE_ESP32 + /// Source compatibility with the historical esp32_ble API (esp32 builds only). + static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); + esp_bt_uuid_t get_uuid() const; +#endif + + /// Expand to the 128-bit Bluetooth Base UUID form. + ESPBTUUID as_128bit() const; + + /// True if the UUID value contains the adjacent byte pair (data1, data2). + bool contains(uint8_t data1, uint8_t data2) const; + + bool operator==(const ESPBTUUID &other) const; + bool operator!=(const ESPBTUUID &other) const { return !(*this == other); } + + /// Write "0xABCD" / "0xABCDEF01" / the dashed 128-bit form into buf + /// (>= UUID_STR_LEN bytes) and return buf. + const char *to_str(char *buf) const; +#if defined(__cpp_lib_span) + const char *to_str(std::span output) const { return this->to_str(output.data()); } +#endif + enum class Type : uint8_t { UUID16, UUID32, UUID128 }; + Type type() const { return this->type_; } + uint16_t uuid16() const { return this->uuid_.uuid16; } + uint32_t uuid32() const { return this->uuid_.uuid32; } + const uint8_t *uuid128() const { return this->uuid_.uuid128; } + + protected: + // Expand to the 128-bit Bluetooth Base UUID byte form (out is 16 bytes, little-endian). + void to_128bit_(uint8_t out[16]) const; + + Type type_{Type::UUID16}; + union { + uint16_t uuid16; + uint32_t uuid32; + uint8_t uuid128[16]; + } uuid_{}; +}; + +// --------------------------------------------------------------------------- +// ServiceData — UUID-tagged advertisement payload (0x16 / 0xFF AD types) +// --------------------------------------------------------------------------- + +struct ServiceData { + ESPBTUUID uuid; + adv_data_t data; +}; + +// --------------------------------------------------------------------------- +// ESPBLEiBeacon +// --------------------------------------------------------------------------- + +class ESPBLEiBeacon { + public: + ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } + explicit ESPBLEiBeacon(const uint8_t *data); + static optional from_manufacturer_data(const ServiceData &data); + + uint16_t get_major() const { return byteswap(this->beacon_data_.major); } + uint16_t get_minor() const { return byteswap(this->beacon_data_.minor); } + int8_t get_signal_power() const { return this->beacon_data_.signal_power; } + ESPBTUUID get_uuid() const { return ESPBTUUID::from_raw_reversed(this->beacon_data_.proximity_uuid); } + + protected: + struct PACKED BeaconData { + uint8_t sub_type; + uint8_t length; + uint8_t proximity_uuid[16]; + uint16_t major; + uint16_t minor; + int8_t signal_power; + } beacon_data_; +}; + +/// Pack a controller-order (LSB-first) MAC into the uint64 the API speaks. +/// +/// The result is the printable-order value esp32 has always sent +/// (esp32_ble::ble_addr_to_uint64), so both proxy paths agree on the wire. +/// This takes the raw controller order delivered by BLEHub's raw-advertisement +/// callback; ESPBTDevice::address_uint64() is the equivalent for an already +/// parsed device, whose address is stored MSB-first. +inline uint64_t mac_lsb_first_to_uint64(const uint8_t *mac) { + uint64_t addr = 0; + for (int i = 0; i < 6; i++) + addr |= static_cast(mac[i]) << (i * 8); + return addr; +} + +// --------------------------------------------------------------------------- +// ESPBTDevice — parsed BLE advertisement +// --------------------------------------------------------------------------- + +class ESPBTDevice { + public: + /// Populate from a raw scan result delivered by a BLE tracker backend. + /// mac is least-significant octet first (BLE controller convention). + void from_scan_result(const uint8_t *mac, int rssi, uint8_t addr_type, const uint8_t *data, uint16_t data_len); + + // Alias the core constant so the two cannot drift apart. + static constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = esphome::MAC_ADDRESS_PRETTY_BUFFER_SIZE; + + /// Return MAC as "XX:XX:XX:XX:XX:XX" string. + std::string address_str() const; + /// Buffer overload: writes "XX:XX:XX:XX:XX:XX\0" into buf (>= 18 bytes), returns buf. + const char *address_str_to(char *buf) const; +#if defined(__cpp_lib_span) + const char *address_str_to(std::span buf) const { + return this->address_str_to(buf.data()); + } +#endif + /// Return MAC as packed uint64 (byte 0 in LSB — matches esp32's address_uint64). + uint64_t address_uint64() const; + /// Raw MAC bytes in printable (MSB-first) order — matches the historical + /// esp32 layout (ESP-IDF bda order). + const uint8_t *address() const { return address_; } +#ifdef USE_ESP32 + // Historical esp32 signature: consumers assign the result to esp_ble_addr_type_t. + esp_ble_addr_type_t get_address_type() const { return static_cast(this->address_type_); } + /// Historical esp32 ingest (esp32 builds only): parse an ESP-IDF scan result. + void parse_scan_rst(const esp32_ble::BLEScanResult &scan_result); + // Exposed through a function for use in lambdas + const esp32_ble::BLEScanResult &get_scan_result() const { return *scan_result_; } +#else + uint8_t get_address_type() const { return this->address_type_; } +#endif + /// Human-readable address type ("PUBLIC", "RANDOM", "RPA_PUBLIC", "RPA_RANDOM" or + /// "UNKNOWN"), backed by the shared BLE_ADDR_TYPE_* constants above. + const char *address_type_str() const; + + int get_rssi() const { return rssi_; } + const std::string &get_name() const { return name_; } + + const std::vector &get_service_uuids() const { return service_uuids_; } + const std::vector &get_manufacturer_datas() const { return manufacturer_datas_; } + const std::vector &get_service_datas() const { return service_datas_; } + const std::vector &get_tx_powers() const { return tx_powers_; } + const optional &get_appearance() const { return appearance_; } + const optional &get_ad_flag() const { return ad_flag_; } + + /// Resolve a Resolvable Private Address against a 16-byte IRK (Bluetooth "ah" + /// function, AES-128). Uses the portable software AES shared with the CCM + /// decryptor; compiled only when a sensor configures irk: (request_irk_support). + bool resolve_irk(const uint8_t *irk) const; + + optional get_ibeacon() const { + for (const auto &it : this->manufacturer_datas_) { + auto res = ESPBLEiBeacon::from_manufacturer_data(it); + if (res.has_value()) + return res; + } + return {}; + } + + protected: + void parse_adv_(const uint8_t *payload, uint16_t len); + + uint8_t address_[6]{0}; + uint8_t address_type_{0}; + int rssi_{0}; + std::string name_{}; + std::vector service_uuids_{}; + std::vector manufacturer_datas_{}; + std::vector service_datas_{}; +#ifdef USE_ESP32 + const esp32_ble::BLEScanResult *scan_result_{nullptr}; +#endif + std::vector tx_powers_{}; + optional appearance_{}; + optional ad_flag_{}; +}; + +// --------------------------------------------------------------------------- +// DiscoveredDeviceLog — shared per-scan-period "Found device" DEBUG logger +// --------------------------------------------------------------------------- + +/// Per-scan-period "Found device" DEBUG logger, deduplicated by MAC address. +/// Shared by all tracker backends so the output format and dedup behaviour stay +/// identical by construction (single implementation instead of per-chip copies). +class DiscoveredDeviceLog { + public: + /// Log the device at DEBUG the first time its MAC is seen this scan period. + void log_device(const char *tag, const ESPBTDevice &device); + /// Reset the per-period dedup list (call when a scan period ends). + void clear() { this->already_discovered_.clear(); } + + protected: + std::vector already_discovered_; +}; + +// --------------------------------------------------------------------------- +// ESPBTDeviceListener — base class for BLE consumers (sensors, proxy, triggers) +// --------------------------------------------------------------------------- + +class ESPBTDeviceListener { + public: + virtual ~ESPBTDeviceListener() = default; + /// Called at the end of each scan duration period. + virtual void on_scan_end() {} + virtual bool parse_device(const ESPBTDevice &device) = 0; +}; + +} // namespace esphome::ble_device_base diff --git a/esphome/components/ble_device_base/ble_hub.h b/esphome/components/ble_device_base/ble_hub.h new file mode 100644 index 0000000000..c02b491237 --- /dev/null +++ b/esphome/components/ble_device_base/ble_hub.h @@ -0,0 +1,84 @@ +// ble_hub.h +// +// BLEHub — the platform-neutral BLE tracker contract. +// +// Every BLE tracker component (esp32_ble_tracker, bk72xx_ble_tracker, +// ln882h_ble_tracker, future chips) implements this interface; every BLE +// consumer (sensor components, bluetooth_proxy) binds to it — in YAML via +// `cv.use_id(BLEHub)`, which resolves whichever tracker the config declares. +// Adding a new BLE chip therefore requires only a new tracker component that +// implements BLEHub: no consumer, registry, or base changes. +// +// Chip differences are expressed as data (HubCapabilities), never as +// platform conditionals in consumers. + +#pragma once + +#include "ble_device.h" + +#include + +namespace esphome::ble_device_base { + +/// One raw advertisement as delivered by the controller — a borrowed view, +/// valid only for the duration of the invoke() callback. +struct RawAdvertisement { + /// Least-significant octet first (BLE controller convention). + const uint8_t *mac; + const uint8_t *data; + uint16_t data_len; + int8_t rssi; // signed dBm + uint8_t addr_type; +}; + +/// Subscriber slot for the raw-advertisement stream (the bluetooth_proxy +/// path). The hub delivers on the ESPHome main loop. Same shape as +/// logger.h's LogCallback: an instance pointer plus a plain function +/// pointer — no virtuals, no std::function. +/// +/// Usage: +/// hub->set_raw_advertisement_callback({this, [](void *self, const RawAdvertisement &adv) { +/// static_cast(self)->on_raw_advertisement(adv); +/// }}); +struct RawAdvertisementCallback { + void *instance{nullptr}; + void (*fn)(void *instance, const RawAdvertisement &adv){nullptr}; + /// A default-constructed slot is "no subscriber"; hubs must guard on this. + bool is_set() const { return this->fn != nullptr; } + void invoke(const RawAdvertisement &adv) const { this->fn(this->instance, adv); } +}; + +/// What a tracker's controller/SDK can do — consumers branch on data, not #ifdefs. +struct HubCapabilities { + /// Controller can send scan requests (active scanning). + bool active_scan; + /// Controller (or tracker) delivers advertisement + scan response as one merged + /// frame. When false, consumers relying on scan-response fields (e.g. names) + /// may only see them where the receiver merges per address (Home Assistant does). + bool merges_scan_response; + /// GATT client connections are available (today: esp32 only, but a chip SDK + /// gaining GATT support only has to flip this bit). + bool gatt; +}; + +class BLEHub { + public: + virtual ~BLEHub() = default; + + /// Register a parsed-advertisement consumer (BLE sensors, automation triggers). + virtual void register_listener(ESPBTDeviceListener *listener) = 0; + + /// Wire the raw-advertisement stream (bluetooth_proxy). One consumer at a time. + virtual void set_raw_advertisement_callback(RawAdvertisementCallback callback) = 0; + + virtual HubCapabilities get_capabilities() const = 0; + + /// Adapter MAC in printable (MSB-first) order, out[0] = MSB. + virtual void get_adapter_mac(uint8_t out[6]) = 0; + + virtual bool scan_running() = 0; + /// True when the current/configured scan mode is active (scan requests sent). + virtual bool scan_active() = 0; +}; + +} // namespace esphome::ble_device_base diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index c70ee637ef..106171d38f 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -5,6 +5,8 @@ #include #include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/text_sensor/text_sensor.h" @@ -18,22 +20,10 @@ class BLEScanner final : public text_sensor::TextSensor, public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - // Escape special characters in the device name for valid JSON - const char *name = device.get_name().c_str(); + // Escape special characters in the device name for valid JSON. Control characters stay in the \u00XX form this + // sensor has always published. char escaped_name[128]; - size_t pos = 0; - for (; *name != '\0' && pos < sizeof(escaped_name) - 7; name++) { - uint8_t c = static_cast(*name); - if (c == '"' || c == '\\') { - escaped_name[pos++] = '\\'; - escaped_name[pos++] = c; - } else if (c < 0x20) { - pos += snprintf(escaped_name + pos, sizeof(escaped_name) - pos, "\\u%04x", c); - } else { - escaped_name[pos++] = c; - } - } - escaped_name[pos] = '\0'; + json_escape_into_buffer(escaped_name, StringRef(device.get_name()), /*short_control_escapes=*/false); char buf[256]; snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}", diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 7ba9e61e19..9820977a13 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -245,7 +245,9 @@ void BluetoothConnection::send_service_for_discovery_() { service_resp.characteristics.init(total_char_count); uint16_t char_offset = 0; esp_gattc_char_elem_t char_result; - while (true) { // characteristics + // Bound by total_char_count: the vector is sized for it, and a malicious peripheral + // can make enumeration return more entries than the count query reported + while (char_offset < total_char_count) { // characteristics uint16_t char_count = 1; esp_gatt_status_t char_status = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle, @@ -287,7 +289,7 @@ void BluetoothConnection::send_service_for_discovery_() { characteristic_resp.descriptors.init(total_desc_count); uint16_t desc_offset = 0; esp_gattc_descr_elem_t desc_result; - while (true) { // descriptors + while (desc_offset < total_desc_count) { // descriptors uint16_t desc_count = 1; esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr( this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index ca30aab943..f1a30cdfa2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -247,7 +247,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest esp_bd_addr_t address; uint64_to_bd_addr(msg.address, address); esp_err_t ret = esp_ble_remove_bond_device(address); - this->send_device_pairing(msg.address, ret == ESP_OK, ret); + this->send_device_unpairing(msg.address, ret == ESP_OK, ret); break; } case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: { @@ -379,9 +379,17 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn } void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { - if (this->api_connection_ != nullptr) { - ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); - return; + if (this->api_connection_ != nullptr && this->api_connection_ != api_connection) { + // A previous subscriber still holds the slot. This is almost always a stale + // connection from a client that dropped without a clean disconnect and has + // not yet hit the keepalive timeout; rejecting the new subscriber would + // silently starve it of advertisements until it reconnects, so the newest + // subscriber wins instead. + char old_peername[socket::SOCKADDR_STR_LEN]; + char new_peername[socket::SOCKADDR_STR_LEN]; + ESP_LOGW(TAG, "Subscription from %s (%s) replaces %s (%s)", api_connection->get_name(), + api_connection->get_peername_to(new_peername), this->api_connection_->get_name(), + this->api_connection_->get_peername_to(old_peername)); } this->api_connection_ = api_connection; this->parent_->recalculate_advertisement_parser_types(); diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index dd4fde5705..a4245f43e6 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -50,7 +50,9 @@ _BUTTON_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent), - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): validate_device_class, cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), } ) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index cd877fc879..d62c718097 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -1,7 +1,7 @@ import logging import esphome.codegen as cg -from esphome.components import web_server_base +from esphome.components import web_server_base, wifi from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv @@ -13,7 +13,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_RTL87XX, PlatformFramework, ) @@ -54,7 +54,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_LN882X, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_RTL87XX, ] ), @@ -101,11 +101,14 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) cg.add_define("USE_CAPTIVE_PORTAL") + # The portal reads wifi scan results from the web server task; this makes the + # wifi component guard them with a lock on multi-threaded platforms. + wifi.request_wifi_scan_results_lock() if config[CONF_COMPRESSION] == "gzip": cg.add_define("USE_CAPTIVE_PORTAL_GZIP") - if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2040): + if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2): cg.add_library("DNSServer", None) diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index a81edc1900..a25ac8d010 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -7,145 +7,146 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP constexpr uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, - 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, - 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, - 0x7e, 0xa0, 0x24, 0x7b, 0x9d, 0x45, 0x73, 0xb8, 0xb3, 0x60, 0x61, 0x38, 0xef, 0x19, 0xcd, 0x83, 0xc5, 0xdf, 0x2a, - 0xc5, 0xec, 0xbe, 0x03, 0xd4, 0xd8, 0x56, 0x94, 0x85, 0x7b, 0x23, 0x41, 0xe5, 0x8a, 0x80, 0x2c, 0x8b, 0x06, 0x68, - 0x55, 0x16, 0x2d, 0x58, 0x8a, 0x58, 0x43, 0xb5, 0x01, 0x4b, 0xfe, 0xbc, 0xff, 0x39, 0xb8, 0x2d, 0x0b, 0xc1, 0xe5, - 0x1a, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0x24, 0x22, - 0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0x04, 0x6f, 0x79, 0x65, 0x1b, 0x52, - 0xc1, 0x86, 0x33, 0x08, 0x86, 0xc3, 0x35, 0x97, 0xdc, 0x72, 0x2a, 0x02, 0xc3, 0xa8, 0x00, 0x92, 0x5c, 0xf7, 0x06, - 0xf4, 0x70, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0x5c, 0x16, 0x86, 0x69, 0xde, 0x59, 0xe4, 0x5c, 0x25, 0xad, 0xaa, 0x7a, - 0x01, 0x65, 0x14, 0x51, 0x63, 0xc0, 0x9a, 0x88, 0xcb, 0x0a, 0x76, 0xe1, 0x32, 0x66, 0x2c, 0x86, 0xdb, 0xdb, 0xf0, - 0xb3, 0x79, 0x56, 0x29, 0xd6, 0xb7, 0x20, 0x6d, 0x28, 0x14, 0xa3, 0x96, 0x2b, 0x19, 0x1a, 0xa0, 0x9a, 0x35, 0x84, - 0x10, 0xfc, 0xa3, 0xa1, 0x1b, 0xc0, 0xdf, 0x7f, 0xef, 0x9d, 0x99, 0x56, 0x60, 0xff, 0x21, 0xc0, 0x81, 0xe6, 0xa7, - 0xfd, 0x3d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x30, 0x35, 0xbc, 0x02, 0xec, 0x7f, 0x8c, 0x3f, 0x85, 0xc6, 0xee, 0x05, - 0x84, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x82, 0x97, 0x42, 0xb1, 0x35, 0xf6, 0xf3, 0xba, 0x97, 0xcc, 0x29, 0x47, 0xc6, - 0x03, 0xff, 0x20, 0xc0, 0x22, 0x4b, 0xde, 0x51, 0xdb, 0x84, 0x2d, 0xdd, 0x79, 0x23, 0xc0, 0xa5, 0x97, 0xfe, 0xe0, - 0xc1, 0xcb, 0x24, 0x8e, 0xfd, 0xeb, 0xe1, 0x15, 0xfb, 0x51, 0x12, 0xc7, 0xb9, 0x06, 0xdb, 0x6b, 0x89, 0xa8, 0xf7, - 0x50, 0x74, 0xd4, 0x36, 0xa8, 0x22, 0xf8, 0x5d, 0x92, 0xa2, 0xe4, 0x75, 0x98, 0xce, 0x7f, 0x0b, 0x5f, 0xa1, 0x9b, - 0x30, 0x9d, 0xb3, 0x57, 0xc1, 0x1c, 0x25, 0x37, 0xc1, 0x1c, 0xa5, 0x69, 0x38, 0x47, 0xf1, 0x17, 0x8c, 0x6a, 0x2e, - 0x04, 0xc1, 0x52, 0x49, 0xc0, 0xc8, 0x58, 0xad, 0xd6, 0x40, 0x30, 0xeb, 0xb5, 0x06, 0x69, 0xdf, 0x2a, 0xa1, 0x34, - 0x8e, 0xca, 0x67, 0xff, 0x97, 0x42, 0xab, 0xa9, 0x34, 0xb5, 0xd2, 0x2d, 0xc1, 0x43, 0xf6, 0xbd, 0x17, 0x07, 0x7b, - 0x44, 0xee, 0xe5, 0x5f, 0x10, 0x03, 0xa5, 0xf9, 0x8a, 0x4b, 0x82, 0x9d, 0xc6, 0x5b, 0x1c, 0x95, 0x0f, 0xfe, 0xf1, - 0x1c, 0x3d, 0x75, 0xd1, 0x4f, 0xf1, 0x28, 0xef, 0xe3, 0x43, 0x61, 0x36, 0x2b, 0xb4, 0x6b, 0x85, 0x34, 0x04, 0x37, - 0xd6, 0x76, 0x59, 0x14, 0x6d, 0xb7, 0xdb, 0x70, 0x3b, 0x0b, 0x95, 0x5e, 0x45, 0x69, 0x1c, 0xc7, 0x91, 0xd9, 0xac, - 0x30, 0x1a, 0x0b, 0x01, 0xa7, 0x37, 0x18, 0x35, 0xc0, 0x57, 0x8d, 0x1d, 0xe0, 0xf2, 0xc5, 0x01, 0x8e, 0x85, 0xe3, - 0x28, 0x1f, 0x3e, 0x5d, 0x58, 0xe1, 0x17, 0x56, 0xe0, 0x47, 0xea, 0xe1, 0x53, 0x98, 0x57, 0x43, 0x98, 0xaf, 0x68, - 0x8a, 0x52, 0x14, 0x0f, 0x4f, 0x1a, 0x38, 0x78, 0x3a, 0x05, 0x4f, 0x4e, 0xe8, 0xe2, 0xe4, 0xa0, 0x76, 0x11, 0xbc, - 0x3e, 0xcb, 0x26, 0x0e, 0xb3, 0x49, 0xe2, 0x47, 0x84, 0x13, 0xf8, 0x65, 0x71, 0x79, 0x0e, 0xd2, 0x0f, 0x97, 0x0c, - 0xce, 0x5a, 0x93, 0x7c, 0x58, 0xd0, 0x39, 0x9a, 0x4f, 0x98, 0x79, 0xe0, 0xe0, 0xf3, 0x09, 0xcd, 0x37, 0x69, 0x93, - 0xb4, 0xc1, 0x22, 0x98, 0xd3, 0x19, 0x9a, 0x4d, 0x8e, 0xcc, 0xd0, 0x6c, 0x93, 0x36, 0x8b, 0x0f, 0x8b, 0x4b, 0x5c, - 0x30, 0xfb, 0x72, 0x15, 0x95, 0xd8, 0xcf, 0x30, 0x7e, 0x8c, 0x5c, 0x5d, 0x46, 0x1e, 0x7e, 0x56, 0x5c, 0x7a, 0x18, - 0xfb, 0xc7, 0x1a, 0x2c, 0x6b, 0x3c, 0x1c, 0x31, 0x25, 0x6b, 0xbe, 0x0a, 0x3f, 0x1b, 0x25, 0xb1, 0x1f, 0xda, 0x06, - 0xa4, 0x77, 0x12, 0x75, 0x82, 0x30, 0x50, 0xbc, 0xa7, 0x14, 0xeb, 0x1f, 0xce, 0xf5, 0x6f, 0xb9, 0x15, 0x40, 0x6c, - 0xe8, 0x1a, 0xf6, 0xfa, 0x8c, 0x5d, 0xaa, 0x6a, 0xff, 0x8d, 0xd6, 0x68, 0x92, 0xb1, 0x2f, 0xb8, 0x94, 0xa0, 0xef, - 0x61, 0x67, 0x09, 0x7e, 0xf7, 0xe6, 0x2d, 0x7a, 0x53, 0x55, 0x1a, 0x8c, 0xc9, 0x10, 0x7e, 0x69, 0xc3, 0x96, 0xb2, - 0xff, 0x5d, 0x57, 0xf2, 0x95, 0xae, 0x7f, 0xf2, 0x9f, 0x39, 0xfa, 0x1d, 0xec, 0x56, 0xe9, 0xf5, 0xa4, 0xcd, 0xb9, - 0x96, 0xbb, 0x0e, 0xd3, 0xc4, 0x86, 0xb4, 0x33, 0xa1, 0x11, 0x9c, 0x81, 0x97, 0xf8, 0x61, 0x4b, 0xbb, 0xc7, 0xa8, - 0xe4, 0x29, 0x51, 0x0f, 0x45, 0xc5, 0x37, 0x88, 0x09, 0x6a, 0x0c, 0xc1, 0x72, 0x54, 0x85, 0xd1, 0x33, 0x34, 0xfc, - 0x94, 0x64, 0x82, 0xb3, 0x35, 0xc1, 0x7f, 0x31, 0x01, 0x7e, 0xda, 0xff, 0x5a, 0x79, 0x57, 0xc6, 0xf0, 0xea, 0xca, - 0x0f, 0x37, 0x54, 0xf4, 0x80, 0x08, 0xb2, 0x0d, 0x37, 0x8f, 0x0e, 0xe6, 0xdf, 0x14, 0xeb, 0xcc, 0xfa, 0xca, 0x0f, - 0x6b, 0xc5, 0x7a, 0xe3, 0xf9, 0xb8, 0x9c, 0xcc, 0x15, 0x74, 0x1c, 0x90, 0xf8, 0x39, 0x7e, 0xe2, 0x51, 0x20, 0xa0, - 0xb6, 0x67, 0x3e, 0x84, 0x5e, 0x1c, 0x8c, 0x27, 0x43, 0x6d, 0x0c, 0xf7, 0x8f, 0x67, 0x64, 0x61, 0x3a, 0x2a, 0x9f, - 0x0a, 0x3a, 0x07, 0x5d, 0xab, 0xc8, 0xd0, 0x41, 0xae, 0x5f, 0x3a, 0x2a, 0xcf, 0x06, 0x23, 0x7a, 0x02, 0x5f, 0x1c, - 0xb8, 0x27, 0xdd, 0x14, 0x5c, 0x9f, 0x35, 0x16, 0x51, 0xc5, 0x37, 0xe5, 0xc3, 0xd1, 0x7f, 0x8c, 0xe3, 0x5f, 0x3d, - 0xe8, 0xfd, 0x1d, 0x08, 0x60, 0x56, 0x69, 0x0f, 0x3f, 0x97, 0x60, 0xb1, 0x3f, 0x06, 0xfc, 0xcb, 0xfd, 0xbb, 0xdf, - 0x88, 0xf2, 0xb4, 0x7f, 0xfd, 0x2d, 0x6e, 0xb7, 0x0a, 0x3e, 0x6a, 0x10, 0xff, 0x26, 0x57, 0x6e, 0x19, 0x5c, 0x7d, - 0xc2, 0x7e, 0x38, 0xc4, 0xfb, 0xf0, 0xb8, 0x11, 0x5c, 0x3b, 0xbf, 0xdc, 0xb5, 0xe2, 0xda, 0x45, 0x18, 0x2c, 0xe6, - 0xfe, 0xf1, 0xe1, 0xe8, 0x1f, 0xfd, 0xbc, 0x88, 0xc6, 0xb9, 0x5e, 0x16, 0xc3, 0x88, 0x2d, 0x7f, 0x38, 0x2c, 0xd5, - 0x2e, 0x30, 0xfc, 0x0b, 0x97, 0xab, 0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0xae, 0xb9, 0xec, 0x7a, 0x7b, - 0xe8, 0x68, 0x55, 0x39, 0xca, 0xbc, 0xdb, 0xe5, 0xb5, 0x92, 0xd6, 0x71, 0x42, 0x96, 0x40, 0x7b, 0x1c, 0xe9, 0xc3, - 0x44, 0xc9, 0x5e, 0xcf, 0xbf, 0x3b, 0xba, 0x82, 0x3b, 0x58, 0xd8, 0xd9, 0x80, 0x0a, 0xbe, 0x92, 0x19, 0x03, 0x69, - 0x41, 0x8f, 0x42, 0x35, 0x6d, 0xb9, 0xd8, 0x67, 0x86, 0x4a, 0x13, 0x18, 0xd0, 0xbc, 0x3e, 0x2e, 0x7b, 0x6b, 0x95, - 0x3c, 0x2c, 0x95, 0xae, 0x40, 0x67, 0x71, 0x3e, 0x02, 0x81, 0xa6, 0x15, 0xef, 0x4d, 0x16, 0xce, 0x34, 0xb4, 0xf9, - 0x92, 0xb2, 0xf5, 0x4a, 0xab, 0x5e, 0x56, 0x01, 0x73, 0x93, 0x36, 0x7b, 0x9e, 0xd4, 0x74, 0x06, 0x2c, 0x9f, 0x4e, - 0x75, 0x5d, 0xe7, 0x82, 0x4b, 0x08, 0xc6, 0x59, 0x96, 0xa5, 0xe1, 0x8d, 0x13, 0xbb, 0x70, 0x33, 0x4c, 0x1d, 0x62, - 0xf4, 0x31, 0x89, 0xe3, 0xef, 0xf2, 0x53, 0x38, 0x71, 0xce, 0x7a, 0x6d, 0x94, 0xce, 0x3a, 0xc5, 0x9d, 0x9b, 0xc7, - 0x96, 0x72, 0x79, 0xe9, 0xbd, 0x2b, 0x93, 0x7c, 0x5a, 0x3f, 0x19, 0x97, 0x83, 0x99, 0x61, 0x09, 0xe5, 0x2d, 0x97, - 0xe3, 0x0e, 0xcd, 0xd2, 0x45, 0xdc, 0xed, 0x8e, 0xe1, 0x54, 0x20, 0x87, 0x13, 0x77, 0x2d, 0x60, 0x97, 0x7f, 0xee, - 0x8d, 0xe5, 0xf5, 0x3e, 0x98, 0x76, 0x70, 0x66, 0x3a, 0xca, 0x20, 0x58, 0x82, 0xdd, 0x02, 0xc8, 0x7c, 0xb0, 0x11, - 0x70, 0x0b, 0xad, 0x99, 0xf2, 0x74, 0x56, 0x33, 0x14, 0xe8, 0xd7, 0xba, 0xfe, 0x1b, 0xb7, 0xab, 0xc5, 0x43, 0x4b, - 0xf5, 0x8a, 0xcb, 0x60, 0xa9, 0xac, 0x55, 0x6d, 0x16, 0xbc, 0xea, 0x76, 0xf9, 0x84, 0x72, 0xca, 0xb2, 0xc4, 0xb9, - 0x39, 0xec, 0xd6, 0x53, 0xbe, 0x93, 0x6e, 0x87, 0x8c, 0x12, 0xbc, 0x9a, 0xf8, 0x06, 0x16, 0x14, 0x9f, 0xd3, 0x93, - 0xcc, 0xbb, 0x1d, 0x72, 0xb8, 0x53, 0xaa, 0x6f, 0xea, 0x5b, 0x9a, 0xc4, 0x7f, 0xf1, 0x45, 0xaa, 0xba, 0x4e, 0x97, - 0xf5, 0x39, 0x53, 0x6e, 0x4d, 0xba, 0xd6, 0x18, 0x4a, 0xab, 0x88, 0xc6, 0xdb, 0x8c, 0xab, 0x8c, 0xb2, 0x70, 0x19, - 0x2e, 0x8b, 0x26, 0x41, 0xbc, 0x22, 0x2d, 0x65, 0xe5, 0xc5, 0xf8, 0x2a, 0xa2, 0x26, 0x39, 0x91, 0x9a, 0xa4, 0xfc, - 0x6a, 0x18, 0x8d, 0xb4, 0xc1, 0xfb, 0xf2, 0xad, 0x92, 0x12, 0x98, 0xe5, 0x72, 0x85, 0xac, 0x42, 0x53, 0x0a, 0xc2, - 0x30, 0x2c, 0x96, 0xba, 0x7c, 0x2f, 0x80, 0x1a, 0x40, 0x5b, 0xca, 0x6d, 0x58, 0x44, 0x23, 0xff, 0xd8, 0xc7, 0xbc, - 0x22, 0x12, 0x6c, 0x39, 0x35, 0x6c, 0xd1, 0xcc, 0x46, 0x03, 0x77, 0x60, 0x9d, 0x26, 0x67, 0x60, 0x56, 0x16, 0x6e, - 0xe5, 0x22, 0x3a, 0x8c, 0x34, 0x12, 0x6d, 0x79, 0xcd, 0xdd, 0x95, 0xa5, 0x2c, 0x86, 0x22, 0x77, 0x1a, 0x5c, 0x9e, - 0xc7, 0xeb, 0xd5, 0x00, 0x09, 0x90, 0x2b, 0xdb, 0x90, 0x59, 0x8a, 0x3a, 0x41, 0x19, 0x34, 0x4a, 0x54, 0xa0, 0xc9, - 0xdd, 0xdd, 0xaf, 0x7f, 0x2f, 0x9d, 0x33, 0x8f, 0x72, 0x9d, 0x59, 0x8f, 0x62, 0x0e, 0x98, 0xa4, 0x16, 0x37, 0xe3, - 0xa5, 0xaa, 0xa3, 0xc6, 0x6c, 0x95, 0xae, 0xbe, 0xd2, 0xf1, 0x7e, 0x42, 0x8e, 0x7a, 0x86, 0xff, 0xd0, 0x2a, 0xe5, - 0x1d, 0xdd, 0x40, 0x11, 0x4d, 0x87, 0x22, 0x72, 0x0e, 0x8f, 0xf4, 0x66, 0xe2, 0x6b, 0x92, 0xf2, 0x8f, 0xfb, 0x37, - 0xe8, 0xcf, 0xae, 0xa2, 0x16, 0xc6, 0xb4, 0x0d, 0x51, 0xb5, 0x60, 0x1b, 0x55, 0x91, 0xf7, 0x7f, 0xdc, 0xdd, 0x9f, - 0x23, 0xec, 0x07, 0x26, 0x04, 0x92, 0x8d, 0xd7, 0xbb, 0x5e, 0x58, 0xde, 0x51, 0x6d, 0x07, 0xb5, 0x81, 0x9b, 0x22, - 0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68, - 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7f, + 0x05, 0x8f, 0x4d, 0x1b, 0xa9, 0xb1, 0xa8, 0x87, 0xd7, 0xde, 0x44, 0x96, 0x54, 0xa4, 0x7b, 0x2d, 0x5a, 0xa0, 0x69, + 0x03, 0xec, 0x36, 0xf7, 0x21, 0x08, 0xb0, 0x34, 0x39, 0xb2, 0x98, 0xa5, 0x48, 0x1d, 0x49, 0xbf, 0x62, 0xf8, 0x7e, + 0xfb, 0x81, 0x92, 0xec, 0xf5, 0x2e, 0x9a, 0x03, 0x0e, 0x86, 0x85, 0x19, 0xce, 0x7b, 0x38, 0x0f, 0x16, 0xff, 0xe0, + 0x9a, 0xb9, 0x7d, 0x07, 0xa8, 0x71, 0xad, 0xac, 0x0a, 0xff, 0x45, 0x92, 0xaa, 0x55, 0x09, 0xaa, 0x2a, 0x1a, 0xa0, + 0xbc, 0x2a, 0x5a, 0x70, 0x14, 0xb1, 0x86, 0x1a, 0x0b, 0xae, 0xfc, 0xeb, 0xee, 0x97, 0xe8, 0x75, 0x55, 0x48, 0xa1, + 0x1e, 0x90, 0x01, 0x59, 0x0a, 0xa6, 0x15, 0x6a, 0x0c, 0xd4, 0x25, 0xa7, 0x8e, 0xe6, 0xa2, 0xa5, 0x2b, 0x18, 0x45, + 0x14, 0x6d, 0xa1, 0xdc, 0x08, 0xd8, 0x76, 0xda, 0x38, 0xc4, 0xb4, 0x72, 0xa0, 0x5c, 0x89, 0xb7, 0x82, 0xbb, 0xa6, + 0xe4, 0xb0, 0x11, 0x0c, 0xa2, 0x1e, 0x99, 0x08, 0x25, 0x9c, 0xa0, 0x32, 0xb2, 0x8c, 0x4a, 0x28, 0xd3, 0xc9, 0xda, + 0x82, 0xe9, 0x11, 0xba, 0x94, 0x50, 0x2a, 0x8d, 0xab, 0xc2, 0x32, 0x23, 0x3a, 0x87, 0xbc, 0xab, 0x65, 0xab, 0xf9, + 0x5a, 0x42, 0x15, 0xc7, 0xd4, 0x5a, 0x70, 0x36, 0x16, 0x8a, 0xc3, 0x8e, 0xd0, 0x6b, 0xb8, 0xa6, 0x2c, 0x4d, 0xc8, + 0x67, 0xfb, 0x0d, 0xd7, 0x6c, 0xdd, 0x82, 0x72, 0x44, 0x6a, 0x46, 0x9d, 0xd0, 0x8a, 0x58, 0xa0, 0x86, 0x35, 0x65, + 0x59, 0xe2, 0x1f, 0x2d, 0xdd, 0x00, 0xfe, 0xfe, 0xfb, 0xe0, 0xcc, 0xb4, 0x02, 0xf7, 0xb3, 0x04, 0x0f, 0xda, 0x9f, + 0xf6, 0x77, 0x74, 0xf5, 0x07, 0x6d, 0x21, 0xc0, 0xd4, 0x0a, 0x0e, 0x38, 0xfc, 0x98, 0x7c, 0x22, 0xd6, 0xed, 0x25, + 0x10, 0x2e, 0x6c, 0x27, 0xe9, 0xbe, 0xc4, 0x4b, 0xa9, 0xd9, 0x03, 0x0e, 0x17, 0xf5, 0x5a, 0x31, 0xaf, 0x1c, 0xe9, + 0x00, 0xc2, 0x83, 0x04, 0x87, 0x5c, 0xf9, 0x8e, 0xba, 0x86, 0xb4, 0x74, 0x17, 0x0c, 0x80, 0x50, 0x41, 0xf6, 0x43, + 0x00, 0xaf, 0xd2, 0x24, 0x09, 0x27, 0xfd, 0x27, 0x09, 0xe3, 0x34, 0x49, 0x16, 0x06, 0xdc, 0xda, 0x28, 0x44, 0x83, + 0xfb, 0xa2, 0xa3, 0xae, 0x41, 0xbc, 0xc4, 0xef, 0xd2, 0x0c, 0xa5, 0x6f, 0x48, 0x36, 0xfb, 0x9d, 0x5c, 0xa3, 0x2b, + 0x92, 0xcd, 0xd8, 0x75, 0x34, 0x43, 0xe9, 0x55, 0x34, 0x43, 0x59, 0x46, 0x66, 0x28, 0xf9, 0x82, 0x51, 0x2d, 0xa4, + 0x2c, 0xb1, 0xd2, 0x0a, 0x30, 0xb2, 0xce, 0xe8, 0x07, 0x28, 0x31, 0x5b, 0x1b, 0x03, 0xca, 0xdd, 0x68, 0xa9, 0x0d, + 0x8e, 0xab, 0x6f, 0xfe, 0x2f, 0x85, 0xce, 0x50, 0x65, 0x6b, 0x6d, 0xda, 0x12, 0xf7, 0xd9, 0x0f, 0x5e, 0x1c, 0xdc, + 0x11, 0xf9, 0x4f, 0x78, 0x41, 0x8c, 0xb4, 0x11, 0x2b, 0xa1, 0x4a, 0xec, 0x35, 0xbe, 0xc6, 0x71, 0x75, 0x1f, 0x1e, + 0xcf, 0xd1, 0x53, 0x1f, 0xfd, 0x18, 0x0f, 0x0f, 0x3e, 0xde, 0x17, 0x76, 0xb3, 0x42, 0xbb, 0x56, 0x2a, 0x5b, 0xe2, + 0xc6, 0xb9, 0x2e, 0x8f, 0xe3, 0xed, 0x76, 0x4b, 0xb6, 0x53, 0xa2, 0xcd, 0x2a, 0xce, 0x92, 0x24, 0x89, 0xed, 0x66, + 0x85, 0xd1, 0x50, 0x08, 0x38, 0xbb, 0xc2, 0xa8, 0x01, 0xb1, 0x6a, 0x5c, 0x0f, 0x57, 0x2f, 0x0e, 0x70, 0x2c, 0x3c, + 0x47, 0x75, 0xff, 0xe9, 0xc2, 0x8a, 0xb9, 0xb0, 0x02, 0x3f, 0xd2, 0x00, 0x9f, 0xc2, 0x7c, 0xd9, 0x87, 0x79, 0x4d, + 0x33, 0x94, 0xa1, 0xa4, 0xff, 0x65, 0x91, 0x87, 0x47, 0x2c, 0x7a, 0x86, 0xa1, 0x0b, 0xcc, 0x43, 0xed, 0x3c, 0x7a, + 0x73, 0x96, 0x4d, 0xfd, 0xc9, 0x26, 0x4d, 0x1e, 0x0f, 0xbc, 0xc0, 0xaf, 0xf3, 0x4b, 0x3c, 0xca, 0x3e, 0x5c, 0x32, + 0x78, 0x6b, 0x4d, 0xfa, 0x61, 0x4e, 0x67, 0x68, 0x36, 0x9e, 0xcc, 0x22, 0x0f, 0x9f, 0x31, 0x34, 0xdb, 0x64, 0x4d, + 0xda, 0x46, 0xf3, 0x68, 0x46, 0xa7, 0x68, 0x3a, 0x3a, 0x32, 0x45, 0xd3, 0x4d, 0xd6, 0xcc, 0x3f, 0xcc, 0x2f, 0xcf, + 0xa2, 0xe9, 0x97, 0x97, 0x71, 0x85, 0xc3, 0x1c, 0xe3, 0xc7, 0xc8, 0xf9, 0x65, 0xe4, 0xe4, 0xb3, 0x16, 0x2a, 0xc0, + 0x38, 0x3c, 0xd6, 0xe0, 0x58, 0x13, 0xe0, 0x98, 0x69, 0x55, 0x8b, 0x15, 0xf9, 0x6c, 0xb5, 0xc2, 0x21, 0x71, 0x0d, + 0xa8, 0xe0, 0x24, 0xea, 0x05, 0xa1, 0xa7, 0x04, 0xcf, 0x29, 0x2e, 0x3c, 0x9c, 0xeb, 0xdf, 0x09, 0x27, 0xa1, 0x74, + 0xc4, 0x37, 0xec, 0xe4, 0x6f, 0xba, 0xe2, 0xa7, 0xfd, 0x6f, 0x3c, 0xc0, 0x2d, 0x65, 0x38, 0x24, 0x42, 0x29, 0x30, + 0x77, 0xb0, 0x73, 0x25, 0x7e, 0xf7, 0xf6, 0x06, 0xbd, 0xe5, 0xdc, 0x80, 0xb5, 0x39, 0xc2, 0xaf, 0x1c, 0x69, 0x29, + 0xfb, 0xba, 0x78, 0x93, 0x3e, 0x95, 0xfe, 0x97, 0xf8, 0x45, 0xa0, 0x3f, 0xc0, 0x6d, 0xb5, 0x79, 0x18, 0xe5, 0xbd, + 0xfd, 0x85, 0x6f, 0x23, 0x56, 0x7e, 0x55, 0x8d, 0x02, 0x87, 0xc3, 0x89, 0xf8, 0x3a, 0x83, 0xb5, 0x82, 0xe3, 0x70, + 0x22, 0xbf, 0xce, 0xd1, 0x59, 0xdf, 0xbc, 0x8e, 0xd0, 0xce, 0x12, 0x2b, 0x05, 0x83, 0x20, 0x0d, 0x49, 0xad, 0xcd, + 0xcf, 0x94, 0x35, 0x8f, 0x09, 0xb2, 0x43, 0x47, 0xab, 0x47, 0x3d, 0xcc, 0x00, 0x75, 0x30, 0xaa, 0x0a, 0x30, 0x17, + 0x1b, 0x1c, 0x2e, 0x14, 0x61, 0x92, 0x5a, 0xeb, 0x47, 0x46, 0xe9, 0x9d, 0xf3, 0xe1, 0xe0, 0x89, 0x1a, 0x22, 0xfd, + 0xf5, 0xee, 0xdd, 0xef, 0xe5, 0x7d, 0x41, 0x87, 0x01, 0x89, 0xbf, 0xc5, 0xa8, 0x67, 0x3e, 0x33, 0x46, 0x12, 0x6a, + 0xe7, 0x2b, 0x5e, 0x07, 0x96, 0x18, 0x6b, 0x45, 0x78, 0x2c, 0x6c, 0x47, 0xd5, 0x73, 0xb6, 0x3e, 0xa6, 0xaa, 0x88, + 0x3d, 0xad, 0x2a, 0x62, 0x5a, 0xbd, 0x38, 0x98, 0xc0, 0xfa, 0xe1, 0xf6, 0x10, 0x1e, 0xef, 0x27, 0x8a, 0xfc, 0x7b, + 0x0d, 0x66, 0x7f, 0x0b, 0x12, 0x98, 0xd3, 0x26, 0xc0, 0xe4, 0x89, 0x60, 0x48, 0x1c, 0xec, 0xdc, 0xcd, 0x38, 0x7f, + 0x2d, 0xf1, 0x87, 0x13, 0x45, 0xb4, 0x62, 0x52, 0xb0, 0x87, 0xf2, 0x1c, 0x71, 0x78, 0x10, 0x64, 0x43, 0xe5, 0x1a, + 0x4e, 0x3c, 0x92, 0xd4, 0x9a, 0xad, 0x6d, 0x10, 0x1e, 0x27, 0x8c, 0xd0, 0xae, 0x03, 0xc5, 0x6f, 0x1a, 0x21, 0x79, + 0xa0, 0xc2, 0x63, 0xf8, 0x78, 0xd3, 0xcf, 0x8c, 0xfb, 0xd5, 0xf0, 0xd1, 0x80, 0xfc, 0x4f, 0xf9, 0xd2, 0x2f, 0x87, + 0x97, 0x9f, 0x70, 0x48, 0xfa, 0xf8, 0xef, 0x1f, 0x37, 0x84, 0x6f, 0xef, 0x57, 0xbb, 0x56, 0x4e, 0x7c, 0xe8, 0xd1, + 0x7c, 0x16, 0x1e, 0xef, 0x8f, 0xe1, 0x31, 0x5c, 0x14, 0xf1, 0x30, 0xe7, 0xab, 0xa2, 0x1f, 0xb9, 0xd5, 0x0f, 0x87, + 0xa5, 0xde, 0x45, 0x56, 0x7c, 0x11, 0x6a, 0x95, 0x0b, 0xd5, 0x80, 0x11, 0xee, 0xc8, 0xc5, 0x66, 0x22, 0x54, 0xb7, + 0x76, 0x87, 0x8e, 0x72, 0xee, 0x29, 0xb3, 0x6e, 0xb7, 0xa8, 0xb5, 0x72, 0x9e, 0x13, 0xf2, 0x14, 0xda, 0xe3, 0x40, + 0xef, 0x27, 0x4c, 0xfe, 0x66, 0xf6, 0xdd, 0x71, 0xa9, 0xf9, 0xfe, 0xe0, 0xd3, 0x10, 0x51, 0x29, 0x56, 0x2a, 0x67, + 0xa0, 0x1c, 0x98, 0x41, 0xa8, 0xa6, 0xad, 0x90, 0xfb, 0xdc, 0x52, 0x65, 0x23, 0x0b, 0x46, 0xd4, 0xc7, 0xe5, 0xda, + 0x39, 0xad, 0x0e, 0x4b, 0x6d, 0x38, 0x98, 0x3c, 0x59, 0x0c, 0x40, 0x64, 0x28, 0x17, 0x6b, 0x9b, 0x93, 0xa9, 0x81, + 0x76, 0xb1, 0xa4, 0xec, 0x61, 0x65, 0xf4, 0x5a, 0xf1, 0x88, 0xf9, 0xc9, 0x9b, 0x7f, 0x9b, 0xd6, 0x74, 0x0a, 0x6c, + 0x31, 0x62, 0x75, 0x5d, 0x2f, 0xa4, 0x50, 0x10, 0x0d, 0xb3, 0x2d, 0xcf, 0xc8, 0x95, 0x17, 0xbb, 0x70, 0x93, 0x64, + 0xfe, 0x60, 0xf0, 0x31, 0x4d, 0x92, 0xef, 0x16, 0xa7, 0x70, 0x92, 0x05, 0x5b, 0x1b, 0xab, 0x4d, 0xde, 0x69, 0xe1, + 0xdd, 0x3c, 0xb6, 0x54, 0xa8, 0x4b, 0xef, 0x7d, 0xd9, 0x2c, 0xc6, 0x75, 0x94, 0x0b, 0xd5, 0x9b, 0xe9, 0x97, 0xd2, + 0xa2, 0x15, 0x6a, 0xd8, 0xa9, 0x79, 0x36, 0x4f, 0xba, 0xdd, 0xf1, 0x54, 0x09, 0x87, 0x13, 0x77, 0x2d, 0x61, 0xb7, + 0xf8, 0xbc, 0xb6, 0x4e, 0xd4, 0xfb, 0x68, 0xdc, 0xc9, 0xb9, 0xed, 0x28, 0x83, 0x68, 0x09, 0x6e, 0x0b, 0xa0, 0x16, + 0xbd, 0x8d, 0x48, 0x38, 0x68, 0xed, 0x98, 0xa7, 0xb3, 0x9a, 0xbe, 0x60, 0x9f, 0xea, 0xfa, 0x5f, 0xdc, 0xbe, 0x8a, + 0x0e, 0x2d, 0x35, 0x2b, 0xa1, 0xa2, 0xa5, 0x76, 0x4e, 0xb7, 0x79, 0x74, 0xdd, 0xed, 0x16, 0xe3, 0x91, 0x57, 0x96, + 0xa7, 0xde, 0xcd, 0x7e, 0xd7, 0x9e, 0xf2, 0x9d, 0x76, 0x3b, 0x64, 0xb5, 0x14, 0x7c, 0xe4, 0xeb, 0x59, 0x50, 0x72, + 0x4e, 0x4f, 0x3a, 0xeb, 0x76, 0xc8, 0x9f, 0x9d, 0x52, 0x7d, 0x55, 0xbf, 0xa6, 0x69, 0xf2, 0x37, 0x37, 0xc2, 0xeb, + 0x3a, 0x5b, 0xd6, 0xe7, 0x4c, 0xf9, 0xb5, 0xe9, 0x57, 0x4b, 0x5f, 0x5a, 0x45, 0x3c, 0xbc, 0x6e, 0x7c, 0x65, 0x54, + 0x85, 0xcf, 0x70, 0x55, 0x34, 0x29, 0x12, 0xbc, 0x6c, 0x29, 0xab, 0x2e, 0x66, 0x5b, 0x11, 0x37, 0xe9, 0x89, 0xd4, + 0xa4, 0xd5, 0x93, 0xb9, 0x35, 0xd0, 0x7a, 0xef, 0xab, 0x1b, 0xad, 0x14, 0x30, 0x27, 0xd4, 0x0a, 0x39, 0x8d, 0xc6, + 0x14, 0x10, 0x42, 0x8a, 0xa5, 0xa9, 0xde, 0x4b, 0xa0, 0x16, 0xd0, 0x96, 0x0a, 0x47, 0x8a, 0x78, 0xe0, 0x1f, 0x3a, + 0x5d, 0xf0, 0x52, 0x81, 0x3b, 0xf7, 0x76, 0x33, 0x1d, 0x0c, 0xdc, 0x82, 0xf3, 0x9a, 0xbc, 0x81, 0x69, 0x55, 0xf8, + 0x15, 0x8c, 0x68, 0xdf, 0xa5, 0x65, 0xbc, 0x15, 0xb5, 0xf0, 0x4f, 0x98, 0xaa, 0xe8, 0x8b, 0xdc, 0x6b, 0xf0, 0x79, + 0x1e, 0x9e, 0x5b, 0x3d, 0x24, 0x41, 0xad, 0x5c, 0x53, 0x4e, 0x33, 0xd4, 0x49, 0xca, 0xa0, 0xd1, 0x92, 0x83, 0x29, + 0x6f, 0x6f, 0x7f, 0xfb, 0x67, 0xe5, 0x9d, 0x79, 0x94, 0xeb, 0xec, 0xc3, 0x20, 0xe6, 0x81, 0x51, 0x6a, 0x7e, 0x35, + 0x3c, 0xb2, 0x3a, 0x6a, 0xed, 0x56, 0x1b, 0xfe, 0x44, 0xc7, 0xfb, 0xf1, 0x70, 0xd0, 0xd3, 0xff, 0xfb, 0x56, 0xa9, + 0x6e, 0xe9, 0x06, 0x8a, 0x78, 0x44, 0x8a, 0xd8, 0x3b, 0x3c, 0xd0, 0x9b, 0x91, 0xaf, 0x49, 0xab, 0x3f, 0xef, 0xde, + 0xa2, 0xbf, 0x3a, 0x4e, 0x1d, 0x0c, 0x69, 0xeb, 0xa3, 0x6a, 0xc1, 0x35, 0x9a, 0x97, 0xef, 0xff, 0xbc, 0xbd, 0x3b, + 0x47, 0xb8, 0xee, 0x99, 0x10, 0x28, 0x36, 0x3c, 0xf7, 0xd6, 0xd2, 0x89, 0x8e, 0x1a, 0xd7, 0xab, 0x8d, 0xfc, 0x14, + 0x39, 0xc5, 0xd0, 0xd3, 0x6b, 0x21, 0x61, 0x08, 0x63, 0x10, 0xac, 0xd0, 0xc9, 0xab, 0x93, 0xb5, 0x67, 0x7e, 0xc5, + 0xc3, 0x6d, 0xc7, 0xc3, 0xd5, 0xc7, 0xfd, 0xcb, 0xf7, 0xbf, 0x81, 0xdb, 0x13, 0xb5, 0x09, 0x0b, 0x00, 0x00}; #else // Brotli (default, smaller) constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, - 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, - 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, - 0xa5, 0x89, 0x25, 0xb3, 0xda, 0x2c, 0xa2, 0x32, 0x9c, 0x57, 0x07, 0x56, 0xbc, 0x34, 0x13, 0xff, 0x5c, 0x0a, 0xa1, - 0x67, 0x82, 0xb8, 0x6b, 0x4c, 0x76, 0x31, 0x6c, 0xe3, 0x40, 0x46, 0xea, 0xb0, 0xd4, 0xf4, 0x3b, 0x02, 0x65, 0x18, - 0xa4, 0xaf, 0xac, 0x6d, 0x55, 0xd6, 0xbe, 0x59, 0x66, 0x7a, 0x7c, 0x60, 0xb2, 0x83, 0x33, 0x23, 0xc9, 0x79, 0x82, - 0x47, 0xb4, 0x28, 0xf4, 0x24, 0xb5, 0x23, 0x5a, 0x44, 0xe1, 0xc3, 0x27, 0x04, 0xe8, 0x0c, 0xdd, 0xb4, 0xd0, 0x8c, - 0xfb, 0x10, 0x39, 0x93, 0x04, 0x2a, 0x66, 0x18, 0x4b, 0x74, 0xca, 0x31, 0x7f, 0xb2, 0xe5, 0x45, 0xc1, 0xdd, 0x72, - 0x49, 0xff, 0x0e, 0xb3, 0xf0, 0x93, 0x18, 0xab, 0x68, 0xad, 0xe1, 0x9d, 0xe4, 0x29, 0xc0, 0xe3, 0x63, 0x54, 0x61, - 0x1b, 0x45, 0xb9, 0x6c, 0x23, 0x0f, 0x99, 0x7f, 0x8e, 0x69, 0xaa, 0xc1, 0xb8, 0x4e, 0x42, 0x9c, 0xc5, 0x6e, 0x69, - 0x40, 0x0e, 0x4f, 0x97, 0xd3, 0x23, 0x18, 0xf5, 0xc8, 0x75, 0x73, 0xb5, 0xbd, 0x46, 0x8a, 0x97, 0x7d, 0x83, 0xe4, - 0x29, 0x72, 0x73, 0xc1, 0x39, 0x8e, 0x7e, 0x84, 0x39, 0x66, 0x57, 0xc6, 0x85, 0x19, 0x8b, 0xf2, 0x4d, 0xd9, 0xfe, - 0x75, 0xa9, 0xe1, 0x2b, 0x21, 0x81, 0x58, 0x51, 0x99, 0xbc, 0xa4, 0x0b, 0x10, 0x6f, 0x86, 0x17, 0x0b, 0x92, 0x00, - 0x11, 0x6f, 0x3b, 0xa4, 0xa4, 0x11, 0x7e, 0x0b, 0x97, 0x85, 0x23, 0x0c, 0x01, 0x6f, 0x2a, 0x18, 0xc6, 0xbe, 0x3d, - 0x77, 0x1a, 0xe6, 0x00, 0x5c, 0x1a, 0x14, 0x47, 0xc6, 0xcc, 0xcc, 0x52, 0xbe, 0x04, 0x19, 0x31, 0x05, 0x46, 0xa0, - 0xc3, 0x69, 0x0c, 0x60, 0xb7, 0x14, 0x57, 0xa0, 0x92, 0xbf, 0xb7, 0x0c, 0xd8, 0x3a, 0x79, 0x09, 0x99, 0xc9, 0x71, - 0x88, 0x01, 0x8b, 0xa5, 0x61, 0x0a, 0xb5, 0xe8, 0xc7, 0x71, 0xe7, 0x70, 0x79, 0xb6, 0xe4, 0x01, 0xfc, 0x1a, 0x4a, - 0x7b, 0x60, 0x6e, 0xef, 0x95, 0x62, 0x59, 0x28, 0xb5, 0x25, 0x56, 0x15, 0xe7, 0xca, 0xad, 0x32, 0xe6, 0xf7, 0x01, - 0x31, 0x34, 0x87, 0x93, 0x0b, 0x9b, 0x9d, 0x26, 0xff, 0xe5, 0x92, 0xad, 0x6f, 0xb8, 0x3b, 0x16, 0xc1, 0xa0, 0x5a, - 0x4f, 0x52, 0x0b, 0x2b, 0xc1, 0xa7, 0x95, 0x7b, 0x24, 0x51, 0xd3, 0xb3, 0x23, 0x62, 0x0b, 0xcc, 0xa0, 0x58, 0xa7, - 0x64, 0x45, 0x2f, 0x0b, 0xdd, 0x1d, 0x97, 0x82, 0x1f, 0xcc, 0x64, 0xdb, 0xd3, 0xf4, 0xb0, 0x8b, 0xc8, 0xcf, 0x15, - 0x81, 0x8b, 0xa1, 0x9d, 0xf8, 0xfc, 0xec, 0x49, 0x40, 0x12, 0x01, 0x09, 0x51, 0xf3, 0x73, 0x18, 0x24, 0x97, 0x55, - 0x85, 0x6a, 0x92, 0x1a, 0xf5, 0x5a, 0x05, 0x54, 0x1f, 0x27, 0x0a, 0xa8, 0xa1, 0x94, 0x58, 0x78, 0x7d, 0x87, 0xa8, - 0xdb, 0x13, 0x66, 0x20, 0x5e, 0x43, 0x18, 0x7a, 0xbb, 0x16, 0x16, 0x07, 0xc8, 0xab, 0x10, 0xe2, 0x50, 0xb9, 0xb1, - 0xd8, 0x21, 0xc8, 0x4a, 0x2e, 0x99, 0x0e, 0x23, 0x52, 0xc6, 0xcb, 0x29, 0x84, 0x91, 0x03, 0xb1, 0xe2, 0x4c, 0x1d, - 0x22, 0xd3, 0xc8, 0x79, 0x00, 0x8b, 0x8b, 0x88, 0x1e, 0x29, 0xd3, 0xae, 0x10, 0x15, 0x22, 0x6d, 0xb0, 0x87, 0x6f, - 0x27, 0x2e, 0x7c, 0xc2, 0x7a, 0x61, 0xbd, 0x22, 0xe5, 0x5f, 0xdd, 0x7b, 0x00, 0x04, 0xf2, 0x7d, 0x5a, 0x03, 0x38, - 0x1f, 0x69, 0x6d, 0x0b, 0xfb, 0xec, 0x45, 0xfe, 0x8b, 0x7f, 0xec, 0x7b, 0xad, 0xc2, 0x33, 0xf1, 0x9e, 0x9c, 0x71, - 0xd9, 0xe8, 0x5e, 0x8f, 0xd4, 0xee, 0x87, 0x45, 0x6c, 0xe2, 0x12, 0xf8, 0xb8, 0xc5, 0xee, 0x43, 0xa6, 0x37, 0x91, - 0xb5, 0x2c, 0x2f, 0xe9, 0xe8, 0x24, 0xd0, 0x45, 0xc1, 0x0c, 0x7c, 0xf0, 0xb2, 0xb5, 0x2d, 0x10, 0x36, 0x7e, 0x18, - 0x7c, 0x79, 0x82, 0x69, 0x3d, 0x35, 0xca, 0x52, 0xee, 0xc9, 0xb5, 0x65, 0xa4, 0xa1, 0xfd, 0x70, 0x7e, 0xe0, 0x7d, - 0x67, 0xf9, 0xa1, 0x71, 0xd2, 0x08, 0x74, 0x33, 0x5f, 0x69, 0xa4, 0x59, 0x03, 0xfd, 0xf8, 0xf0, 0x70, 0x1a, 0x50, - 0x43, 0xfb, 0x61, 0xf0, 0x38, 0x18, 0x88, 0x85, 0x36, 0x23, 0x06, 0x4f, 0x02, 0xbb, 0x78, 0x1a, 0xaa, 0xd2, 0x02, - 0x5e, 0xa0, 0x74, 0x30, 0xc8, 0x7a, 0x66, 0xab, 0xd9, 0x43, 0x99, 0x45, 0xb7, 0x0c, 0x5c, 0xec, 0xc8, 0x03, 0x0e, - 0x0b, 0xca, 0x4a, 0x22, 0x48, 0xfb, 0xb7, 0x3d, 0x82, 0x07, 0x8d, 0x1b, 0x21, 0x87, 0x4d, 0x57, 0xa4, 0x5b, 0xd4, - 0xe3, 0x88, 0x02, 0xc4, 0x81, 0xf9, 0x47, 0xe4, 0xbf, 0x3e, 0x39, 0xbb, 0x4f, 0x7e, 0x91, 0x63, 0x98, 0x97, 0xe4, - 0x52, 0x01, 0x58, 0xba, 0x32, 0xbf, 0xae, 0xff, 0x45, 0xa1, 0xbc, 0x9b, 0xa4, 0x09, 0x0e, 0x79, 0xc0, 0x41, 0x86, - 0x52, 0x88, 0x55, 0x39, 0x9d, 0xb6, 0xed, 0x35, 0x68, 0x29, 0xfa, 0xe6, 0x6c, 0x3d, 0x0a, 0xcd, 0x6a, 0x28, 0xfd, - 0x65, 0x24, 0xce, 0x38, 0x98, 0x01, 0xd9, 0x3f, 0x1b, 0x4c, 0xc4, 0x5c, 0x1d, 0xaa, 0x21, 0x78, 0x67, 0xaf, 0x55, - 0x72, 0x34, 0xf8, 0x1b, 0x03, 0x21, 0x27, 0x08, 0xbd, 0x59, 0x60, 0x48, 0x0d, 0xe2, 0x56, 0x9b, 0x30, 0x92, 0x8f, - 0x67, 0x8a, 0x7f, 0x20, 0xbd, 0x2d, 0xfd, 0xc5, 0xb0, 0xa6, 0xaa, 0x77, 0x75, 0x26, 0x33, 0x2f, 0x20, 0x2a, 0xab, - 0x5c, 0xd1, 0x3b, 0xda, 0xb2, 0x4c, 0xa4, 0x86, 0x25, 0x8d, 0x49, 0x05, 0xaf, 0x7a, 0xa8, 0xd4, 0x9c, 0x0d, 0xd3, - 0x38, 0xa6, 0x5c, 0x29, 0x6b, 0x16, 0x27, 0x07, 0xf1, 0xbe, 0xe2, 0x24, 0xc1, 0x8d, 0x25, 0x76, 0xbc, 0xf6, 0x0d, - 0xc2, 0x94, 0x25, 0xb8, 0xf3, 0x07, 0x9a, 0x49, 0xf4, 0x89, 0x82, 0x4d, 0x51, 0xb1, 0x96, 0x61, 0x62, 0x8d, 0xc8, - 0x61, 0x65, 0x0d, 0x14, 0x34, 0x02, 0x65, 0x94, 0xcc, 0x1d, 0x85, 0x00, 0x0f, 0x1a, 0x57, 0x68, 0x15, 0xcf, 0xa4, - 0xa2, 0x7d, 0x6d, 0x53, 0x60, 0xce, 0x5c, 0x61, 0x82, 0x17, 0x32, 0xc1, 0x87, 0x02, 0x0c, 0x91, 0x85, 0x57, 0x51, - 0xbe, 0xb2, 0x38, 0x9f, 0x3d, 0x2a, 0x52, 0x5a, 0xad, 0xba, 0x46, 0x9e, 0x3c, 0x8a, 0xa0, 0x46, 0x15, 0xf4, 0x59, - 0x74, 0x5f, 0x2a, 0xae, 0x96, 0x56, 0xf0, 0x54, 0x39, 0xaf, 0xac, 0x2a, 0xb9, 0xad, 0x32, 0x50, 0xc9, 0xc1, 0xee, - 0xd2, 0x0d, 0x34, 0xaa, 0x98, 0x4d, 0x6d, 0x3d, 0xc6, 0xb9, 0x5b, 0x00, 0x5f, 0xea, 0xda, 0x16, 0xa6, 0x08, 0x43, - 0x58, 0x4d, 0x8d, 0x07, 0x55, 0x62, 0x81, 0x44, 0xcc, 0x31, 0x04, 0x4b, 0x4c, 0x8b, 0x3e, 0xff, 0xd8, 0xf6, 0x65, - 0x19, 0xa1, 0x94, 0x62, 0x65, 0x0a, 0xdd, 0x60, 0x38, 0xd3, 0xbe, 0x0d, 0xa3, 0x99, 0xd5, 0x37, 0x68, 0xa1, 0x71, - 0xa3, 0x41, 0xe7, 0xbe, 0x9d, 0x72, 0x84, 0x75, 0xb6, 0x8d, 0x98, 0xd6, 0xb8, 0x2d, 0x43, 0x85, 0x5d, 0xf9, 0xca, - 0xc3, 0x96, 0xa5, 0xa6, 0xe7, 0x50, 0x88, 0x6b, 0x84, 0x58, 0x44, 0x45, 0x20, 0xdf, 0x1e, 0x5a, 0xc9, 0xce, 0x42, - 0x2a, 0x1f, 0x3e, 0x3c, 0x7b, 0x68, 0x3c, 0x34, 0x8b, 0x36, 0xba, 0x1f, 0xce, 0x0f, 0xa0, 0x60, 0x37, 0x5f, 0x1a, - 0x03, 0x2b, 0x86, 0x29, 0x45, 0x7b, 0xb4, 0xb7, 0x06, 0x68, 0x17, 0x7e, 0x13, 0x76, 0x91, 0x4d, 0x27, 0xee, 0xbc, - 0x7e, 0x80, 0xc2, 0x66, 0xac, 0xc6, 0xbf, 0xeb, 0x7f, 0xd7, 0x84, 0x79, 0xf3, 0xf1, 0xde, 0xec, 0xa6, 0x93, 0xa8, - 0x13, 0x3b, 0x4a, 0x81, 0xfa, 0x11, 0x1e, 0x4a, 0xd2, 0x50, 0x2a, 0xea, 0x9a, 0xc2, 0x37, 0x08, 0xed, 0x01, 0xf5, - 0xa2, 0xd5, 0x32, 0x29, 0x49, 0xc4, 0x1a, 0x11, 0xc0, 0xda, 0x24, 0x28, 0x84, 0x38, 0x60, 0x80, 0xcf, 0xd0, 0x45, - 0x83, 0xa7, 0xca, 0x52, 0x5c, 0xac, 0x23, 0x01}; + 0x1b, 0x08, 0x0b, 0x00, 0xe4, 0x7f, 0x9b, 0xad, 0xbb, 0x97, 0x53, 0xde, 0xb7, 0x25, 0x0e, 0x69, 0xd4, 0x69, 0x89, + 0xba, 0xa5, 0x55, 0x22, 0x04, 0x27, 0xeb, 0x00, 0x52, 0xac, 0xbc, 0xec, 0x5f, 0xfb, 0xb5, 0x7a, 0x12, 0x0a, 0xd6, + 0x48, 0x84, 0x4a, 0x48, 0x5a, 0xcb, 0xbd, 0xb7, 0x72, 0x66, 0x4e, 0x62, 0xd8, 0xbd, 0x7f, 0x88, 0x68, 0x86, 0x28, + 0xd6, 0xf1, 0xda, 0x2c, 0xa2, 0x32, 0x5c, 0xdd, 0xab, 0xb2, 0x15, 0xbc, 0x24, 0x13, 0xe6, 0xbd, 0x0c, 0x52, 0x63, + 0x82, 0x88, 0x6d, 0x74, 0x71, 0x51, 0x9c, 0xe2, 0x40, 0x56, 0xea, 0xb0, 0x5c, 0xb7, 0x1d, 0x81, 0x3a, 0x0c, 0xf2, + 0xd7, 0xa8, 0x5c, 0x35, 0x2d, 0x43, 0xb3, 0x42, 0x37, 0x7c, 0x60, 0xd8, 0xc1, 0x95, 0x91, 0x94, 0x3c, 0x29, 0x20, + 0x96, 0x20, 0xf6, 0x24, 0xb7, 0x83, 0x4a, 0x06, 0xf1, 0xc3, 0x2f, 0x88, 0x50, 0x55, 0x35, 0x2d, 0xe8, 0xb6, 0x21, + 0x38, 0x61, 0x8e, 0x44, 0x2a, 0xac, 0x39, 0xcf, 0x74, 0xaa, 0x31, 0x7f, 0x62, 0x32, 0x9b, 0x99, 0x42, 0x0a, 0xf6, + 0x6f, 0xd8, 0x89, 0x3f, 0x49, 0xb1, 0xa2, 0x52, 0x0a, 0x8e, 0xb3, 0x27, 0x0b, 0x07, 0x07, 0x58, 0xb0, 0x8f, 0xaa, + 0xdc, 0x68, 0x12, 0x0f, 0x95, 0xbf, 0xc7, 0x36, 0xd5, 0x60, 0x5a, 0xc7, 0x80, 0xac, 0x52, 0xf7, 0xa7, 0x2d, 0xb6, + 0x64, 0xda, 0xda, 0x11, 0x8d, 0x6a, 0xe5, 0xba, 0xe9, 0xda, 0xdc, 0x62, 0xc3, 0xcb, 0xae, 0xc1, 0xe1, 0x11, 0xb6, + 0x33, 0x29, 0x04, 0x09, 0xfe, 0x09, 0x08, 0xc2, 0x1f, 0x98, 0x16, 0x26, 0x0d, 0xce, 0xd7, 0x75, 0xfb, 0xd7, 0xa5, + 0x82, 0xd7, 0x32, 0x44, 0x72, 0xc1, 0xc2, 0xe4, 0x15, 0xcb, 0x50, 0xfc, 0xd3, 0x58, 0x64, 0x34, 0x41, 0x32, 0xbe, + 0x1f, 0x08, 0x43, 0x16, 0x14, 0xf7, 0x70, 0x2c, 0x1c, 0x61, 0x09, 0x78, 0x73, 0xd1, 0x30, 0xf6, 0xed, 0x85, 0x55, + 0x50, 0x02, 0xf0, 0x68, 0x50, 0x5c, 0x1a, 0xd7, 0x3b, 0x8e, 0xf2, 0x23, 0xc8, 0x88, 0x39, 0xd0, 0x84, 0xf7, 0xa6, + 0xd1, 0xa3, 0xfb, 0xe5, 0x44, 0xc0, 0x4c, 0x2f, 0x6f, 0x19, 0x70, 0x75, 0xf6, 0x1c, 0xb8, 0xce, 0x89, 0x4f, 0x01, + 0x8d, 0xa1, 0x71, 0xf2, 0x97, 0xf8, 0xe7, 0xd3, 0xf1, 0xe1, 0xfa, 0xfc, 0xc8, 0x03, 0x78, 0x14, 0xa0, 0x3d, 0x28, + 0xb7, 0xeb, 0x26, 0x52, 0x59, 0xa8, 0xb5, 0x0d, 0x5c, 0x8a, 0x6b, 0xe5, 0xf6, 0x30, 0xd6, 0xf7, 0x71, 0x31, 0xe8, + 0xbd, 0xc9, 0xfa, 0xf5, 0x71, 0x9d, 0xff, 0xf6, 0x69, 0x62, 0x5f, 0xb5, 0xc7, 0x06, 0x43, 0x54, 0xb5, 0x87, 0xf1, + 0xcc, 0x84, 0xe8, 0xb7, 0x56, 0x38, 0x43, 0x6a, 0xa6, 0x2f, 0x53, 0xd1, 0x89, 0x48, 0x8d, 0x72, 0x75, 0x4a, 0x17, + 0xf6, 0x8d, 0xb2, 0xeb, 0xb1, 0x6b, 0x29, 0x1e, 0xd5, 0x74, 0xc7, 0xb3, 0xf4, 0xf1, 0x04, 0x0d, 0xbf, 0x08, 0x81, + 0x8f, 0xfe, 0x8d, 0xfc, 0xf2, 0x35, 0x92, 0xa0, 0x24, 0x88, 0x12, 0x6a, 0xe6, 0x2f, 0x01, 0x94, 0x5c, 0x4b, 0xf9, + 0x6b, 0x9a, 0xf6, 0x1a, 0x35, 0x11, 0x8a, 0xc6, 0x04, 0x8d, 0x50, 0x64, 0x48, 0x2d, 0x8b, 0xdf, 0xdf, 0xa1, 0xd1, + 0xfd, 0x21, 0xd7, 0x40, 0x96, 0x00, 0xb1, 0x9f, 0x54, 0xc2, 0xe1, 0x00, 0x79, 0x15, 0x80, 0xf8, 0xca, 0x8e, 0xc5, + 0x06, 0x03, 0xdf, 0x72, 0x49, 0xc0, 0x08, 0x27, 0x90, 0xe3, 0x14, 0xc2, 0xac, 0xc3, 0x83, 0xc9, 0xf6, 0x9d, 0x12, + 0xab, 0x46, 0xae, 0x03, 0x74, 0x1d, 0x27, 0xce, 0x91, 0x1d, 0x4e, 0xb4, 0xbe, 0x80, 0xe7, 0x6a, 0x6d, 0x0a, 0x20, + 0x47, 0x6b, 0x00, 0x4e, 0x25, 0x52, 0xe6, 0xf5, 0xe9, 0x43, 0x84, 0xc1, 0x0d, 0x4b, 0x04, 0xb3, 0x91, 0x49, 0xf5, + 0xe9, 0xc4, 0x2e, 0x1b, 0xf9, 0x98, 0xf9, 0xea, 0x9e, 0xb8, 0xf6, 0x53, 0xf8, 0x42, 0xc2, 0x60, 0x5c, 0xa9, 0xd2, + 0xfe, 0x0a, 0xe5, 0xd4, 0x7d, 0x36, 0x76, 0x04, 0x12, 0x38, 0xa1, 0xc4, 0x30, 0xb8, 0xf2, 0x69, 0x86, 0x5b, 0xe7, + 0xe5, 0xa0, 0xc0, 0xfe, 0x91, 0x19, 0x03, 0x1e, 0xa9, 0x93, 0x26, 0x49, 0x58, 0xd5, 0xf6, 0x33, 0x4e, 0x0a, 0x89, + 0x64, 0x1e, 0xb4, 0x7a, 0x5d, 0x0d, 0x12, 0xcf, 0x2b, 0xdd, 0x35, 0x90, 0x55, 0xc3, 0x0e, 0xcd, 0x0e, 0xad, 0x6b, + 0x8f, 0x43, 0xd0, 0xb4, 0x6a, 0xdb, 0x4b, 0xe5, 0xa4, 0x9b, 0x09, 0x23, 0x1a, 0x43, 0xa9, 0xff, 0x61, 0x8b, 0x07, + 0xd6, 0x0f, 0x83, 0x23, 0xbe, 0x8a, 0x66, 0xa2, 0x10, 0x2f, 0x11, 0x01, 0x0d, 0xc6, 0x96, 0xba, 0x87, 0xaa, 0xa8, + 0x44, 0x7c, 0x1e, 0x34, 0xdb, 0xba, 0x41, 0x90, 0xf0, 0x7a, 0xdb, 0x63, 0x60, 0x96, 0x8d, 0x64, 0xcb, 0x2f, 0x28, + 0x96, 0x04, 0xd5, 0xc0, 0x7a, 0xe8, 0xec, 0xd1, 0x61, 0x1b, 0x09, 0x4b, 0x4c, 0x6e, 0x1b, 0x31, 0x98, 0x9c, 0x6d, + 0xdb, 0xd0, 0xec, 0xa4, 0x0f, 0x0a, 0xe0, 0xc8, 0x35, 0xc4, 0x93, 0xdc, 0xee, 0x19, 0x00, 0x80, 0x07, 0xf5, 0xcf, + 0xe0, 0x7f, 0x75, 0xf8, 0x52, 0x3a, 0xfc, 0x0d, 0x84, 0xa5, 0xc1, 0xb2, 0x1c, 0x25, 0x40, 0xc5, 0x8b, 0xb3, 0xdb, + 0x7a, 0x1b, 0x44, 0xff, 0x79, 0x9a, 0x26, 0xc4, 0xe7, 0x9e, 0x78, 0x4c, 0xa5, 0x94, 0xab, 0x78, 0x34, 0x9d, 0xb5, + 0xb7, 0x24, 0x26, 0xe7, 0x9a, 0xf3, 0xe5, 0x2a, 0x35, 0x4b, 0xbe, 0x74, 0xd7, 0x01, 0xbc, 0x71, 0x72, 0x03, 0x5c, + 0x40, 0x24, 0x17, 0x61, 0x5b, 0x7b, 0x19, 0xc2, 0x7f, 0x4e, 0x5b, 0x24, 0xfb, 0x8b, 0xc3, 0x51, 0x00, 0x3d, 0x09, + 0x04, 0x05, 0x72, 0x64, 0xf6, 0xf0, 0x6b, 0x99, 0x38, 0x93, 0x5b, 0xac, 0x0c, 0xff, 0x40, 0x7b, 0x53, 0xba, 0xab, + 0x61, 0xc9, 0xa2, 0xde, 0xd6, 0x2b, 0x0c, 0xbd, 0x85, 0xac, 0x4c, 0x64, 0x8b, 0xe6, 0x69, 0x2b, 0x56, 0x10, 0x1b, + 0x08, 0x59, 0x6c, 0x55, 0x0a, 0xaa, 0x93, 0x85, 0x1d, 0x45, 0xda, 0xc6, 0x39, 0xe6, 0x6a, 0xab, 0x71, 0x73, 0x46, + 0x0f, 0xf7, 0xab, 0x4e, 0x88, 0xae, 0x8c, 0xe0, 0x91, 0xda, 0x35, 0x8c, 0xd3, 0x18, 0x92, 0x3d, 0xab, 0x2f, 0x0d, + 0xd6, 0xc9, 0x06, 0xdb, 0xc2, 0x62, 0xcb, 0x8a, 0x23, 0x5b, 0x28, 0x2e, 0x9b, 0x96, 0xc4, 0xc1, 0x42, 0xa9, 0x8d, + 0x69, 0xe5, 0x8f, 0x89, 0x12, 0x11, 0x9a, 0x56, 0x3b, 0x3c, 0x2b, 0xb4, 0xb2, 0x7b, 0xfd, 0xdb, 0x80, 0x92, 0xb4, + 0xca, 0x44, 0x37, 0x8c, 0x94, 0x2f, 0x4a, 0xb4, 0xc4, 0x28, 0x83, 0x8a, 0x78, 0xcb, 0xd2, 0x5c, 0x5c, 0x35, 0x29, + 0x95, 0x35, 0x2f, 0x99, 0x28, 0x4f, 0x22, 0x18, 0x70, 0x85, 0xee, 0x2c, 0xb9, 0xef, 0x14, 0x57, 0x73, 0x23, 0x45, + 0xae, 0xdc, 0x54, 0x56, 0x55, 0x78, 0x56, 0xad, 0x48, 0x26, 0x27, 0xbf, 0xcb, 0xd7, 0x54, 0xa9, 0xa8, 0xd7, 0xb5, + 0x71, 0x9c, 0xe7, 0x79, 0x89, 0x5c, 0xa9, 0x6a, 0x53, 0xe8, 0xfa, 0x0d, 0x69, 0x36, 0xed, 0xad, 0x33, 0xc9, 0x75, + 0x17, 0xe9, 0x8f, 0x31, 0x58, 0xa6, 0x27, 0xfc, 0x79, 0xc6, 0xb6, 0xd5, 0x65, 0x90, 0x31, 0xc6, 0x9d, 0x29, 0x95, + 0x83, 0xe5, 0x4e, 0xbb, 0xd7, 0xdc, 0x8e, 0xd0, 0x3a, 0x54, 0x27, 0xce, 0xf5, 0xdb, 0xbd, 0x89, 0x3c, 0x61, 0xb3, + 0x71, 0x23, 0xc7, 0x55, 0x9e, 0xea, 0x50, 0xe4, 0x37, 0xae, 0x72, 0x34, 0x66, 0xb9, 0x6e, 0x2c, 0x0a, 0x69, 0x8d, + 0x94, 0x8b, 0x98, 0x08, 0x05, 0x3c, 0x45, 0x45, 0xe1, 0x6c, 0x22, 0xc5, 0x8f, 0x1f, 0x9f, 0x3f, 0xd2, 0x01, 0x12, + 0xec, 0x86, 0x2f, 0x87, 0x8b, 0x47, 0x30, 0xd0, 0x77, 0x77, 0x1a, 0x13, 0x2d, 0xc6, 0x31, 0x65, 0x77, 0xd8, 0x8c, + 0xe7, 0xe0, 0x16, 0xf9, 0x4b, 0xd4, 0xc5, 0xa8, 0x67, 0x79, 0xe7, 0xc3, 0x07, 0x94, 0x46, 0xa3, 0x8c, 0x67, 0xd3, + 0xff, 0x5f, 0x96, 0xfa, 0xed, 0xa7, 0xd3, 0xdd, 0x4f, 0x27, 0x49, 0x27, 0xcf, 0xa4, 0x02, 0xc3, 0x27, 0x3c, 0x96, + 0x64, 0x24, 0x55, 0xc8, 0x36, 0x45, 0x68, 0x90, 0xea, 0x03, 0x0b, 0x46, 0xa7, 0x8d, 0xb4, 0x26, 0x91, 0x07, 0x4c, + 0x80, 0x7b, 0x93, 0xa8, 0x10, 0xcb, 0x5e, 0x8d, 0x42, 0x86, 0x3e, 0x2a, 0x7c, 0x99, 0x79, 0x8e, 0xcb, 0x43, 0x28}; // Backwards compatibility alias #define INDEX_GZ INDEX_BR diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 365e5f64db..8094903008 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -2,6 +2,8 @@ #ifdef USE_CAPTIVE_PORTAL #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/wifi/wifi_component.h" #include "captive_index.h" @@ -24,23 +26,30 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", mac_str, App.get_name().c_str()); #endif - for (auto &scan : wifi::global_wifi_component->get_scan_result()) { - if (scan.get_is_hidden()) - continue; + // An SSID can contain a " or \ that would break the JSON, so escape it before writing it out. An SSID is at most + // 32 bytes (IEEE 802.11), so this is large enough that nothing is ever dropped. Reused for every scan result. + char escaped_ssid[32 * JSON_ESCAPE_MAX_EXPANSION + 1]; + { + // Invariant: only bounded in-memory work under the lock; the network send + // happens later in request->send() + wifi::ScanResultsLock lock(wifi::global_wifi_component); + for (const auto &scan : wifi::global_wifi_component->get_scan_result()) { + if (scan.get_is_hidden()) + continue; - // Assumes no " in ssid, possible unicode isses? + json_escape_into_buffer(escaped_ssid, scan.get_ssid()); #ifdef USE_ESP8266 - stream->print(ESPHOME_F(",{\"ssid\":\"")); - stream->print(scan.get_ssid().c_str()); - stream->print(ESPHOME_F("\",\"rssi\":")); - stream->print(scan.get_rssi()); - stream->print(ESPHOME_F(",\"lock\":")); - stream->print(scan.get_with_auth()); - stream->print(ESPHOME_F("}")); + stream->print(ESPHOME_F(",{\"ssid\":\"")); + stream->print(escaped_ssid); + stream->print(ESPHOME_F("\",\"rssi\":")); + stream->print(scan.get_rssi()); + stream->print(ESPHOME_F(",\"lock\":")); + stream->print(scan.get_with_auth()); + stream->print(ESPHOME_F("}")); #else - stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), - scan.get_with_auth()); + stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), scan.get_with_auth()); #endif + } } stream->print(ESPHOME_F("]}")); request->send(stream); diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 0feb384ac2..01e3ed0cd5 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -21,6 +21,7 @@ MULTI_CONF = True ns = cg.esphome_ns.namespace("cc1101") CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice) +CC1101Listener = ns.class_("CC1101Listener") # Config keys CONF_RX_ATTENUATION = "rx_attenuation" @@ -48,6 +49,15 @@ CONF_FILTER_LENGTH_FSK_MSK = "filter_length_fsk_msk" CONF_FILTER_LENGTH_ASK_OOK = "filter_length_ask_ook" CONF_FREEZE = "freeze" CONF_HYST_LEVEL = "hyst_level" +CONF_FOC_BS_CS_GATE = "foc_bs_cs_gate" +CONF_FOC_LIMIT = "foc_limit" +CONF_FOC_PRE_K = "foc_pre_k" +CONF_FOC_POST_K = "foc_post_k" +CONF_BS_LIMIT = "bs_limit" +CONF_BS_PRE_KI = "bs_pre_ki" +CONF_BS_PRE_KP = "bs_pre_kp" +CONF_BS_POST_KI = "bs_post_ki" +CONF_BS_POST_KP = "bs_post_kp" # Packet mode config keys CONF_PACKET_MODE = "packet_mode" @@ -161,6 +171,64 @@ HYST_LEVEL = { "High": HystLevel.HYST_LEVEL_HIGH, } +FocLimit = ns.enum("FocLimit", True) +FOC_LIMIT = { + "Disabled": FocLimit.FOC_LIMIT_DISABLED, + "BW/8": FocLimit.FOC_LIMIT_BW_8, + "BW/4": FocLimit.FOC_LIMIT_BW_4, + "BW/2": FocLimit.FOC_LIMIT_BW_2, +} + +FocPreK = ns.enum("FocPreK", True) +FOC_PRE_K = { + "K": FocPreK.FOC_PRE_K_K, + "2K": FocPreK.FOC_PRE_K_2K, + "3K": FocPreK.FOC_PRE_K_3K, + "4K": FocPreK.FOC_PRE_K_4K, +} + +FocPostK = ns.enum("FocPostK", True) +FOC_POST_K = { + "Same": FocPostK.FOC_POST_K_SAME, + "K/2": FocPostK.FOC_POST_K_K_2, +} + +BsLimit = ns.enum("BsLimit", True) +BS_LIMIT = { + "Disabled": BsLimit.BS_LIMIT_DISABLED, + "3.125%": BsLimit.BS_LIMIT_3P125_PERCENT, + "6.25%": BsLimit.BS_LIMIT_6P25_PERCENT, + "12.5%": BsLimit.BS_LIMIT_12P5_PERCENT, +} + +BsPreKi = ns.enum("BsPreKi", True) +BS_PRE_KI = { + "KI": BsPreKi.BS_PRE_KI_KI, + "2KI": BsPreKi.BS_PRE_KI_2KI, + "3KI": BsPreKi.BS_PRE_KI_3KI, + "4KI": BsPreKi.BS_PRE_KI_4KI, +} + +BsPreKp = ns.enum("BsPreKp", True) +BS_PRE_KP = { + "KP": BsPreKp.BS_PRE_KP_KP, + "2KP": BsPreKp.BS_PRE_KP_2KP, + "3KP": BsPreKp.BS_PRE_KP_3KP, + "4KP": BsPreKp.BS_PRE_KP_4KP, +} + +BsPostKi = ns.enum("BsPostKi", True) +BS_POST_KI = { + "Same": BsPostKi.BS_POST_KI_SAME, + "KI/2": BsPostKi.BS_POST_KI_KI_2, +} + +BsPostKp = ns.enum("BsPostKp", True) +BS_POST_KP = { + "Same": BsPostKp.BS_POST_KP_SAME, + "KP": BsPostKp.BS_POST_KP_KP, +} + # Optional settings to generate setter calls for CONFIG_MAP = { cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0), @@ -214,6 +282,15 @@ CONFIG_MAP = { cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False), cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False), cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False), + cv.Optional(CONF_FOC_BS_CS_GATE): cv.boolean, + cv.Optional(CONF_FOC_LIMIT): cv.enum(FOC_LIMIT, upper=False), + cv.Optional(CONF_FOC_PRE_K): cv.enum(FOC_PRE_K, upper=False), + cv.Optional(CONF_FOC_POST_K): cv.enum(FOC_POST_K, upper=False), + cv.Optional(CONF_BS_LIMIT): cv.enum(BS_LIMIT, upper=False), + cv.Optional(CONF_BS_PRE_KI): cv.enum(BS_PRE_KI, upper=False), + cv.Optional(CONF_BS_PRE_KP): cv.enum(BS_PRE_KP, upper=False), + cv.Optional(CONF_BS_POST_KI): cv.enum(BS_POST_KI, upper=False), + cv.Optional(CONF_BS_POST_KP): cv.enum(BS_POST_KP, upper=False), cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean, cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t, cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index ea0138e1dd..f7b90b91cf 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -672,6 +672,69 @@ void CC1101Component::set_hyst_level(HystLevel value) { } } +void CC1101Component::set_foc_bs_cs_gate(bool value) { + this->state_.FOC_BS_CS_GATE = value ? 1 : 0; + if (this->initialized_) { + this->write_(Register::FOCCFG); + } +} + +void CC1101Component::set_foc_limit(FocLimit value) { + this->state_.FOC_LIMIT = static_cast(value); + if (this->initialized_) { + this->write_(Register::FOCCFG); + } +} + +void CC1101Component::set_foc_pre_k(FocPreK value) { + this->state_.FOC_PRE_K = static_cast(value); + if (this->initialized_) { + this->write_(Register::FOCCFG); + } +} + +void CC1101Component::set_foc_post_k(FocPostK value) { + this->state_.FOC_POST_K = static_cast(value); + if (this->initialized_) { + this->write_(Register::FOCCFG); + } +} + +void CC1101Component::set_bs_limit(BsLimit value) { + this->state_.BS_LIMIT = static_cast(value); + if (this->initialized_) { + this->write_(Register::BSCFG); + } +} + +void CC1101Component::set_bs_pre_ki(BsPreKi value) { + this->state_.BS_PRE_KI = static_cast(value); + if (this->initialized_) { + this->write_(Register::BSCFG); + } +} + +void CC1101Component::set_bs_pre_kp(BsPreKp value) { + this->state_.BS_PRE_KP = static_cast(value); + if (this->initialized_) { + this->write_(Register::BSCFG); + } +} + +void CC1101Component::set_bs_post_ki(BsPostKi value) { + this->state_.BS_POST_KI = static_cast(value); + if (this->initialized_) { + this->write_(Register::BSCFG); + } +} + +void CC1101Component::set_bs_post_kp(BsPostKp value) { + this->state_.BS_POST_KP = static_cast(value); + if (this->initialized_) { + this->write_(Register::BSCFG); + } +} + void CC1101Component::set_packet_mode(bool value) { this->state_.PKT_FORMAT = static_cast(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL); diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index 065ffd5250..79bfc9cb33 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -71,6 +71,17 @@ class CC1101Component final : public Component, void set_wait_time(WaitTime value); void set_hyst_level(HystLevel value); + // Frequency offset compensation and bit synchronization settings + void set_foc_bs_cs_gate(bool value); + void set_foc_limit(FocLimit value); + void set_foc_pre_k(FocPreK value); + void set_foc_post_k(FocPostK value); + void set_bs_limit(BsLimit value); + void set_bs_pre_ki(BsPreKi value); + void set_bs_pre_kp(BsPreKp value); + void set_bs_post_ki(BsPostKi value); + void set_bs_post_kp(BsPostKp value); + // Packet mode settings void set_packet_mode(bool value); void set_packet_length(uint8_t value); diff --git a/esphome/components/cc1101/cc1101defs.h b/esphome/components/cc1101/cc1101defs.h index 59b29f7478..6748f4369a 100644 --- a/esphome/components/cc1101/cc1101defs.h +++ b/esphome/components/cc1101/cc1101defs.h @@ -231,6 +231,56 @@ enum class HystLevel : uint8_t { HYST_LEVEL_HIGH, }; +enum class FocLimit : uint8_t { + FOC_LIMIT_DISABLED, + FOC_LIMIT_BW_8, + FOC_LIMIT_BW_4, + FOC_LIMIT_BW_2, +}; + +enum class FocPreK : uint8_t { + FOC_PRE_K_K, + FOC_PRE_K_2K, + FOC_PRE_K_3K, + FOC_PRE_K_4K, +}; + +enum class FocPostK : uint8_t { + FOC_POST_K_SAME, + FOC_POST_K_K_2, +}; + +enum class BsLimit : uint8_t { + BS_LIMIT_DISABLED, + BS_LIMIT_3P125_PERCENT, + BS_LIMIT_6P25_PERCENT, + BS_LIMIT_12P5_PERCENT, +}; + +enum class BsPreKi : uint8_t { + BS_PRE_KI_KI, + BS_PRE_KI_2KI, + BS_PRE_KI_3KI, + BS_PRE_KI_4KI, +}; + +enum class BsPreKp : uint8_t { + BS_PRE_KP_KP, + BS_PRE_KP_2KP, + BS_PRE_KP_3KP, + BS_PRE_KP_4KP, +}; + +enum class BsPostKi : uint8_t { + BS_POST_KI_SAME, + BS_POST_KI_KI_2, +}; + +enum class BsPostKp : uint8_t { + BS_POST_KP_SAME, + BS_POST_KP_KP, +}; + enum class PacketFormat : uint8_t { PACKET_FORMAT_FIFO, PACKET_FORMAT_SYNC_SERIAL, diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index fc1b0f368e..fe050fca22 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -274,7 +274,7 @@ def climate_schema( @setup_entity("climate") async def setup_climate_core_(var, config): - visual = config[CONF_VISUAL] + visual = config.get(CONF_VISUAL, {}) if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add(var.set_visual_min_temperature_override(min_temp)) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 599894c8a9..6c776e0228 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -205,6 +205,9 @@ class ClimateTraits { float get_visual_max_humidity() const { return this->visual_max_humidity_; } void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } + TemperatureUnit get_temperature_unit() const { return this->temperature_unit_; } + void set_temperature_unit(TemperatureUnit unit) { this->temperature_unit_ = unit; } + protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { @@ -274,6 +277,7 @@ class ClimateTraits { climate::ClimateFanModeMask supported_fan_modes_; climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimatePresetMask supported_presets_; + TemperatureUnit temperature_unit_{TemperatureUnit::CELSIUS}; /** Custom mode storage - pointers to vectors owned by the Climate base class. * diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 85878a6306..7476385563 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -16,6 +16,7 @@ CONF_COLOR_DEPTH = "color_depth" CONF_CRC_ENABLE = "crc_enable" CONF_DATA_BITS = "data_bits" CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection" CONF_ENABLED = "enabled" CONF_GYROSCOPE_ODR = "gyroscope_odr" CONF_GYROSCOPE_RANGE = "gyroscope_range" @@ -23,6 +24,7 @@ CONF_IAQ = "iaq" CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_LIBRETINY = "libretiny" CONF_LOOP = "loop" +CONF_NOX_INDEX = "nox_index" CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" @@ -30,14 +32,17 @@ CONF_PARITY = "parity" CONF_RECEIVER_FREQUENCY = "receiver_frequency" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" +CONF_SCAN_PARAMETERS = "scan_parameters" CONF_SHA256 = "sha256" CONF_STATE_SAVE_INTERVAL = "state_save_interval" CONF_STOP_BITS = "stop_bits" CONF_USE_PSRAM = "use_psram" +CONF_VOC_INDEX = "voc_index" CONF_VOLUME_INCREMENT = "volume_increment" CONF_VOLUME_INITIAL = "volume_initial" CONF_VOLUME_MAX = "volume_max" CONF_VOLUME_MIN = "volume_min" +CONF_WINDOW = "window" ICON_CURRENT_DC = "mdi:current-dc" ICON_SOLAR_PANEL = "mdi:solar-panel" diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 839ca532e6..7639e15334 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -131,7 +131,9 @@ _COVER_SCHEMA = ( cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All( cv.requires_component("mqtt"), cv.boolean ), - cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), diff --git a/esphome/components/cst328/__init__.py b/esphome/components/cst328/__init__.py new file mode 100644 index 0000000000..374df64898 --- /dev/null +++ b/esphome/components/cst328/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["i2c"] + +cst328_ns = cg.esphome_ns.namespace("cst328") diff --git a/esphome/components/cst328/binary_sensor/__init__.py b/esphome/components/cst328/binary_sensor/__init__.py new file mode 100644 index 0000000000..6d881cc6c1 --- /dev/null +++ b/esphome/components/cst328/binary_sensor/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv + +from .. import cst328_ns +from ..touchscreen import CST328ButtonListener, CST328Touchscreen + +CONF_CST328_ID = "cst328_id" + +CST328Button = cst328_ns.class_( + "CST328Button", + binary_sensor.BinarySensor, + cg.Component, + CST328ButtonListener, + cg.Parented.template(CST328Touchscreen), +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST328Button).extend( + { + cv.GenerateID(CONF_CST328_ID): cv.use_id(CST328Touchscreen), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_CST328_ID]) diff --git a/esphome/components/cst328/binary_sensor/cst328_button.cpp b/esphome/components/cst328/binary_sensor/cst328_button.cpp new file mode 100644 index 0000000000..b58f4b4b9f --- /dev/null +++ b/esphome/components/cst328/binary_sensor/cst328_button.cpp @@ -0,0 +1,16 @@ +#include "cst328_button.h" +#include "esphome/core/log.h" + +namespace esphome::cst328 { +static const char *const TAG = "cst328.binary_sensor"; + +void CST328Button::setup() { + this->parent_->register_button_listener(this); + this->publish_initial_state(false); +} + +void CST328Button::dump_config() { LOG_BINARY_SENSOR("", "CST328 Button", this); } + +void CST328Button::update_button(bool state) { this->publish_state(state); } + +} // namespace esphome::cst328 diff --git a/esphome/components/cst328/binary_sensor/cst328_button.h b/esphome/components/cst328/binary_sensor/cst328_button.h new file mode 100644 index 0000000000..a9ed4785e5 --- /dev/null +++ b/esphome/components/cst328/binary_sensor/cst328_button.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "../touchscreen/cst328_touchscreen.h" + +namespace esphome::cst328 { + +class CST328Button : public binary_sensor::BinarySensor, + public Component, + public CST328ButtonListener, + public Parented { + public: + void setup() override; + void dump_config() override; + void update_button(bool state) override; +}; + +} // namespace esphome::cst328 diff --git a/esphome/components/cst328/touchscreen/__init__.py b/esphome/components/cst328/touchscreen/__init__.py new file mode 100644 index 0000000000..18c00bb6c5 --- /dev/null +++ b/esphome/components/cst328/touchscreen/__init__.py @@ -0,0 +1,38 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c, touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +from .. import cst328_ns + +CST328Touchscreen = cst328_ns.class_( + "CST328Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CST328ButtonListener = cst328_ns.class_("CST328ButtonListener") + +CONFIG_SCHEMA = ( + touchscreen.touchscreen_schema("100ms") + .extend( + { + cv.GenerateID(): cv.declare_id(CST328Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x1A)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/esphome/components/cst328/touchscreen/cst328_touchscreen.cpp b/esphome/components/cst328/touchscreen/cst328_touchscreen.cpp new file mode 100644 index 0000000000..5e1a2ebf72 --- /dev/null +++ b/esphome/components/cst328/touchscreen/cst328_touchscreen.cpp @@ -0,0 +1,168 @@ +#include "cst328_touchscreen.h" +#include "esphome/core/log.h" + +namespace esphome::cst328 { + +static const char *const TAG = "cst328.touchscreen"; + +static const uint32_t CST328_BEFORE_RESET_TIMEOUT = 50; // 50 ms from datasheet +static const uint32_t CST328_TRANSITION_TIMEOUT = 300; // 200 ms from datasheet, but typically much less +static const uint16_t CST328_FW_CRC = 0xCACA; // Expected firmware CRC value +static const uint8_t CST328_SYNC_BYTE = 0xAB; // Sync byte used in communication + +static const uint8_t ZERO_BYTE = 0; + +#define I2C_WARN_ON_ERROR(x, log_tag, format, ...) \ + do { \ + i2c::ErrorCode err_rc_ = (x); \ + if (err_rc_ != i2c::ERROR_OK) { \ + ESP_LOGW(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \ + this->status_set_warning(format); \ + } \ + } while (0) + +#define I2C_FAIL_ON_ERROR(x, log_tag, format, ...) \ + do { \ + i2c::ErrorCode err_rc_ = (x); \ + if (err_rc_ != i2c::ERROR_OK) { \ + ESP_LOGE(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \ + this->mark_failed(); \ + return; \ + } \ + } while (0) + +void CST328Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up CST328 Touchscreen..."); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + this->set_timeout(CST328_BEFORE_RESET_TIMEOUT, [this] { this->reset_device_(); }); + } else { + this->continue_setup_(); + } +} + +void CST328Touchscreen::reset_device_() { + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + this->set_timeout(CST328_TRANSITION_TIMEOUT, [this] { this->continue_setup_(); }); +} + +void CST328Touchscreen::continue_setup_() { + ESP_LOGV(TAG, "Continuing CST328 setup..."); + + uint8_t data_byte{0}; + uint8_t buf[24]{}; + + I2C_FAIL_ON_ERROR(this->write_register16(CST_WM_DEBUG_INFO, buf, 0), TAG, "Failed to enter debug/info mode"); + I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_CRC_AND_BOOT_TIME, buf, 4), TAG, + "Failed to read FW CRC and boot time"); + + uint16_t fw_crc = buf[2] + (buf[3] << 8); + if (fw_crc != CST328_FW_CRC) { + ESP_LOGE(TAG, "Error: Firmware CRC mismatch, expected 0x%04X but got 0x%04X", CST328_FW_CRC, fw_crc); + this->mark_failed(); + return; + } + + I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_CHIP_TYPE_AND_PROJECT_ID, buf, 4), TAG, + "Failed to read chip and project ID"); + + this->chip_id_ = buf[2] + (buf[3] << 8); + this->project_id_ = buf[0] + (buf[1] << 8); + ESP_LOGD(TAG, "Chip ID %X, project ID %X", this->chip_id_, this->project_id_); + I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_REVISION, buf, 4), TAG, "Failed to read FW version"); + + this->fw_ver_major_ = buf[3]; + this->fw_ver_minor_ = buf[2]; + this->fw_build_ = buf[0] + (buf[1] << 8); + ESP_LOGV(TAG, "FW version %d.%d.%d", this->fw_ver_major_, this->fw_ver_minor_, this->fw_build_); + + if (i2c::ERROR_OK == this->read_register16(CST_REG_X_Y_RESOLUTION, buf, 4)) { + this->x_raw_max_ = buf[0] + (buf[1] << 8); + this->y_raw_max_ = buf[2] + (buf[3] << 8); + } else { + this->x_raw_max_ = this->display_->get_native_width(); + this->y_raw_max_ = this->display_->get_native_height(); + } + + I2C_WARN_ON_ERROR(this->write_register16(CST_WM_NORMAL, buf, 0), TAG, "Failed to enter normal mode"); + I2C_WARN_ON_ERROR(this->read_register16(CST_REG_TOUCH_INFORMATION, &data_byte, 1), TAG, "Failed to read sync"); + I2C_WARN_ON_ERROR(this->write_register16(CST_REG_TOUCH_INFORMATION, &CST328_SYNC_BYTE, 1), TAG, + "Failed to write sync"); + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + + this->setup_complete_ = true; + ESP_LOGV(TAG, "CST328 setup complete"); +} + +void CST328Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "CST328 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Chip ID: 0x%04X, Project ID: 0x%04X", this->chip_id_, this->project_id_); + ESP_LOGCONFIG(TAG, " FW version: %d.%d.%d", this->fw_ver_major_, this->fw_ver_minor_, this->fw_build_); + ESP_LOGCONFIG(TAG, " X/Y resolution: %d/%d", this->x_raw_max_, this->y_raw_max_); +} + +void CST328Touchscreen::update_button_state_(bool state) { + if (this->button_touched_ == state) { + return; + } + this->button_touched_ = state; + for (auto *listener : this->button_listeners_) { + listener->update_button(state); + } +} + +void CST328Touchscreen::update_touches() { + if (!this->setup_complete_) { + this->skip_update_ = true; + return; + } + + uint8_t touch_data[CST328_TOUCH_DATA_SIZE]; + + this->status_clear_warning(); + + if (i2c::ERROR_OK != this->read_register16(CST_REG_TOUCH_INFORMATION, touch_data, CST328_TOUCH_DATA_SIZE)) { + ESP_LOGW(TAG, "Failed to read touch data"); + this->status_set_warning(); + this->skip_update_ = true; + return; + } + + uint8_t touch_cnt = touch_data[CST_REG_FINGER_COUNT_IDX] & 0x0F; + if (touch_cnt == 0 || touch_cnt > CST328_TOUCH_MAX_POINTS) { + this->update_button_state_(false); + } else { + this->update_button_state_(true); + + uint8_t data_idx = 0; + for (uint8_t i = 0; i < touch_cnt; i++) { + uint8_t id = touch_data[data_idx] >> 4; + int16_t x = (touch_data[data_idx + 1] << 4) | ((touch_data[data_idx + 3] >> 4) & 0x0F); + int16_t y = (touch_data[data_idx + 2] << 4) | (touch_data[data_idx + 3] & 0x0F); + int16_t z = touch_data[data_idx + 4]; + + this->add_raw_touch_position_(id, x, y, z); + data_idx += (i == 0) ? 7 : 5; + } + } + + bool cleanup_error = false; + cleanup_error |= (i2c::ERROR_OK != this->write_register16(CST_REG_TOUCH_FINGER_NUMBER, &ZERO_BYTE, 1)); + cleanup_error |= (i2c::ERROR_OK != this->write_register16(CST_REG_TOUCH_INFORMATION, &CST328_SYNC_BYTE, 1)); + + if (cleanup_error) { + ESP_LOGW(TAG, "Failed to clean up touch registers"); + } +} + +} // namespace esphome::cst328 diff --git a/esphome/components/cst328/touchscreen/cst328_touchscreen.h b/esphome/components/cst328/touchscreen/cst328_touchscreen.h new file mode 100644 index 0000000000..234ec6eee0 --- /dev/null +++ b/esphome/components/cst328/touchscreen/cst328_touchscreen.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome::cst328 { + +static const uint8_t CST328_TOUCH_MAX_POINTS = 5; +static const uint8_t CST328_TOUCH_DATA_SIZE = CST328_TOUCH_MAX_POINTS * 5 + 2; + +static const uint16_t CST_REG_TOUCH_INFORMATION = 0xD000; +static const uint16_t CST_REG_TOUCH_FINGER_NUMBER = 0xD005; + +static const uint16_t CST_REG_FINGER_COUNT_IDX = CST_REG_TOUCH_FINGER_NUMBER - CST_REG_TOUCH_INFORMATION; + +static const uint16_t CST_REG_X_Y_RESOLUTION = 0xD1F8; +static const uint16_t CST_REG_FW_CRC_AND_BOOT_TIME = 0xD1FC; +static const uint16_t CST_REG_CHIP_TYPE_AND_PROJECT_ID = 0xD204; +static const uint16_t CST_REG_FW_REVISION = 0xD208; + +static const uint16_t CST_WM_DEBUG_INFO = 0xD101; +static const uint16_t CST_WM_NORMAL = 0xD109; + +class CST328ButtonListener { + public: + virtual void update_button(bool state) = 0; +}; + +class CST328Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void register_button_listener(CST328ButtonListener *listener) { this->button_listeners_.push_back(listener); } + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void update_touches() override; + void reset_device_(); + void continue_setup_(); + void update_button_state_(bool state); + + InternalGPIOPin *interrupt_pin_{}; + GPIOPin *reset_pin_{}; + + std::vector button_listeners_; + bool button_touched_{}; + + uint16_t chip_id_{}; + uint16_t project_id_{}; + uint8_t fw_ver_major_{}; + uint8_t fw_ver_minor_{}; + uint16_t fw_build_{}; + + bool setup_complete_{}; +}; + +} // namespace esphome::cst328 diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index dc032f442e..3e94d04f21 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -70,7 +70,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( }, "debug_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "debug_host.cpp": {PlatformFramework.HOST_NATIVE}, - "debug_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "debug_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, "debug_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 1cc04dcbd8..55b29310a1 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -28,7 +28,7 @@ size_t DebugComponent::get_device_info_(std::span ESP_LOGD(TAG, "LibreTiny debug info:\n" " Version: %s\n" - " Chip: %s (%04x) @ %u MHz\n" + " Chip: %s (%04x) @ %" PRIu32 " MHz\n" " Chip ID: 0x%06" PRIX32 "\n" " Board: %s\n" " Flash: %" PRIu32 " KiB\n" @@ -38,7 +38,7 @@ size_t DebugComponent::get_device_info_(std::span lt_get_board_code(), flash_kib, ram_kib, reset_reason); pos = buf_append_str(buf, size, pos, "|Version: "); - pos = buf_append_str(buf, size, pos, LT_BANNER_STR + 10); + pos = buf_append_str(buf, size, pos, <_BANNER_STR[10]); pos = buf_append_str(buf, size, pos, "|Reset Reason: "); pos = buf_append_str(buf, size, pos, reset_reason); pos = buf_append_str(buf, size, pos, "|Chip Name: "); diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2.cpp similarity index 91% rename from esphome/components/debug/debug_rp2040.cpp rename to esphome/components/debug/debug_rp2.cpp index adc23dbf51..336e9c7e06 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2.cpp @@ -1,5 +1,5 @@ #include "debug_component.h" -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/defines.h" #include "esphome/core/log.h" #include @@ -9,8 +9,8 @@ #else #include #endif -#ifdef USE_RP2040_CRASH_HANDLER -#include "esphome/components/rp2040/crash_handler.h" +#ifdef USE_RP2_CRASH_HANDLER +#include "esphome/components/rp2/crash_handler.h" #endif namespace esphome::debug { @@ -41,8 +41,8 @@ const char *DebugComponent::get_reset_reason_(std::span constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; char *buf = buffer.data(); - uint32_t cpu_freq = ::rp2040.f_cpu(); + uint32_t cpu_freq = RP2040::f_cpu(); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq); diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 9666c8e507..3b70f947d2 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -30,6 +30,7 @@ from esphome.const import ( CONF_SECOND, CONF_SLEEP_DURATION, CONF_TIME_ID, + CONF_TRIGGER_ID, CONF_WAKEUP_PIN, PLATFORM_BK72XX, PLATFORM_ESP32, @@ -234,6 +235,15 @@ EXT1_WAKEUP_MODES = { } WakeupCauseToRunDuration = deep_sleep_ns.struct("WakeupCauseToRunDuration") +WakeupCause = deep_sleep_ns.enum("WakeupCause") +WakeTrigger = deep_sleep_ns.class_( + "WakeTrigger", automation.Trigger.template(WakeupCause), cg.Component +) +Ext1WakeTrigger = deep_sleep_ns.class_( + "Ext1WakeTrigger", automation.Trigger.template(), cg.Component +) + +CONF_ON_WAKE = "on_wake" CONF_WAKEUP_PIN_MODE = "wakeup_pin_mode" CONF_ESP32_EXT1_WAKEUP = "esp32_ext1_wakeup" CONF_TOUCH_WAKEUP = "touch_wakeup" @@ -256,6 +266,22 @@ WAKEUP_PIN_SCHEMA = cv.Schema( } ) +EXT1_WAKEUP_PIN_SCHEMA = cv.Schema( + { + cv.Required(CONF_PIN): cv.All( + pins.internal_gpio_input_pin_schema, validate_pin_number_esp32 + ), + cv.Optional(CONF_ON_WAKE): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Ext1WakeTrigger)} + ), + } +) + +# Entries that are not in the {pin: ..., on_wake: ...} form are treated as a +# bare pin config (the original syntax, e.g. a plain "GPIO5" or {number: 5}). +validate_ext1_wakeup_pin = cv.maybe_simple_value(EXT1_WAKEUP_PIN_SCHEMA, key=CONF_PIN) + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -282,8 +308,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_PINS): cv.ensure_list( - pins.internal_gpio_input_pin_schema, - validate_pin_number_esp32, + validate_ext1_wakeup_pin, ), cv.Required(CONF_MODE): cv.All( cv.enum(EXT1_WAKEUP_MODES, upper=True), @@ -292,6 +317,12 @@ CONFIG_SCHEMA = cv.All( } ), ), + cv.Optional(CONF_ON_WAKE): cv.All( + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), + automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger)} + ), + ), cv.Optional(CONF_TOUCH_WAKEUP): cv.All( cv.only_on_esp32, esp32.only_on_variant( @@ -323,7 +354,7 @@ async def to_code(config): if CONF_WAKEUP_PIN in config: pins_as_list = config.get(CONF_WAKEUP_PIN, []) if CORE.is_bk72xx: - cg.add(var.init_wakeup_pins_(len(pins_as_list))) + cg.add(var.init_wakeup_pins(len(pins_as_list))) for item in pins_as_list: cg.add( var.add_wakeup_pin( @@ -362,16 +393,27 @@ async def to_code(config): ) cg.add(var.set_run_duration(wakeup_cause_to_run_duration)) - if CONF_ESP32_EXT1_WAKEUP in config: - conf = config[CONF_ESP32_EXT1_WAKEUP] + if (ext1_conf := config.get(CONF_ESP32_EXT1_WAKEUP)) is not None: mask = 0 - for pin in conf[CONF_PINS]: - mask |= 1 << pin[CONF_NUMBER] + for pin_conf in ext1_conf[CONF_PINS]: + number = pin_conf[CONF_PIN][CONF_NUMBER] + mask |= 1 << number + for wake_conf in pin_conf.get(CONF_ON_WAKE, []): + trigger = cg.new_Pvariable(wake_conf[CONF_TRIGGER_ID], number) + await cg.register_component(trigger, wake_conf) + await automation.build_automation(trigger, [], wake_conf) + cg.add_define("USE_DEEP_SLEEP_ON_WAKE") struct = cg.StructInitializer( - Ext1Wakeup, ("mask", mask), ("wakeup_mode", conf[CONF_MODE]) + Ext1Wakeup, ("mask", mask), ("wakeup_mode", ext1_conf[CONF_MODE]) ) cg.add(var.set_ext1_wakeup(struct)) + for wake_conf in config.get(CONF_ON_WAKE, []): + trigger = cg.new_Pvariable(wake_conf[CONF_TRIGGER_ID]) + await cg.register_component(trigger, wake_conf) + await automation.build_automation(trigger, [(WakeupCause, "cause")], wake_conf) + cg.add_define("USE_DEEP_SLEEP_ON_WAKE") + if CONF_TOUCH_WAKEUP in config: cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP])) if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations: diff --git a/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp index 8dca32689b..73e0331c76 100644 --- a/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp +++ b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp @@ -7,6 +7,21 @@ namespace esphome::deep_sleep { static const char *const TAG = "deep_sleep.bk72xx"; +#ifdef USE_DEEP_SLEEP_ON_WAKE +WakeupCause get_wakeup_cause() { + switch (lt_get_reboot_reason()) { + case REBOOT_REASON_SLEEP_GPIO: + return WAKEUP_CAUSE_GPIO; + case REBOOT_REASON_SLEEP_RTC: + return WAKEUP_CAUSE_TIMER; + case REBOOT_REASON_SLEEP_USB: + return WAKEUP_CAUSE_UNKNOWN; + default: + return WAKEUP_CAUSE_NONE; + } +} +#endif // USE_DEEP_SLEEP_ON_WAKE + optional DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } void DeepSleepComponent::dump_config_platform_() { @@ -15,15 +30,15 @@ void DeepSleepComponent::dump_config_platform_() { } } -bool DeepSleepComponent::pin_prevents_sleep_(WakeUpPinItem &pinItem) const { - return (pinItem.wakeup_pin_mode == WAKEUP_PIN_MODE_KEEP_AWAKE && pinItem.wakeup_pin != nullptr && - !this->sleep_duration_.has_value() && (pinItem.wakeup_level == get_real_pin_state_(*pinItem.wakeup_pin))); +bool DeepSleepComponent::pin_prevents_sleep_(WakeUpPinItem &pin_item) const { + return (pin_item.wakeup_pin_mode == WAKEUP_PIN_MODE_KEEP_AWAKE && pin_item.wakeup_pin != nullptr && + !this->sleep_duration_.has_value() && (pin_item.wakeup_level == get_real_pin_state_(*pin_item.wakeup_pin))); } bool DeepSleepComponent::prepare_to_sleep_() { - if (wakeup_pins_.size() > 0) { + if (!this->wakeup_pins_.empty()) { for (WakeUpPinItem &item : this->wakeup_pins_) { - if (pin_prevents_sleep_(item)) { + if (this->pin_prevents_sleep_(item)) { // Defer deep sleep until inactive if (!this->next_enter_deep_sleep_) { this->status_set_warning(); @@ -44,7 +59,7 @@ void DeepSleepComponent::deep_sleep_() { item.wakeup_level = !item.wakeup_level; } } - ESP_LOGI(TAG, "Wake-up on P%u %s (%d)", item.wakeup_pin->get_pin(), item.wakeup_level ? "HIGH" : "LOW", + ESP_LOGI(TAG, "Wake-up on P%u %s (%" PRId32 ")", item.wakeup_pin->get_pin(), item.wakeup_level ? "HIGH" : "LOW", static_cast(item.wakeup_pin_mode)); } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 896ed092aa..a620d52a02 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -60,6 +60,65 @@ struct WakeupCauseToRunDuration { #endif // USE_ESP32 +#ifdef USE_DEEP_SLEEP_ON_WAKE + +/// Why the device woke from deep sleep. Passed to on_wake automations. +enum WakeupCause : uint8_t { + /// The device did not wake from deep sleep (for example a cold boot, reset or OTA restart). + WAKEUP_CAUSE_NONE = 0, + /// The device woke from deep sleep, but the source could not be identified. + WAKEUP_CAUSE_UNKNOWN, + /// The device was woken by the sleep timer. + WAKEUP_CAUSE_TIMER, + /// The device was woken by a GPIO pin (wakeup_pin or esp32_ext1_wakeup). + WAKEUP_CAUSE_GPIO, + /// The device was woken by a touch pad. + WAKEUP_CAUSE_TOUCH, +}; + +/// Return why the device woke from deep sleep. Implemented per platform. +WakeupCause get_wakeup_cause(); + +/** Setup priority of on_wake triggers. + * + * Between restoring global variables (setup_priority::HARDWARE, 800) and on_boot automations at + * their default priority (600), so on_wake automations can update state (e.g. globals) that + * on_boot automations then use. + */ +inline constexpr float ON_WAKE_TRIGGER_SETUP_PRIORITY = 700.0f; + +/// Fires once on boot when the device woke from deep sleep, with the wakeup cause. +class WakeTrigger : public Trigger, public Component { + public: + void setup() override { + const WakeupCause cause = get_wakeup_cause(); + if (cause != WAKEUP_CAUSE_NONE) { + this->trigger(cause); + } + } + float get_setup_priority() const override { return ON_WAKE_TRIGGER_SETUP_PRIORITY; } +}; + +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) +/// Fires once on boot when the device was woken from deep sleep by the given ext1 pin. +class Ext1WakeTrigger : public Trigger<>, public Component { + public: + explicit Ext1WakeTrigger(uint8_t pin) : pin_(pin) {} + void setup() override { + if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT1 && + (esp_sleep_get_ext1_wakeup_status() & (1ULL << this->pin_))) { + this->trigger(); + } + } + float get_setup_priority() const override { return ON_WAKE_TRIGGER_SETUP_PRIORITY; } + + protected: + uint8_t pin_; +}; +#endif + +#endif // USE_DEEP_SLEEP_ON_WAKE + template class EnterDeepSleepAction; template class PreventDeepSleepAction; @@ -84,7 +143,7 @@ class DeepSleepComponent final : public Component { #endif // USE_ESP32 #if defined(USE_BK72XX) - void init_wakeup_pins_(size_t capacity) { this->wakeup_pins_.init(capacity); } + void init_wakeup_pins(size_t capacity) { this->wakeup_pins_.init(capacity); } void add_wakeup_pin(InternalGPIOPin *wakeup_pin, WakeupPinMode wakeup_pin_mode) { this->wakeup_pins_.emplace_back(WakeUpPinItem{wakeup_pin, wakeup_pin_mode, !wakeup_pin->is_inverted()}); } @@ -132,7 +191,7 @@ class DeepSleepComponent final : public Component { bool should_teardown_(); #ifdef USE_BK72XX - bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const; + bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const; bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); } #endif // USE_BK72XX diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index 7cb8e53efd..f64e1f37e1 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -30,6 +30,25 @@ namespace esphome::deep_sleep { static const char *const TAG = "deep_sleep"; +#ifdef USE_DEEP_SLEEP_ON_WAKE +WakeupCause get_wakeup_cause() { + switch (esp_sleep_get_wakeup_cause()) { + case ESP_SLEEP_WAKEUP_EXT0: + case ESP_SLEEP_WAKEUP_EXT1: + case ESP_SLEEP_WAKEUP_GPIO: + return WAKEUP_CAUSE_GPIO; + case ESP_SLEEP_WAKEUP_TIMER: + return WAKEUP_CAUSE_TIMER; + case ESP_SLEEP_WAKEUP_TOUCHPAD: + return WAKEUP_CAUSE_TOUCH; + case ESP_SLEEP_WAKEUP_UNDEFINED: + return WAKEUP_CAUSE_NONE; + default: + return WAKEUP_CAUSE_UNKNOWN; + } +} +#endif // USE_DEEP_SLEEP_ON_WAKE + optional DeepSleepComponent::get_run_duration_() const { if (this->wakeup_cause_to_run_duration_.has_value()) { esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause(); diff --git a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp index 9239a7fb31..2b98f4b855 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp @@ -3,10 +3,27 @@ #include +#ifdef USE_DEEP_SLEEP_ON_WAKE +extern "C" { +#include +} +#endif + namespace esphome::deep_sleep { static const char *const TAG = "deep_sleep"; +#ifdef USE_DEEP_SLEEP_ON_WAKE +WakeupCause get_wakeup_cause() { + // The ESP8266 can only wake from deep sleep through the RTC timer (via GPIO16 -> RST). + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + if (ESP.getResetInfoPtr()->reason == REASON_DEEP_SLEEP_AWAKE) { + return WAKEUP_CAUSE_TIMER; + } + return WAKEUP_CAUSE_NONE; +} +#endif // USE_DEEP_SLEEP_ON_WAKE + optional DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } void DeepSleepComponent::dump_config_platform_() {} diff --git a/esphome/components/deep_sleep/deep_sleep_zephyr.cpp b/esphome/components/deep_sleep/deep_sleep_zephyr.cpp index f77b73cd58..cadf7bf42d 100644 --- a/esphome/components/deep_sleep/deep_sleep_zephyr.cpp +++ b/esphome/components/deep_sleep/deep_sleep_zephyr.cpp @@ -1,13 +1,36 @@ #include "deep_sleep_component.h" #ifdef USE_ZEPHYR +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/wake.h" #include +#include namespace esphome::deep_sleep { static const char *const TAG = "deep_sleep"; +// The Zephyr watchdog has a short window (2s, or 10s with Zigbee) and +// WDT_OPT_PAUSE_IN_SLEEP only pauses it during true hardware sleep — not while a +// radio thread (e.g. the Zigbee stack) keeps the CPU busy in k_sem_take(). Feed +// it at least this often while waiting so it does not reset the device. +static const uint32_t WDT_FEED_INTERVAL_MS = 1000; + +static bool wakeable_delay_feed_wdt(uint32_t ms) { + while (ms > 0) { + const uint32_t step = std::min(ms, WDT_FEED_INTERVAL_MS); + esphome::internal::wakeable_delay(step); + esphome::arch_feed_wdt(); + if (esphome::wake_request_take()) { + return true; + } + if (ms != UINT32_MAX) { + ms -= step; + } + } + return false; +} + optional DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } void DeepSleepComponent::dump_config_platform_() {} @@ -15,8 +38,9 @@ void DeepSleepComponent::dump_config_platform_() {} bool DeepSleepComponent::prepare_to_sleep_() { return true; } void DeepSleepComponent::deep_sleep_() { + bool woke = false; if (this->sleep_duration_.has_value()) { - esphome::internal::wakeable_delay(static_cast(*this->sleep_duration_ / 1000)); + woke = wakeable_delay_feed_wdt(static_cast(*this->sleep_duration_ / 1000)); } else { #ifndef USE_ZIGBEE // the device can be woken up through one of the following signals: @@ -29,10 +53,9 @@ void DeepSleepComponent::deep_sleep_() { // The system is reset when it wakes up from System OFF mode. sys_poweroff(); #else - esphome::internal::wakeable_delay(UINT32_MAX); + woke = wakeable_delay_feed_wdt(UINT32_MAX); #endif } - const bool woke = esphome::wake_request_take(); if (woke) { ESP_LOGD(TAG, "Woken up by another thread"); } else { diff --git a/esphome/components/ds248x/__init__.py b/esphome/components/ds248x/__init__.py new file mode 100644 index 0000000000..5a26ceab50 --- /dev/null +++ b/esphome/components/ds248x/__init__.py @@ -0,0 +1,112 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_SLEEP_PIN, CONF_TYPE + +CODEOWNERS = ["@tomwellnitz"] +MULTI_CONF = True +DEPENDENCIES = ["i2c"] + +CONF_DS248X_ID = "ds248x_id" +CONF_BUS_SLEEP = "bus_sleep" +CONF_HUB_SLEEP = "hub_sleep" +CONF_ACTIVE_PULLUP = "active_pullup" + +CONF_RESET_LOW_TIME = "reset_low_time" +CONF_MASTER_SAMPLE_TIME = "master_sample_time" +CONF_WRITE_0_LOW_TIME = "write_0_low_time" +CONF_RECOVERY_TIME = "recovery_time" +CONF_ACTIVE_PULLUP_RESISTANCE = "active_pullup_resistance" + +TYPE_DS2482_100 = "ds2482-100" +TYPE_DS2482_101 = "ds2482-101" +TYPE_DS2482_800 = "ds2482-800" +TYPE_DS2484 = "ds2484" + +CHANNEL_COUNTS = { + TYPE_DS2482_100: 1, + TYPE_DS2482_101: 1, + TYPE_DS2482_800: 8, + TYPE_DS2484: 1, +} + +ds248x_ns = cg.esphome_ns.namespace("ds248x") +DS248xComponent = ds248x_ns.class_("DS248xComponent", cg.Component, i2c.I2CDevice) + + +def _component_schema(*extras): + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DS248xComponent), + cv.Optional(CONF_ACTIVE_PULLUP, default=False): cv.boolean, + } + ) + for extra in extras: + schema = schema.extend(extra) + return schema.extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x18)) + + +SLEEP_SCHEMA = { + cv.Optional(CONF_SLEEP_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_BUS_SLEEP, default=False): cv.boolean, + cv.Optional(CONF_HUB_SLEEP, default=False): cv.boolean, +} + +DS2484_SCHEMA = { + cv.Optional(CONF_RESET_LOW_TIME): cv.int_range(min=0, max=15), + cv.Optional(CONF_MASTER_SAMPLE_TIME): cv.int_range(min=0, max=15), + cv.Optional(CONF_WRITE_0_LOW_TIME): cv.int_range(min=0, max=15), + cv.Optional(CONF_RECOVERY_TIME): cv.int_range(min=0, max=15), + cv.Optional(CONF_ACTIVE_PULLUP_RESISTANCE): cv.enum( + { + # DS2484 Table 7: value codes 0-5 map to 500 ohm, 6-15 map to 1000 ohm. + "500ohm": 0, + "1000ohm": 6, + } + ), +} + +CONFIG_SCHEMA = cv.typed_schema( + { + TYPE_DS2482_100: _component_schema(), + TYPE_DS2482_101: _component_schema(SLEEP_SCHEMA), + TYPE_DS2482_800: _component_schema(), + TYPE_DS2484: _component_schema(SLEEP_SCHEMA, DS2484_SCHEMA), + }, + key=CONF_TYPE, + lower=True, +) + + +def get_channel_count(config): + return CHANNEL_COUNTS[config[CONF_TYPE]] + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_active_pullup(config[CONF_ACTIVE_PULLUP])) + cg.add(var.set_channel_count(get_channel_count(config))) + + if CONF_BUS_SLEEP in config: + cg.add(var.set_bus_sleep(config[CONF_BUS_SLEEP])) + if CONF_HUB_SLEEP in config: + cg.add(var.set_hub_sleep(config[CONF_HUB_SLEEP])) + + if CONF_RESET_LOW_TIME in config: + cg.add(var.set_val_trstl(config[CONF_RESET_LOW_TIME])) + if CONF_MASTER_SAMPLE_TIME in config: + cg.add(var.set_val_tmsp(config[CONF_MASTER_SAMPLE_TIME])) + if CONF_WRITE_0_LOW_TIME in config: + cg.add(var.set_val_tw0l(config[CONF_WRITE_0_LOW_TIME])) + if CONF_RECOVERY_TIME in config: + cg.add(var.set_val_trec0(config[CONF_RECOVERY_TIME])) + if CONF_ACTIVE_PULLUP_RESISTANCE in config: + cg.add(var.set_val_rwpu(config[CONF_ACTIVE_PULLUP_RESISTANCE])) + + if CONF_SLEEP_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_SLEEP_PIN]) + cg.add(var.set_sleep_pin(pin)) diff --git a/esphome/components/ds248x/ds248x.cpp b/esphome/components/ds248x/ds248x.cpp new file mode 100644 index 0000000000..c8f7395119 --- /dev/null +++ b/esphome/components/ds248x/ds248x.cpp @@ -0,0 +1,320 @@ +#include "ds248x.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome::ds248x { + +static const char *const TAG = "ds248x"; + +void DS248xComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up DS248x..."); + + // Wake up device if sleep pin is configured + if (this->sleep_pin_) { + this->sleep_pin_->setup(); + this->sleep_pin_->pin_mode(esphome::gpio::FLAG_OUTPUT); + this->sleep_pin_->digital_write(true); // Wake up + delay(1); // DS2482-101 Datasheet: tOSCWUP = 100μs (using 10x margin) + } + + // Probe device + ESP_LOGD(TAG, "Probing DS248x..."); + uint8_t status = 0; + if (this->read(&status, 1) == i2c::ERROR_OK) { + ESP_LOGD(TAG, "Device responded! Status: 0x%02x", status); + } else { + ESP_LOGW(TAG, "Device did not respond. Trying reset anyway..."); + } + + if (!this->device_reset_()) { + ESP_LOGW(TAG, "DS248x reset failed during setup!"); + } + + // Configure device + if (!this->device_configure_()) { + ESP_LOGE(TAG, "DS248x configuration failed!"); + this->mark_failed(); + return; + } + + // Reset to Channel 0 + this->select_channel(0); + + ESP_LOGI(TAG, "DS248x initialized successfully."); +} + +void DS248xComponent::on_shutdown() { + if (this->sleep_pin_ && (this->hub_sleep_ || this->bus_sleep_)) { + this->sleep_pin_->digital_write(false); // Sleep + } +} + +void DS248xComponent::dump_config() { + ESP_LOGCONFIG(TAG, "DS248x:"); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Channel Count: %d", this->channel_count_); + ESP_LOGCONFIG(TAG, " Active Pullup: %s", YESNO(this->active_pullup_)); + if (this->ds2484_mode_) { + ESP_LOGCONFIG(TAG, " DS2484 Mode: enabled"); + } +} + +// --- Internal Helpers --- + +// Datasheet command durations are sub-2ms; allow a little margin before forcing recovery. +static constexpr uint32_t BUSY_TIMEOUT_MS = 5; + +bool DS248xComponent::set_read_pointer_(uint8_t ptr) { return this->write_byte(DS248X_COMMAND_SETREADPTR, ptr); } + +bool DS248xComponent::wait_busy_() { + uint32_t start = millis(); + do { + uint8_t status; + if (this->read(&status, 1) == i2c::ERROR_OK && !(status & DS248X_STATUS_BUSY)) + return true; + delayMicroseconds(100); + } while (millis() - start < BUSY_TIMEOUT_MS); + ESP_LOGW(TAG, "DS248x busy timeout"); + bool recovered = this->device_reset_() && this->device_configure_(); + this->current_channel_ = -1; + if (!recovered) { + ESP_LOGE(TAG, "DS248x recovery failed after busy timeout"); + this->mark_failed(); + } + return false; +} + +bool DS248xComponent::device_reset_() { + ESP_LOGD(TAG, "Resetting device..."); + uint8_t cmd = DS248X_COMMAND_RESET; + if (this->write(&cmd, 1) != i2c::ERROR_OK) + return false; + + uint8_t status; + if (this->read(&status, 1) != i2c::ERROR_OK) + return false; + + if (!(status & DS248X_STATUS_RST)) { + ESP_LOGW(TAG, "Device reset failed (RST bit not set)"); + return false; + } + + this->current_channel_ = -1; + return true; +} + +bool DS248xComponent::device_configure_() { + ESP_LOGD(TAG, "Configuring device..."); + + if (!this->write_config_()) { + ESP_LOGW(TAG, "Config write/verify failed"); + return false; + } + + ESP_LOGD(TAG, "Configured successfully"); + + // DS2484 Configuration + if (this->ds2484_mode_) { + if (this->ds2484_trstl_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_TRSTL, this->ds2484_trstl_)) + return false; + if (this->ds2484_tmsp_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_TMSP, this->ds2484_tmsp_)) + return false; + if (this->ds2484_tw0l_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_TW0L, this->ds2484_tw0l_)) + return false; + if (this->ds2484_trec0_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_TREC0, this->ds2484_trec0_)) + return false; + if (this->ds2484_rwpu_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_RWPU, this->ds2484_rwpu_)) + return false; + } + + return true; +} + +bool DS248xComponent::configure_ds2484_port_(uint8_t param, uint8_t val) { + uint8_t cmd = DS2484_COMMAND_ADJUSTPORT; + // Control Byte format (DS2484 Table 6): P[2:0] in bits 7:5, OD in bit 4, VAL[3:0] in bits 3:0 + uint8_t data = ((param & 0x07) << 5) | (val & 0x0F); + + // The DS2484 always acknowledges the Adjust 1-Wire Port control byte (datasheet "Adjust + // 1-Wire Port"), so a successful write confirms the update. We deliberately do not read + // back to verify: a single read of the Port Configuration register always returns the + // fixed 8-byte report starting at Byte 1 (tRSTL standard speed), not the parameter that + // was just written, so a per-parameter readback comparison would spuriously fail for + // tMSP/tW0L/tREC0/RWPU. + if (!this->write_byte(cmd, data)) { + ESP_LOGW(TAG, "DS2484 port config failed (param %d)", param); + return false; + } + + return this->set_read_pointer_(DS248X_POINTER_STATUS); +} + +bool DS248xComponent::write_config_() { + uint8_t config = 0; + if (this->active_pullup_) + config |= DS248X_CONFIG_ACTIVE_PULLUP; + + // The DS248x only accepts the config byte if the upper nibble is the one's-complement of the lower nibble. + uint8_t config_byte = (config & 0x0F) | ((~config & 0x0F) << 4); + + if (!this->write_byte(DS248X_COMMAND_WRITECONFIG, config_byte)) { + ESP_LOGW(TAG, "Failed to write config byte"); + return false; + } + + if (!this->set_read_pointer_(DS248X_POINTER_CONFIG)) { + return false; + } + + uint8_t read_config; + if (this->read(&read_config, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Failed to read back config byte"); + return false; + } + + if ((read_config & 0x0F) != (config_byte & 0x0F)) { + ESP_LOGW(TAG, "Config mismatch! Wrote 0x%02x, Read 0x%02x", config_byte, read_config); + return false; + } + + return this->set_read_pointer_(DS248X_POINTER_STATUS); +} + +// --- Channel Selection --- + +// Channel select codes: write code -> expected read code +static constexpr uint8_t CHANNEL_WRITE_CODES[8] = {0xF0, 0xE1, 0xD2, 0xC3, 0xB4, 0xA5, 0x96, 0x87}; +static constexpr uint8_t CHANNEL_READ_CODES[8] = {0xB8, 0xB1, 0xAA, 0xA3, 0x9C, 0x95, 0x8E, 0x87}; + +bool DS248xComponent::select_channel(uint8_t channel) { + if (this->channel_count_ <= 1) + return true; + if (channel >= this->channel_count_) + return false; + + if (this->current_channel_ == channel) + return true; + + if (!this->write_byte(DS248X_COMMAND_CHANNELSELECT, CHANNEL_WRITE_CODES[channel])) { + this->current_channel_ = -1; + return false; + } + + uint8_t read_code; + if (this->read(&read_code, 1) != i2c::ERROR_OK) { + this->current_channel_ = -1; + return false; + } + + if (read_code != CHANNEL_READ_CODES[channel]) { + ESP_LOGW(TAG, "Channel select failed! Expected 0x%02x, got 0x%02x", CHANNEL_READ_CODES[channel], read_code); + this->current_channel_ = -1; + return false; + } + + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + this->current_channel_ = channel; + return true; +} + +// --- 1-Wire Bus Operations --- + +bool DS248xComponent::ow_reset(bool &presence) { + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + uint8_t cmd = DS248X_COMMAND_RESETWIRE; + if (this->write(&cmd, 1) != i2c::ERROR_OK) + return false; + + if (!this->wait_busy_()) { + ESP_LOGW(TAG, "ow_reset: wait busy failed"); + return false; + } + + uint8_t status; + if (this->read(&status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "ow_reset: read status failed"); + return false; + } + + if (status & DS248X_STATUS_SD) { + ESP_LOGW(TAG, "Short detected on 1-Wire bus!"); + return false; + } + + presence = (status & DS248X_STATUS_PPD); + return true; +} + +bool DS248xComponent::ow_write_byte(uint8_t byte) { + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + if (!this->wait_busy_()) { + ESP_LOGW(TAG, "Device busy before writing byte 0x%02x", byte); + return false; + } + + uint8_t cmd[2] = {DS248X_COMMAND_WRITEBYTE, byte}; + if (this->write(cmd, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "I2C write failed for byte 0x%02x", byte); + return false; + } + + if (!this->wait_busy_()) { + ESP_LOGW(TAG, "Timeout waiting for write byte to complete!"); + return false; + } + + return true; +} + +bool DS248xComponent::ow_read_byte(uint8_t &byte) { + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + uint8_t cmd = DS248X_COMMAND_READBYTE; + if (this->write(&cmd, 1) != i2c::ERROR_OK) + return false; + + if (!this->wait_busy_()) + return false; + + if (!this->set_read_pointer_(DS248X_POINTER_DATA)) + return false; + + if (this->read(&byte, 1) != i2c::ERROR_OK) + return false; + + return true; +} + +bool DS248xComponent::search_triplet(bool search_direction, uint8_t &status) { + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + // DS248x Datasheet: 1-Wire Triplet command requires 2 bytes: + // Byte 1: Command code 0x78 + // Byte 2: Direction byte (bit 7 = V, search direction if discrepancy) + uint8_t buffer[2] = {DS248X_COMMAND_TRIPLET, static_cast(search_direction ? 0x80 : 0x00)}; + if (this->write(buffer, 2) != i2c::ERROR_OK) + return false; + + if (!this->wait_busy_()) + return false; + + if (this->read(&status, 1) != i2c::ERROR_OK) + return false; + + return true; +} + +} // namespace esphome::ds248x diff --git a/esphome/components/ds248x/ds248x.h b/esphome/components/ds248x/ds248x.h new file mode 100644 index 0000000000..0873ee0e2a --- /dev/null +++ b/esphome/components/ds248x/ds248x.h @@ -0,0 +1,133 @@ +#pragma once + +// DS248x I2C-to-1-Wire Bridge Family +// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ds2482-100.pdf +// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ds2482-800.pdf +// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ds2484.pdf + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::ds248x { + +// DS248x I2C Commands +static constexpr uint8_t DS248X_COMMAND_RESET = 0xF0; +static constexpr uint8_t DS248X_COMMAND_SETREADPTR = 0xE1; +static constexpr uint8_t DS248X_COMMAND_WRITECONFIG = 0xD2; +static constexpr uint8_t DS248X_COMMAND_CHANNELSELECT = 0xC3; +static constexpr uint8_t DS248X_COMMAND_RESETWIRE = 0xB4; +static constexpr uint8_t DS248X_COMMAND_WRITEBYTE = 0xA5; +static constexpr uint8_t DS248X_COMMAND_READBYTE = 0x96; +static constexpr uint8_t DS248X_COMMAND_TRIPLET = 0x78; +static constexpr uint8_t DS2484_COMMAND_ADJUSTPORT = 0xC3; + +// DS2484 "Adjust 1-Wire Port" parameter codes (datasheet Table 6, control byte P[2:0]) +static constexpr uint8_t DS2484_PORT_PARAM_TRSTL = 0x0; +static constexpr uint8_t DS2484_PORT_PARAM_TMSP = 0x1; +static constexpr uint8_t DS2484_PORT_PARAM_TW0L = 0x2; +static constexpr uint8_t DS2484_PORT_PARAM_TREC0 = 0x3; +static constexpr uint8_t DS2484_PORT_PARAM_RWPU = 0x4; + +// DS248x Status Register Bits +static constexpr uint8_t DS248X_STATUS_BUSY = 0x01; +static constexpr uint8_t DS248X_STATUS_PPD = 0x02; +static constexpr uint8_t DS248X_STATUS_SD = 0x04; +static constexpr uint8_t DS248X_STATUS_RST = 0x10; +static constexpr uint8_t DS248X_STATUS_SBR = 0x20; +static constexpr uint8_t DS248X_STATUS_TSB = 0x40; +static constexpr uint8_t DS248X_STATUS_DIR = 0x80; + +// DS248x Register Pointers +static constexpr uint8_t DS248X_POINTER_STATUS = 0xF0; +static constexpr uint8_t DS248X_POINTER_DATA = 0xE1; +static constexpr uint8_t DS248X_POINTER_CONFIG = 0xC3; + +// DS248x Configuration Bits +static constexpr uint8_t DS248X_CONFIG_ACTIVE_PULLUP = 0x01; + +/** + * @brief DS248x I2C-to-1-Wire Bridge Component. + * + * This component manages the DS248x chip (DS2482-100, DS2482-800, DS2484). + * It provides low-level 1-Wire bus operations via I2C. + * + * Usage: Configure DS248xOneWireBus instances for each channel. + * These buses implement the one_wire::OneWireBus interface for compatibility + * with all existing 1-Wire device components (dallas_temp, etc.). + */ +class DS248xComponent : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void on_shutdown() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_sleep_pin(InternalGPIOPin *pin) { this->sleep_pin_ = pin; } + void set_bus_sleep(bool enabled) { this->bus_sleep_ = enabled; } + void set_hub_sleep(bool enabled) { this->hub_sleep_ = enabled; } + void set_channel_count(uint8_t count) { this->channel_count_ = count; } + void set_active_pullup(bool enabled) { this->active_pullup_ = enabled; } + + // DS2484 Timing Parameters + void set_val_trstl(uint8_t val) { + this->ds2484_trstl_ = val; + this->ds2484_mode_ = true; + } + void set_val_tmsp(uint8_t val) { + this->ds2484_tmsp_ = val; + this->ds2484_mode_ = true; + } + void set_val_tw0l(uint8_t val) { + this->ds2484_tw0l_ = val; + this->ds2484_mode_ = true; + } + void set_val_trec0(uint8_t val) { + this->ds2484_trec0_ = val; + this->ds2484_mode_ = true; + } + void set_val_rwpu(uint8_t val) { + this->ds2484_rwpu_ = val; + this->ds2484_mode_ = true; + } + + /// Get the channel count (1 for DS2482-100/DS2484, 8 for DS2482-800) + uint8_t get_channel_count() const { return this->channel_count_; } + + // --- Core 1-Wire API (used by DS248xOneWireBus) --- + bool select_channel(uint8_t channel); + bool ow_reset(bool &presence); + bool ow_write_byte(uint8_t byte); + bool ow_read_byte(uint8_t &byte); + + // --- Search support (used by DS248xOneWireBus) --- + bool search_triplet(bool search_direction, uint8_t &status); + + protected: + InternalGPIOPin *sleep_pin_{nullptr}; + uint8_t channel_count_ = 1; + bool bus_sleep_{false}; + bool hub_sleep_{false}; + bool active_pullup_ = false; + + // DS2484 Config + bool ds2484_mode_ = false; + static constexpr uint8_t DS2484_PARAM_UNSET = 0xFF; + uint8_t ds2484_trstl_{DS2484_PARAM_UNSET}; + uint8_t ds2484_tmsp_{DS2484_PARAM_UNSET}; + uint8_t ds2484_tw0l_{DS2484_PARAM_UNSET}; + uint8_t ds2484_trec0_{DS2484_PARAM_UNSET}; + uint8_t ds2484_rwpu_{DS2484_PARAM_UNSET}; + + int8_t current_channel_{-1}; + + // Internal helpers + bool set_read_pointer_(uint8_t ptr); + bool wait_busy_(); + bool device_reset_(); + bool device_configure_(); + bool configure_ds2484_port_(uint8_t param, uint8_t val); + bool write_config_(); +}; + +} // namespace esphome::ds248x diff --git a/esphome/components/ds248x/ds248x_one_wire_bus.cpp b/esphome/components/ds248x/ds248x_one_wire_bus.cpp new file mode 100644 index 0000000000..ed5b05bab7 --- /dev/null +++ b/esphome/components/ds248x/ds248x_one_wire_bus.cpp @@ -0,0 +1,171 @@ +#include "ds248x_one_wire_bus.h" +#include "ds248x.h" +#include "esphome/core/log.h" + +namespace esphome::ds248x { + +static const char *const TAG = "ds248x.one_wire"; + +void DS248xOneWireBus::setup() { + ESP_LOGCONFIG(TAG, "Setting up DS248x 1-Wire Bus (Channel %d)...", this->channel_); + + // Parent setup happens in DS248xComponent::setup() + // We just need to scan for devices on this channel + if (!this->ensure_channel_()) { + ESP_LOGE(TAG, "Failed to select channel %d during setup", this->channel_); + this->mark_failed(); + return; + } + + // Perform device search on this channel + this->search(); + + ESP_LOGCONFIG(TAG, "Found %zu devices on channel %d", this->devices_.size(), this->channel_); +} + +void DS248xOneWireBus::dump_config() { + ESP_LOGCONFIG(TAG, "DS248x 1-Wire Bus (Channel %d):", this->channel_); + this->dump_devices_(TAG); +} + +bool DS248xOneWireBus::ensure_channel_() { + if (this->parent_ == nullptr) { + ESP_LOGE(TAG, "Parent not set!"); + return false; + } + return this->parent_->select_channel(this->channel_); +} + +int DS248xOneWireBus::reset_int() { + if (!this->ensure_channel_()) { + return -1; + } + + bool presence = false; + if (!this->parent_->ow_reset(presence)) { + return -1; + } + return presence ? 1 : 0; +} + +void DS248xOneWireBus::write8(uint8_t val) { + if (!this->ensure_channel_()) { + return; + } + if (!this->parent_->ow_write_byte(val)) { + ESP_LOGE(TAG, "Failed to write byte 0x%02X on channel %d", val, this->channel_); + } +} + +void DS248xOneWireBus::write64(uint64_t val) { + if (!this->ensure_channel_()) { + return; + } + for (uint8_t i = 0; i < 8; i++) { + uint8_t byte = static_cast(val >> (i * 8)); + if (!this->parent_->ow_write_byte(byte)) { + ESP_LOGE(TAG, "Failed to write byte %d/8 (0x%02X) on channel %d - aborting write64", i + 1, byte, this->channel_); + return; // Stop writing to prevent sending corrupted data + } + } +} + +uint8_t DS248xOneWireBus::read8() { + if (!this->ensure_channel_()) { + return 0; + } + uint8_t value = 0; + if (!this->parent_->ow_read_byte(value)) { + ESP_LOGE(TAG, "Failed to read byte on channel %d", this->channel_); + } + return value; +} + +uint64_t DS248xOneWireBus::read64() { + if (!this->ensure_channel_()) { + return 0; + } + uint64_t value = 0; + for (uint8_t i = 0; i < 8; i++) { + uint8_t byte = 0; + if (!this->parent_->ow_read_byte(byte)) { + ESP_LOGE(TAG, "Failed to read byte %d/8 on channel %d - returning partial data", i + 1, this->channel_); + return value; // Return partial data to avoid blocking, caller should validate + } + value |= (static_cast(byte) << (i * 8)); + } + return value; +} + +void DS248xOneWireBus::reset_search() { + this->search_last_discrepancy_ = 0; + this->search_last_device_flag_ = false; + this->search_address_ = 0; +} + +uint64_t DS248xOneWireBus::search_int() { + if (!this->ensure_channel_()) { + return 0; + } + + if (this->search_last_device_flag_) { + return 0; + } + + uint8_t last_zero = 0; + uint64_t address = this->search_address_; + + // Iterate through all 64 bits + for (uint8_t bit_number = 1; bit_number <= 64; bit_number++) { + uint64_t bit_mask = 1ULL << (bit_number - 1); + + // Determine search direction + bool search_direction; + if (bit_number < this->search_last_discrepancy_) { + search_direction = (address & bit_mask) != 0; + } else { + search_direction = (bit_number == this->search_last_discrepancy_); + } + + // Perform triplet operation + uint8_t status = 0; + if (!this->parent_->search_triplet(search_direction, status)) { + ESP_LOGW(TAG, "1-Wire triplet failed at bit %d on channel %d - aborting search", bit_number, this->channel_); + this->reset_search(); + return 0; + } + + bool id_bit = (status & DS248X_STATUS_SBR) != 0; + bool cmp_id_bit = (status & DS248X_STATUS_TSB) != 0; + bool dir_taken = (status & DS248X_STATUS_DIR) != 0; + + if (id_bit && cmp_id_bit) { + // No devices participating + this->reset_search(); + return 0; + } + + if (!id_bit && !cmp_id_bit && !dir_taken) { + // Discrepancy, went 0 - record position + last_zero = bit_number; + } + + // Update address based on direction taken + if (dir_taken) { + address |= bit_mask; + } else { + address &= ~bit_mask; + } + } + + // Search successful + this->search_last_discrepancy_ = last_zero; + if (last_zero == 0) { + this->search_last_device_flag_ = true; + } + this->search_address_ = address; + + return address; +} + +} // namespace esphome::ds248x diff --git a/esphome/components/ds248x/ds248x_one_wire_bus.h b/esphome/components/ds248x/ds248x_one_wire_bus.h new file mode 100644 index 0000000000..0591796d60 --- /dev/null +++ b/esphome/components/ds248x/ds248x_one_wire_bus.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/one_wire/one_wire_bus.h" + +namespace esphome::ds248x { + +class DS248xComponent; + +/** + * @brief OneWireBus implementation for DS248x I2C-to-1-Wire bridges. + * + * This class wraps the DS248xComponent to provide the one_wire::OneWireBus interface, + * enabling compatibility with all existing 1-Wire device components (dallas_temp, etc.). + * + * For DS2482-800, multiple instances of this class can be created (one per channel). + * For DS2482-100/DS2484, a single instance is used. + */ +class DS248xOneWireBus : public one_wire::OneWireBus, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS - 1.0f; } + + /// Set the parent DS248x component + void set_parent(DS248xComponent *parent) { this->parent_ = parent; } + + /// Set the 1-Wire channel (0-7, only relevant for DS2482-800) + void set_channel(uint8_t channel) { this->channel_ = channel; } + + /// Get the channel number + uint8_t get_channel() const { return this->channel_; } + + // OneWireBus interface implementation + int reset_int() override; + void write8(uint8_t val) override; + void write64(uint64_t val) override; + uint8_t read8() override; + uint64_t read64() override; + + protected: + void reset_search() override; + uint64_t search_int() override; + + /// Select the channel on the DS248x before any 1-Wire operation + bool ensure_channel_(); + + DS248xComponent *parent_{nullptr}; + uint8_t channel_{0}; + + // Search state + uint64_t search_address_{0}; + uint8_t search_last_discrepancy_{0}; + bool search_last_device_flag_{false}; +}; + +} // namespace esphome::ds248x diff --git a/esphome/components/ds248x/one_wire.py b/esphome/components/ds248x/one_wire.py new file mode 100644 index 0000000000..19861eae36 --- /dev/null +++ b/esphome/components/ds248x/one_wire.py @@ -0,0 +1,56 @@ +"""DS248x 1-Wire Bus Platform. + +This platform creates one_wire bus instances backed by a DS248x I2C-to-1-Wire bridge. +It supports DS2482-100/101 (single channel), DS2482-800 (8 channels), and DS2484 (single channel). + +For multi-channel devices (DS2482-800), create one platform entry per channel. +Each entry becomes a separate one_wire bus that can be used by dallas_temp and other 1-Wire devices. +""" + +from esphome import final_validate as fv +import esphome.codegen as cg +from esphome.components.one_wire import OneWireBus +import esphome.config_validation as cv +from esphome.const import CONF_CHANNEL, CONF_ID + +from . import CONF_DS248X_ID, DS248xComponent, ds248x_ns, get_channel_count + +CODEOWNERS = ["@tomwellnitz"] +DEPENDENCIES = ["ds248x"] + +DS248xOneWireBus = ds248x_ns.class_("DS248xOneWireBus", OneWireBus, cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DS248xOneWireBus), + cv.GenerateID(CONF_DS248X_ID): cv.use_id(DS248xComponent), + cv.Optional(CONF_CHANNEL, default=0): cv.int_range(min=0, max=7), + } +).extend(cv.COMPONENT_SCHEMA) + + +def _final_validate(config): + """Validate that the channel is within the parent's channel count.""" + fconf = fv.full_config.get() + path = fconf.get_path_for_id(config[CONF_DS248X_ID])[:-1] + parent_config = fconf.get_config_for_path(path) + channel_count = get_channel_count(parent_config) + channel = config[CONF_CHANNEL] + + if channel >= channel_count: + raise cv.Invalid( + f"Channel {channel} is invalid for DS248x with {channel_count} channel(s). " + f"Valid range: 0-{channel_count - 1}" + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + parent = await cg.get_variable(config[CONF_DS248X_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/emc2101/emc2101.cpp b/esphome/components/emc2101/emc2101.cpp index 464f49fe51..f46082f5e7 100644 --- a/esphome/components/emc2101/emc2101.cpp +++ b/esphome/components/emc2101/emc2101.cpp @@ -145,9 +145,9 @@ float Emc2101Component::get_external_temperature() { return NAN; } - // join msb and lsb (5 least significant bits are not used) - uint16_t raw = (msb << 8 | lsb) >> 5; - return raw * 0.125; + // join msb and lsb (5 least significant bits are not used); msb is signed, so read as int16_t + int16_t raw = static_cast((msb << 8) | lsb) >> 5; + return raw * 0.125f; } float Emc2101Component::get_speed() { diff --git a/esphome/components/epaper_spi/colorconv.h b/esphome/components/epaper_spi/colorconv.h index d4ffd034a1..7b7c48c0b0 100644 --- a/esphome/components/epaper_spi/colorconv.h +++ b/esphome/components/epaper_spi/colorconv.h @@ -81,4 +81,134 @@ constexpr NATIVE_COLOR color_to_bwr(Color color, NATIVE_COLOR hw_black, NATIVE_C return color_to_bwyr(color, hw_black, hw_white, /*hw_yellow=*/hw_white, hw_red); } +/** Map RGB color to discrete BWYRGB hex 6 color key + * + * Divides the RGB cube into 8 corners by which components are "on" (over 128), same as + * color_to_bwyr, but also resolves the green and blue corners instead of folding them into + * white/black. + * + * @tparam NATIVE_COLOR Type of native hardware color values + * @param color RGB color to convert from + * @param hw_black Native value for black + * @param hw_white Native value for white + * @param hw_yellow Native value for yellow + * @param hw_red Native value for red + * @param hw_green Native value for green + * @param hw_blue Native value for blue + * @return Converted native hardware color value + * @internal Constexpr. Does not depend on side effects ("pure"). + */ +template +constexpr NATIVE_COLOR color_to_bwyrgb(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, + NATIVE_COLOR hw_yellow, NATIVE_COLOR hw_red, NATIVE_COLOR hw_green, + NATIVE_COLOR hw_blue) { + const auto [min_rgb, max_rgb] = std::minmax({color.r, color.g, color.b}); + + if ((max_rgb - min_rgb) < COLORCONV_GRAY_THRESHOLD) { + if ((static_cast(color.r) + color.g + color.b) > 382) { + return hw_white; + } + return hw_black; + } + + const bool r_on = (color.r > 128); + const bool g_on = (color.g > 128); + const bool b_on = (color.b > 128); + + if (r_on && g_on && !b_on) { + return hw_yellow; + } + if (r_on && !g_on && !b_on) { + return hw_red; + } + if (!r_on && g_on && !b_on) { + return hw_green; + } + if (!r_on && !g_on && b_on) { + return hw_blue; + } + // Handle "impure" colors (cyan, magenta) by folding into the closest primary. + if (!r_on && g_on && b_on) { + return hw_green; // cyan + } + if (r_on && !g_on) { + return hw_red; // magenta + } + if (r_on) { + // All high (but not gray) -> white + return hw_white; + } + // !r_on && !g_on && !b_on + // All low (but not gray) -> black + return hw_black; +} + +/** Map RGB color to discrete BWYRGBO hex 7 color key + * + * Same corner logic as color_to_bwyrgb, except the red/yellow corner is split three ways + * instead of two, for panels with a dedicated orange ink. + * + * @tparam NATIVE_COLOR Type of native hardware color values + * @param color RGB color to convert from + * @param hw_black Native value for black + * @param hw_white Native value for white + * @param hw_yellow Native value for yellow + * @param hw_red Native value for red + * @param hw_green Native value for green + * @param hw_blue Native value for blue + * @param hw_orange Native value for orange + * @return Converted native hardware color value + * @internal Constexpr. Does not depend on side effects ("pure"). + */ +template +constexpr NATIVE_COLOR color_to_bwyrgbo(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, + NATIVE_COLOR hw_yellow, NATIVE_COLOR hw_red, NATIVE_COLOR hw_green, + NATIVE_COLOR hw_blue, NATIVE_COLOR hw_orange) { + const auto [min_rgb, max_rgb] = std::minmax({color.r, color.g, color.b}); + + if ((max_rgb - min_rgb) < COLORCONV_GRAY_THRESHOLD) { + if ((static_cast(color.r) + color.g + color.b) > 382) { + return hw_white; + } + return hw_black; + } + + const bool r_on = (color.r > 128); + const bool g_on = (color.g > 128); + const bool b_on = (color.b > 128); + + if (r_on && !b_on) { + // Between red and yellow: split the gradient in three instead of two, since this panel has a + // dedicated orange ink. Named orange (e.g. 0xFFA500) has g close to the midpoint, so the + // plain g_on (>128) threshold used by color_to_bwyrgb can't tell it apart from yellow. + if (color.g > 170) { + return hw_yellow; + } + if (color.g > 85) { + return hw_orange; + } + return hw_red; + } + if (!r_on && g_on && !b_on) { + return hw_green; + } + if (!r_on && !g_on && b_on) { + return hw_blue; + } + // Handle "impure" colors (cyan, magenta) by folding into the closest primary. + if (!r_on && g_on && b_on) { + return hw_green; // cyan + } + if (r_on && !g_on) { + return hw_red; // magenta + } + if (r_on) { + // All high (but not gray) -> white + return hw_white; + } + // !r_on && !g_on && !b_on + // All low (but not gray) -> black + return hw_black; +} + } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index ce28fb0d67..0b82850f1e 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -112,6 +112,7 @@ def model_schema(config): cv.positive_time_period_milliseconds, cv.Range(max=core.TimePeriod(milliseconds=500)), ), + **model.get_config_options(), } ) @@ -198,6 +199,7 @@ async def to_code(config): ) await display.register_display(var, config) + config = await model.to_code(var, config) await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) diff --git a/esphome/components/epaper_spi/epaper_spi_4bpp.cpp b/esphome/components/epaper_spi/epaper_spi_4bpp.cpp new file mode 100644 index 0000000000..820dc3c77a --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_4bpp.cpp @@ -0,0 +1,82 @@ +#include "epaper_spi_4bpp.h" + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_spi.4bpp"; + +void EPaper4bpp::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + EPaperBase::fill(color); + return; + } + + auto pixel_color = this->color_to_native(color); + + // We store 2 pixels per byte + this->buffer_.fill(pixel_color + (pixel_color << 4)); + + // Whole buffer just changed; mark the entire canvas dirty. + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->width_; + this->y_high_ = this->height_; +} + +void EPaper4bpp::clear() { + // clear buffer to white, just like real paper. + this->fill(COLOR_ON); +} + +void HOT EPaper4bpp::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + auto pixel_bits = this->color_to_native(color); + uint32_t pixel_position = x + y * this->get_width_internal(); + uint32_t byte_position = pixel_position / 2; + auto original = this->buffer_[byte_position]; + if ((pixel_position & 1) != 0) { + this->buffer_[byte_position] = (original & 0xF0) | pixel_bits; + } else { + this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4); + } +} + +bool HOT EPaper4bpp::transfer_data() { + const uint32_t start_time = App.get_loop_component_start_time(); + const size_t buffer_length = this->buffer_length_; + if (this->current_data_index_ == 0) { + this->command(CMD_TRANSFER_DATA); + } + + size_t buf_idx = 0; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + while (this->current_data_index_ != buffer_length) { + bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; + + if (buf_idx == sizeof bytes_to_send) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->disable(); + ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis()); + buf_idx = 0; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + return false; + } + } + } + // Finished the entire dataset + if (buf_idx != 0) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->disable(); + } + this->current_data_index_ = 0; + return true; +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_4bpp.h b/esphome/components/epaper_spi/epaper_spi_4bpp.h new file mode 100644 index 0000000000..6eebe24c91 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_4bpp.h @@ -0,0 +1,35 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * Intermediate base for panels with a 4-bit-per-pixel native buffer layout (2 pixels per byte). + * + * Owns buffer sizing, fill()/clear()/draw_pixel_at() and the chunked SPI transfer loop shared by + * this family of controllers. Concrete subclasses supply only their RGB -> 4-bit color mapping via + * color_to_native() plus their IC-specific power/refresh/sleep command sequences. + */ +class EPaper4bpp : public EPaperBase { + public: + EPaper4bpp(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) { + this->buffer_length_ = width * height / 2; // 2 pixels per byte + } + + void fill(Color color) override; + void clear() override; + + protected: + void draw_pixel_at(int x, int y, Color color) override; + bool transfer_data() override; + + /// Map an RGB color to this panel's native 4-bit color key (low nibble). + virtual uint8_t color_to_native(Color color) = 0; + + static constexpr uint8_t CMD_TRANSFER_DATA = 0x10; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_inkplate6color.cpp b/esphome/components/epaper_spi/epaper_spi_inkplate6color.cpp new file mode 100644 index 0000000000..b81542b6c1 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_inkplate6color.cpp @@ -0,0 +1,47 @@ +// Reference: https://github.com/SolderedElectronics/Inkplate-Arduino-library (src/boards/Inkplate6COLOR) + +#include "epaper_spi_inkplate6color.h" +#include "colorconv.h" + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_spi.inkplate6color"; + +// Native hardware color codes for this panel's 4-bit color values. +enum Inkplate6ColorHex : uint8_t { + BLACK = 0, + WHITE = 1, + GREEN = 2, + BLUE = 3, + RED = 4, + YELLOW = 5, + ORANGE = 6, +}; + +uint8_t EPaperInkplate6Color::color_to_native(Color color) { + return color_to_bwyrgbo(color, BLACK, WHITE, YELLOW, RED, GREEN, BLUE, ORANGE); +} + +void EPaperInkplate6Color::power_on() { + ESP_LOGV(TAG, "Power on"); + this->command(0x04); +} + +void EPaperInkplate6Color::power_off() { + ESP_LOGV(TAG, "Power off"); + this->command(0x02); +} + +void EPaperInkplate6Color::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh"); // full refresh only; partial is unused + this->cmd_data(0x12, {0x00}); +} + +void EPaperInkplate6Color::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + this->cmd_data(0x07, {0xA5}); +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_inkplate6color.h b/esphome/components/epaper_spi/epaper_spi_inkplate6color.h new file mode 100644 index 0000000000..8b00552ff8 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_inkplate6color.h @@ -0,0 +1,22 @@ +#pragma once + +#include "epaper_spi_4bpp.h" + +namespace esphome::epaper_spi { + +// Soldered Inkplate 6COLOR: 600x448 7-color (black/white/green/blue/red/yellow/orange) e-paper, +// UC8159-family controller. +class EPaperInkplate6Color final : public EPaper4bpp { + public: + using EPaper4bpp::EPaper4bpp; + + protected: + uint8_t color_to_native(Color color) override; + + void refresh_screen(bool partial) override; + void power_on() override; + void power_off() override; + void deep_sleep() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_mono.cpp b/esphome/components/epaper_spi/epaper_spi_mono.cpp index ee117304c4..fffb2b5e84 100644 --- a/esphome/components/epaper_spi/epaper_spi_mono.cpp +++ b/esphome/components/epaper_spi/epaper_spi_mono.cpp @@ -14,10 +14,9 @@ void EPaperMono::refresh_screen(bool partial) { } void EPaperMono::deep_sleep() { - ESP_LOGV(TAG, "Deep sleep"); - if (this->is_using_partial_update_()) { - this->cmd_data(0x10, {0x00}); // sleep in power on mode - } else { + // Deep sleep loses RAM so cannot be used with partial update + if (!this->is_using_partial_update_()) { + ESP_LOGV(TAG, "Deep sleep"); this->cmd_data(0x10, {0x03}); // deep sleep } } diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp index 1ef2dd12c3..f47d37d550 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -1,76 +1,23 @@ #include "epaper_spi_spectra_e6.h" - -#include +#include "colorconv.h" #include "esphome/core/log.h" namespace esphome::epaper_spi { static constexpr const char *const TAG = "epaper_spi.6c"; -static constexpr unsigned char GRAY_THRESHOLD = 50; -enum E6Color { - BLACK, - WHITE, - YELLOW, - RED, - SKIP_1, - BLUE, - GREEN, - CYAN, - SKIP_2, +// Native hardware color codes for this panel's 4-bit color values. +enum E6Color : uint8_t { + BLACK = 0, + WHITE = 1, + YELLOW = 2, + RED = 3, + BLUE = 5, + GREEN = 6, }; -static uint8_t color_to_hex(Color color) { - // --- Step 1: Check for Grayscale (Black or White) --- - // We define "grayscale" as a color where the min and max components - // are close to each other. - unsigned char max_rgb = std::max({color.r, color.g, color.b}); - unsigned char min_rgb = std::min({color.r, color.g, color.b}); - - if ((max_rgb - min_rgb) < GRAY_THRESHOLD) { - // It's a shade of gray. Map to BLACK or WHITE. - // We split the luminance at the halfway point (382 = (255*3)/2) - if ((static_cast(color.r) + color.g + color.b) > 382) { - return WHITE; - } - return BLACK; - } - // --- Step 2: Check for Primary/Secondary Colors --- - // If it's not gray, it's a color. We check which components are - // "on" (over 128) vs "off". This divides the RGB cube into 8 corners. - bool r_on = (color.r > 128); - bool g_on = (color.g > 128); - bool b_on = (color.b > 128); - - if (r_on && g_on && !b_on) { - return YELLOW; - } - if (r_on && !g_on && !b_on) { - return RED; - } - if (!r_on && g_on && !b_on) { - return GREEN; - } - if (!r_on && !g_on && b_on) { - return BLUE; - } - // Handle "impure" colors (Cyan, Magenta) - if (!r_on && g_on && b_on) { - // Cyan (G+B) -> Closest is Green or Blue. Pick Green. - return GREEN; - } - if (r_on && !g_on) { - // Magenta (R+B) -> Closest is Red or Blue. Pick Red. - return RED; - } - // Handle the remaining corners (White-ish, Black-ish) - if (r_on) { - // All high (but not gray) -> White - return WHITE; - } - // !r_on && !g_on && !b_on - // All low (but not gray) -> Black - return BLACK; +uint8_t EPaperSpectraE6::color_to_native(Color color) { + return color_to_bwyrgb(color, BLACK, WHITE, YELLOW, RED, GREEN, BLUE); } void EPaperSpectraE6::power_on() { @@ -92,71 +39,4 @@ void EPaperSpectraE6::deep_sleep() { ESP_LOGV(TAG, "Deep sleep"); this->cmd_data(0x07, {0xA5}); } - -void EPaperSpectraE6::fill(Color color) { - // If clipping is active, fall back to base implementation - if (this->get_clipping().is_set()) { - EPaperBase::fill(color); - return; - } - - auto pixel_color = color_to_hex(color); - - // We store 2 pixels per byte - this->buffer_.fill(pixel_color + (pixel_color << 4)); -} - -void EPaperSpectraE6::clear() { - // clear buffer to white, just like real paper. - this->fill(COLOR_ON); -} - -void HOT EPaperSpectraE6::draw_pixel_at(int x, int y, Color color) { - if (!this->rotate_coordinates_(x, y)) - return; - auto pixel_bits = color_to_hex(color); - uint32_t pixel_position = x + y * this->get_width_internal(); - uint32_t byte_position = pixel_position / 2; - auto original = this->buffer_[byte_position]; - if ((pixel_position & 1) != 0) { - this->buffer_[byte_position] = (original & 0xF0) | pixel_bits; - } else { - this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4); - } -} - -bool HOT EPaperSpectraE6::transfer_data() { - const uint32_t start_time = App.get_loop_component_start_time(); - const size_t buffer_length = this->buffer_length_; - if (this->current_data_index_ == 0) { - this->command(0x10); - } - - size_t buf_idx = 0; - uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; - while (this->current_data_index_ != buffer_length) { - bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; - - if (buf_idx == sizeof bytes_to_send) { - this->start_data_(); - this->write_array(bytes_to_send, buf_idx); - this->disable(); - ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis()); - buf_idx = 0; - - if (millis() - start_time > MAX_TRANSFER_TIME) { - // Let the main loop run and come back next loop - return false; - } - } - } - // Finished the entire dataset - if (buf_idx != 0) { - this->start_data_(); - this->write_array(bytes_to_send, buf_idx); - this->disable(); - } - this->current_data_index_ = 0; - return true; -} } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h index 9c251068af..e1e3d3d763 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -1,28 +1,20 @@ #pragma once -#include "epaper_spi.h" +#include "epaper_spi_4bpp.h" namespace esphome::epaper_spi { -class EPaperSpectraE6 final : public EPaperBase { +class EPaperSpectraE6 final : public EPaper4bpp { public: - EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, - size_t init_sequence_length) - : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) { - this->buffer_length_ = width * height / 2; // 2 pixels per byte - } - - void fill(Color color) override; - void clear() override; + using EPaper4bpp::EPaper4bpp; protected: + uint8_t color_to_native(Color color) override; + void refresh_screen(bool partial) override; void power_on() override; void power_off() override; void deep_sleep() override; - void draw_pixel_at(int x, int y, Color color) override; - - bool transfer_data() override; }; } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_t133a01.cpp b/esphome/components/epaper_spi/epaper_spi_t133a01.cpp new file mode 100644 index 0000000000..5735333761 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_t133a01.cpp @@ -0,0 +1,367 @@ +#include "epaper_spi_t133a01.h" + +#include + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_spi.t133a01"; + +// Color indices used in the 4bpp buffer (sprite-side) +// These MUST match the Arduino GFX TFT_eSPI.h color definitions and +// the remap_color()/COLOR_GET mapping: +// 0x0F=BLACK, 0x00=WHITE, 0x02=GREEN, 0x06=RED, 0x0B=YELLOW, 0x0D=BLUE +static constexpr uint8_t T133A01_BLACK = 0x0F; +static constexpr uint8_t T133A01_WHITE = 0x00; +static constexpr uint8_t T133A01_GREEN = 0x02; +static constexpr uint8_t T133A01_RED = 0x06; +static constexpr uint8_t T133A01_YELLOW = 0x0B; +static constexpr uint8_t T133A01_BLUE = 0x0D; + +// T133A01 register addresses +static constexpr uint8_t R00_PSR = 0x00; +static constexpr uint8_t R01_PWR = 0x01; +static constexpr uint8_t R02_POF = 0x02; +static constexpr uint8_t R04_PON = 0x04; +static constexpr uint8_t R05_BTST_N = 0x05; +static constexpr uint8_t R06_BTST_P = 0x06; +static constexpr uint8_t R10_DTM = 0x10; +static constexpr uint8_t R12_DRF = 0x12; +static constexpr uint8_t R50_CDI = 0x50; +static constexpr uint8_t R61_TRES = 0x61; +static constexpr uint8_t RA5_DCDC = 0xA5; +static constexpr uint8_t RE0_CCSET = 0xE0; +static constexpr uint8_t RE3_PWS = 0xE3; + +/** + * COLOR_GET remap table from T133A01_Defines.h. + * Translates 4bpp sprite color index to the hardware pixel encoding. + * Sprite: 0x0F=BLACK 0x00=WHITE 0x02=GREEN 0x06=RED 0x0B=YELLOW 0x0D=BLUE + * HW: 0x00=BLACK 0x01=WHITE 0x06=GREEN 0x03=RED 0x02=YELLOW 0x05=BLUE + */ +uint8_t EPaperT133A01::remap_color(uint8_t index) { + switch (index & 0x0F) { + case 0x0F: + return 0x00; // Black + case 0x00: + return 0x01; // White + case 0x02: + return 0x06; // Green + case 0x06: + return 0x03; // Red + case 0x0B: + return 0x02; // Yellow + case 0x0D: + return 0x05; // Blue + default: + return 0x01; // White fallback + } +} + +/** + * Map an ESPHome Color to a 4-bit sprite color index. + * Index values match the Arduino GFX TFT_eSPI color definitions: + * 0x00=WHITE, 0x02=GREEN, 0x06=RED, 0x0B=YELLOW, 0x0D=BLUE, 0x0F=BLACK + */ +uint8_t EPaperT133A01::color_to_index(Color color) { + unsigned char max_rgb = std::max({color.r, color.g, color.b}); + unsigned char min_rgb = std::min({color.r, color.g, color.b}); + + // Check for grayscale + if ((max_rgb - min_rgb) < 50) { + if ((static_cast(color.r) + color.g + color.b) > 382) { + return T133A01_WHITE; + } + return T133A01_BLACK; + } + + bool r_on = (color.r > 128); + bool g_on = (color.g > 128); + bool b_on = (color.b > 128); + + if (r_on && g_on && !b_on) + return T133A01_YELLOW; + if (r_on && !g_on && !b_on) + return T133A01_RED; + if (!r_on && g_on && !b_on) + return T133A01_GREEN; + if (!r_on && !g_on && b_on) + return T133A01_BLUE; + // Handle mixed colors: map to nearest primary + if (!r_on && g_on && b_on) + return T133A01_GREEN; // Cyan -> Green + if (r_on && !g_on) + return T133A01_RED; // Magenta -> Red + if (r_on) + return T133A01_WHITE; + return T133A01_BLACK; +} + +void EPaperT133A01::setup() { + // Base setup initialises the buffer, the standard pins and the SPI bus. + EPaperBase::setup(); + + // Both chip-selects are driven directly by this driver (the dual-CS + // protocol needs CS held HIGH while CS1 receives data, which the SPI + // bus cannot do). Start both deselected (HIGH). + this->cs_pin_->setup(); + this->cs_pin_->digital_write(true); + this->cs1_pin_->setup(); + this->cs1_pin_->digital_write(true); +} + +bool EPaperT133A01::reset() { + for (auto *enable_pin : this->enable_pins_) { + enable_pin->digital_write(true); + } + if (this->reset_pin_ != nullptr) { + if (this->state_ == EPaperState::RESET) { + this->reset_pin_->digital_write(false); + return false; + } + this->reset_pin_->digital_write(true); + } + return true; +} + +/** + * Initialise the T133A01 display. + * + * The init sequence uses a mix of CS and CS1 commands as per the Arduino driver. + * The base class init_sequence is NOT used for T133A01 because the dual-CS + * protocol requires per-command routing. + */ +bool EPaperT133A01::initialise(bool partial) { + // Init sequence mirrors the Arduino GFX library's EPD_INIT() macro + // (T133A01_Defines.h). Commands routed to CS only leave CS1 deselected; + // commands routed to both controllers assert CS and CS1 together. + + // 0x74 - panel config (CS only) + this->write_command_(0x74, {0x00, 0x0C, 0x0C, 0xD9, 0xDD, 0xDD, 0x15, 0x15, 0x55}, true, false); + delay(10); + + // 0xF0 - panel config (CS + CS1) + this->write_command_(0xF0, {0x49, 0x55, 0x13, 0x5D, 0x05, 0x10}, true, true); + delay(10); + + // PSR - Panel Setting Register (CS + CS1) + this->write_command_(0x00, {0xDF, 0x69}, true, true); + delay(10); + + // DCDC (CS only) + this->write_command_(RA5_DCDC, {0x44, 0x54, 0x00}, true, false); + delay(10); + + // CDI (CS + CS1) + this->write_command_(R50_CDI, {0x37}, true, true); + delay(10); + + // 0x60 (CS + CS1) + this->write_command_(0x60, {0x03, 0x03}, true, true); + delay(10); + + // 0x86 (CS + CS1) + this->write_command_(0x86, {0x10}, true, true); + delay(10); + + // PWS - Phase Width Setting (CS + CS1) + this->write_command_(RE3_PWS, {0x22}, true, true); + delay(10); + + // TRES - Resolution Setting (CS + CS1). + // With width=1200, height=1600: first word = width = 1200, second word = height/2 = 800. + this->write_command_(R61_TRES, + {(uint8_t) (this->width_ >> 8), (uint8_t) (this->width_ & 0xFF), + (uint8_t) ((this->height_ / 2) >> 8), (uint8_t) ((this->height_ / 2) & 0xFF)}, + true, true); + delay(10); + + // PWR - Power Setting (CS only) + this->write_command_(R01_PWR, {0x0F, 0x00, 0x28, 0x2C, 0x28, 0x38}, true, false); + delay(10); + + // 0xB6 (CS only) + this->write_command_(0xB6, {0x07}, true, false); + delay(10); + + // BTST_P (CS only) + this->write_command_(R06_BTST_P, {0xE0, 0x20}, true, false); + delay(10); + + // 0xB7 (CS only) + this->write_command_(0xB7, {0x01}, true, false); + delay(10); + + // BTST_N (CS only) + this->write_command_(R05_BTST_N, {0xE0, 0x20}, true, false); + delay(10); + + // 0xB0 (CS only) + this->write_command_(0xB0, {0x01}, true, false); + delay(10); + + // 0xB1 (CS only) + this->write_command_(0xB1, {0x02}, true, false); + delay(10); + + return true; +} + +void EPaperT133A01::write_command_(uint8_t command, const uint8_t *data, size_t length, bool use_cs, bool use_cs1) { + ESP_LOGV(TAG, "Command: 0x%02X, Length: %u, CS: %d, CS1: %d", command, (unsigned) length, use_cs, use_cs1); + // Chip-selects are active-low: assert the requested controllers. + this->cs_pin_->digital_write(!use_cs); + this->cs1_pin_->digital_write(!use_cs1); + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(command); + if (length > 0) { + this->dc_pin_->digital_write(true); + this->write_array(data, length); + } + this->disable(); + this->cs_pin_->digital_write(true); + this->cs1_pin_->digital_write(true); +} + +void EPaperT133A01::fill(Color color) { + if (this->get_clipping().is_set()) { + EPaperBase::fill(color); + return; + } + auto pixel_color = color_to_index(color); + this->buffer_.fill(pixel_color + (pixel_color << 4)); +} + +void EPaperT133A01::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + auto pixel_bits = color_to_index(color); + uint32_t pixel_position = x + y * this->get_width_internal(); + uint32_t byte_position = pixel_position / 2; + auto original = this->buffer_[byte_position]; + if ((pixel_position & 1) != 0) { + this->buffer_[byte_position] = (original & 0xF0) | pixel_bits; + } else { + this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4); + } +} + +void EPaperT133A01::power_on() { + ESP_LOGV(TAG, "Power on"); + this->write_command_(R04_PON, true, true); +} + +void EPaperT133A01::power_off() { + ESP_LOGV(TAG, "Power off"); + this->write_command_(R02_POF, {0x00}, true, true); +} + +void EPaperT133A01::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh screen"); + // Display Refresh + this->write_command_(R12_DRF, {0x01}, true, true); +} + +void EPaperT133A01::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + this->write_command_(0x07, {0xA5}, true, true); +} + +bool HOT EPaperT133A01::transfer_data() { + const uint32_t start_time = millis(); + const uint16_t bytes_per_half_row = this->width_ / 4; + const uint16_t total_rows = this->height_; + const uint16_t bytes_per_row = this->width_ / 2; + uint8_t line_data[400] = {}; + + size_t half = this->current_data_index_; + + // --- CCSET: select color set before data transfer (CS + CS1) --- + if (half == 0) { + this->write_command_(RE0_CCSET, {0x01}, true, true); + this->wait_for_idle_(true); + delay(10); + } + + // --- CS phase: left half of each row via CS --- + // T133A01 requires CS to stay LOW for the ENTIRE DTM data stream. + // Toggling CS between chunks resets the controller's data pointer, + // causing only the last chunk to be retained. Keep CS asserted + // across timeout boundaries by NOT deselecting on yield. + if (half < total_rows) { + if (half == 0) { + this->cs_pin_->digital_write(false); // select CS + this->cs1_pin_->digital_write(true); // deselect CS1 + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(R10_DTM); + this->dc_pin_->digital_write(true); + } + + while (half < total_rows) { + size_t buf_offset = half * bytes_per_row; + for (uint16_t col = 0; col < bytes_per_half_row; col++) { + uint8_t b = this->buffer_[buf_offset + col]; + line_data[col] = (remap_color(b >> 4) << 4) | remap_color(b & 0x0F); + } + this->write_array(line_data, bytes_per_half_row); + half++; + this->current_data_index_ = half; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + return false; + } + } + ESP_LOGD(TAG, "CS phase done"); + this->disable(); + this->cs_pin_->digital_write(true); // deselect CS + } + + // --- CS1 phase: right half of each row via CS1 --- + // Same continuous-transaction requirement as the CS phase. + // CS is held HIGH so only CS1 receives the data. + if (half >= total_rows && half < total_rows * 2) { + size_t cs1_row = half - total_rows; + + if (cs1_row == 0) { + this->cs_pin_->digital_write(true); // deselect CS + this->cs1_pin_->digital_write(false); // select CS1 + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(R10_DTM); + this->dc_pin_->digital_write(true); + } + + while (half < total_rows * 2) { + size_t row = half - total_rows; + size_t buf_offset = row * bytes_per_row + bytes_per_half_row; + for (uint16_t col = 0; col < bytes_per_half_row; col++) { + uint8_t b = this->buffer_[buf_offset + col]; + line_data[col] = (remap_color(b >> 4) << 4) | remap_color(b & 0x0F); + } + this->write_array(line_data, bytes_per_half_row); + half++; + this->current_data_index_ = half; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + return false; + } + } + ESP_LOGD(TAG, "CS1 phase done"); + this->disable(); + this->cs1_pin_->digital_write(true); // deselect CS1 + } + + this->current_data_index_ = 0; + return true; +} + +void EPaperT133A01::dump_config() { + EPaperBase::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_pin_); + LOG_PIN(" CS1 Pin: ", this->cs1_pin_); +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_t133a01.h b/esphome/components/epaper_spi/epaper_spi_t133a01.h new file mode 100644 index 0000000000..0d07fc03ae --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_t133a01.h @@ -0,0 +1,77 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * T133A01-based 6-color e-paper display driver. + * + * The T133A01 controller uses a dual-CS SPI architecture: + * - CS (primary): Controls the first half of pixel data transfer + * - CS1 (secondary): Controls panel commands (init, power, refresh) and + * the second half of pixel data transfer + * + * Color depth: 4 bits per pixel, supporting 6 colors: + * White, Green, Red, Yellow, Blue, Black + * + * Buffer layout: 2 pixels per byte (4bpp packed), total buffer size + * is width * height / 2 bytes. + */ +class EPaperT133A01 : public EPaperBase { + public: + EPaperT133A01(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) { + this->buffer_length_ = (size_t) width * height / 2; // 2 pixels per byte at 4bpp + } + + void set_cs_pins(GPIOPin *cs, GPIOPin *cs1) { + this->cs_pin_ = cs; + this->cs1_pin_ = cs1; + } + + void fill(Color color) override; + + void setup() override; + void dump_config() override; + void draw_pixel_at(int x, int y, Color color) override; + + protected: + bool reset() override; + bool initialise(bool partial) override; + void refresh_screen(bool partial) override; + void power_on() override; + void power_off() override; + void deep_sleep() override; + + bool transfer_data() override; + + /** + * Send a command (and optional data) selecting one or both controllers. + * Both chip-selects are active-low and managed directly by this driver. + * @param command The command byte to send + * @param data Optional pointer to data bytes to send after the command + * @param length Number of data bytes to send after the command + * @param use_cs assert CS (left controller) for this transaction + * @param use_cs1 assert CS1 (right controller) for this transaction + */ + void write_command_(uint8_t command, const uint8_t *data, size_t length, bool use_cs, bool use_cs1); + void write_command_(uint8_t command, std::initializer_list data, bool use_cs, bool use_cs1) { + this->write_command_(command, data.begin(), data.size(), use_cs, use_cs1); + } + void write_command_(uint8_t command, bool use_cs, bool use_cs1) { + this->write_command_(command, nullptr, 0, use_cs, use_cs1); + } + + /// Convert Color to 4-bit T133A01 color index + static uint8_t color_to_index(Color color); + + /// Apply COLOR_GET remap table to translate sprite indices to hardware values + static uint8_t remap_color(uint8_t index); + + GPIOPin *cs_pin_{nullptr}; + GPIOPin *cs1_pin_{nullptr}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/__init__.py b/esphome/components/epaper_spi/models/__init__.py index 3fcf3217ec..34e65061f3 100644 --- a/esphome/components/epaper_spi/models/__init__.py +++ b/esphome/components/epaper_spi/models/__init__.py @@ -2,16 +2,20 @@ from typing import Any, Self import esphome.config_validation as cv from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH +from esphome.cpp_generator import MockObj class EpaperModel: models: dict[str, Self] = {} + # Whether the driver manages chip-select itself instead of via the SPI bus. + manages_cs: bool = False + def __init__( self, name: str, class_name: str, - initsequence=None, + initsequence=(), **defaults, ): name = name.upper() @@ -35,6 +39,25 @@ class EpaperModel: def get_constructor_args(self, config) -> tuple: return () + def get_config_options(self) -> dict: + """ + Return model-specific configuration schema options. + The base implementation adds nothing; specific models override this to + declare extra options without cluttering the shared schema. + :return: A mapping suitable for cv.Schema.extend() + """ + return {} + + async def to_code(self, var: MockObj, config: dict) -> dict: + """ + Generate model-specific code for the options added by add_options(). + The base implementation does nothing; specific models override this. + The config can be updated in place to add or remove options. + :param var: The component variable + :param config: The validated configuration + """ + return config + def get_dimensions(self, config) -> tuple[int, int]: if CONF_DIMENSIONS in config: # Explicit dimensions, just use as is diff --git a/esphome/components/epaper_spi/models/inkplate6color.py b/esphome/components/epaper_spi/models/inkplate6color.py new file mode 100644 index 0000000000..33fecab848 --- /dev/null +++ b/esphome/components/epaper_spi/models/inkplate6color.py @@ -0,0 +1,57 @@ +# Reference: https://github.com/SolderedElectronics/Inkplate-Arduino-library (src/boards/Inkplate6COLOR) + +from esphome.components.mipi import delay + +from . import EpaperModel + + +class Inkplate6ColorModel(EpaperModel): + def __init__(self, name, class_name="EPaperInkplate6Color", **kwargs): + super().__init__(name, class_name, **kwargs) + + # fmt: off + def get_init_sequence(self, config: dict): + width, height = self.get_dimensions(config) + return ( + (0x00, 0xEF, 0x08,), # panel setting + (0x01, 0x37, 0x00, 0x05, 0x05,), # power setting + (0x03, 0x00,), # power off sequence setting + (0x06, 0xC7, 0xC7, 0x1D,), # booster soft start + (0x41, 0x00,), # temperature sensor enable + (0x50, 0x37,), # VCOM and data interval + (0x60, 0x20,), # TCON setting + (0x61, width // 256, width % 256, height // 256, height % 256,), # resolution set + (0xE3, 0xAA,), # power saving + delay(100), + (0x50, 0x37,), # VCOM and data interval, resent once the power-saving setting settles + ) + + +# Native orientation is landscape (600x448). +inkplate6color = Inkplate6ColorModel( + "inkplate6color", + width=600, + height=448, + # Vendor library drives the panel at 2MHz; the controller doesn't reliably support faster rates. + data_rate="2MHz", + # Vendor library waits 200ms after releasing reset before talking to the panel. + reset_duration="200ms", + # A full 7-color refresh takes tens of seconds; disallow faster updates to avoid FSM update loops. + minimum_update_interval="30s", + # Panel's native buffer orientation is rotated 180 degrees relative to the logical + # rotation=0 orientation; confirmed on real hardware. + mirror_x=True, + mirror_y=True, + # Default GPIO pins for the on-board Inkplate 6COLOR wiring. + reset_pin=19, + dc_pin=33, + cs_pin=27, + busy_pin={ + "number": 32, + "inverted": True, # hardware: LOW=busy, HIGH=idle + "mode": { + "input": True, + "pullup": True, + }, + }, +) diff --git a/esphome/components/epaper_spi/models/t133a01.py b/esphome/components/epaper_spi/models/t133a01.py new file mode 100644 index 0000000000..0a57b95795 --- /dev/null +++ b/esphome/components/epaper_spi/models/t133a01.py @@ -0,0 +1,71 @@ +"""T133A01-based e-paper displays. + +The T133A01 is a 6-color e-paper controller IC that drives large panels +(1200x1600 portrait). It uses a dual-CS SPI architecture where CS +controls one half of the pixel data and CS1 controls the other half, +as well as panel-level commands (power on, refresh, power off). + +Supported models: +- Seeed-reTerminal-E1004: 1200x1600 pixels, 6-color (T133A01 panel) +""" + +from esphome import pins +import esphome.codegen as cg +from esphome.const import CONF_CS_PIN +from esphome.cpp_generator import MockObj + +from . import EpaperModel + +CONF_CS1_PIN = "cs1_pin" + + +class T133A01Model(EpaperModel): + """EpaperModel subclass for T133A01-based 6-color e-paper displays.""" + + # The driver drives CS and CS1 directly for the dual-CS protocol. + manages_cs = True + + def __init__(self, name, class_name="EPaperT133A01", **defaults): + super().__init__(name, class_name, **defaults) + + def get_config_options(self) -> dict: + # CS1 is the second chip-select required by the dual-CS architecture. + # fallback=None makes it required unless the model provides a default. + return { + self.option(CONF_CS1_PIN, fallback=None): pins.gpio_output_pin_schema, + } + + async def to_code(self, var: MockObj, config: dict) -> dict: + cs = await cg.gpio_pin_expression(config[CONF_CS_PIN]) + cs1 = await cg.gpio_pin_expression(config[CONF_CS1_PIN]) + cg.add(var.set_cs_pins(cs, cs1)) + # Remove CS and CS1 from the config so that the base class doesn't try to handle them. + return {k: v for k, v in config.items() if k not in (CONF_CS_PIN, CONF_CS1_PIN)} + + +t133a01_base = T133A01Model( + "t133a01", + minimum_update_interval="30s", + data_rate="10MHz", +) + +# Seeed reTerminal E1004 - 13.3" 6-color e-paper (1200x1600, T133A01) +# Portrait orientation (1200 wide × 1600 tall), matching the Arduino +# Setup523 defines TFT_WIDTH=1200, TFT_HEIGHT=1600. +# CS and CS1 each receive half of each row's pixel data +# (300 bytes = 600 pixels per controller, for all 1600 rows). +Seeed_reTerminal_E1004 = t133a01_base.extend( + "Seeed-reTerminal-E1004", + width=1200, + height=1600, + cs_pin=10, + cs1_pin=2, + dc_pin=11, + reset_pin=38, + busy_pin={ + "number": 13, + "inverted": True, + "mode": {"input": True}, + }, + enable_pin=12, +) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a5528da672..6491b9a5e6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -11,6 +11,7 @@ from typing import Any from esphome import yaml_util import esphome.codegen as cg +from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION import esphome.config_validation as cv from esphome.const import ( CONF_ADVANCED, @@ -29,6 +30,7 @@ from esphome.const import ( CONF_PATH, CONF_PLATFORM_VERSION, CONF_PLATFORMIO_OPTIONS, + CONF_PROJECT, CONF_REF, CONF_SAFE_MODE, CONF_SIZE, @@ -107,7 +109,9 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" +CONF_KEY_ID = "key_id" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" +CONF_NVS_ENCRYPTION = "nvs_encryption" CONF_RELEASE = "release" CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification" CONF_SIGNING_KEY = "signing_key" @@ -165,6 +169,20 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = { VARIANT_ESP32, } +# NVS encryption (HMAC peripheral scheme) is only available on variants that +# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original +# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral +# should be added here. +NVS_ENCRYPTION_HMAC_VARIANTS = { + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32P4, +} + COMPILER_OPTIMIZATIONS = { "DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG", "NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE", @@ -796,14 +814,16 @@ def _is_framework_url(source: str) -> bool: # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(3, 3, 9), - "latest": cv.Version(3, 3, 9), - "dev": cv.Version(3, 3, 9), + "recommended": cv.Version(3, 3, 11), + "latest": cv.Version(3, 3, 11), + "dev": cv.Version(3, 3, 11), } ARDUINO_PLATFORM_VERSION_LOOKUP = { cv.Version( 4, 0, 0, "alpha1" ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", + cv.Version(3, 3, 11): cv.Version(55, 3, 311), + cv.Version(3, 3, 10): cv.Version(55, 3, 39), cv.Version(3, 3, 9): cv.Version(55, 3, 39), cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"), cv.Version(3, 3, 7): cv.Version(55, 3, 37), @@ -826,6 +846,8 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { cv.Version(4, 0, 0, "alpha1"): cv.Version(6, 0, 1), + cv.Version(3, 3, 11): cv.Version(5, 5, 5), + cv.Version(3, 3, 10): cv.Version(5, 5, 5), cv.Version(3, 3, 9): cv.Version(5, 5, 4), cv.Version(3, 3, 8): cv.Version(5, 5, 4), cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"), @@ -847,9 +869,9 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 4), - "latest": cv.Version(5, 5, 4), - "dev": cv.Version(5, 5, 4), + "recommended": cv.Version(5, 5, 5), + "latest": cv.Version(5, 5, 5), + "dev": cv.Version(5, 5, 5), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { @@ -859,6 +881,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { cv.Version( 6, 0, 0 ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", + cv.Version(5, 5, 5): cv.Version(55, 3, 311), cv.Version(5, 5, 4): cv.Version(55, 3, 39), cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), @@ -879,8 +902,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 39), - "latest": cv.Version(55, 3, 39), + "recommended": cv.Version(55, 3, 311), + "latest": cv.Version(55, 3, 311), "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } @@ -1008,7 +1031,9 @@ def _check_esp_idf_versions(config: ConfigType) -> ConfigType: def _validate_toolchain(value) -> Toolchain: - return Toolchain(cv.one_of(*(t.value for t in Toolchain), lower=True)(value)) + return Toolchain( + cv.one_of(Toolchain.PLATFORMIO, Toolchain.ESP_IDF, lower=True)(value) + ) def _resolve_toolchain(value: ConfigType) -> ConfigType: @@ -1098,12 +1123,131 @@ def _detect_variant(value): return value +def _ota_downgrade_protection_errors( + project_version: str | None, signed_ota_enabled: bool +) -> list[cv.Invalid]: + """Validate prerequisites for OTA downgrade protection. + + Called only when the feature is enabled. Returns a ``cv.Invalid`` for each + unmet requirement: a dotted-numeric project version (the firmware version + compared on-device) and signed OTA (so the embedded version cannot be + forged). + """ + path = [CONF_FRAMEWORK, CONF_ADVANCED, CONF_ENABLE_OTA_DOWNGRADE_PROTECTION] + errs: list[cv.Invalid] = [] + if not project_version: + errs.append( + cv.Invalid( + f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires a " + f"'{CONF_PROJECT}' with a '{CONF_VERSION}' to be set in the " + f"'{CONF_ESPHOME}' section; this version is the firmware version " + "compared during OTA.", + path=path, + ) + ) + elif not re.fullmatch(r"\d+(\.\d+)*", project_version): + # The on-device comparison parses dotted-numeric versions only. + errs.append( + cv.Invalid( + f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires the " + f"'{CONF_PROJECT}' '{CONF_VERSION}' to be dotted-numeric (such " + f"as '1.2.3'), got '{project_version}'.", + path=path, + ) + ) + if not signed_ota_enabled: + errs.append( + cv.Invalid( + f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires " + f"'{CONF_SIGNED_OTA_VERIFICATION}' to be enabled; without signed " + "OTA the embedded version cannot be trusted.", + path=path, + ) + ) + return errs + + +_SIGNED_OTA_VERIFICATION_SCHEMA = cv.Schema( + { + cv.Optional(CONF_SIGNING_KEY): cv.file_, + cv.Optional(CONF_VERIFICATION_KEY): cv.file_, + cv.Optional(CONF_SIGNING_SCHEME, default="rsa3072"): cv.one_of( + *SIGNING_SCHEMES, lower=True + ), + } +) + + +@schema_extractor("schema") +def _validate_signed_ota_verification(value): + if value is SCHEMA_EXTRACT: + # Expose the inner schema so the language-schema dumper can walk the + # signing_key / verification_key / signing_scheme options. + return _SIGNED_OTA_VERIFICATION_SCHEMA + if value is None: + # A bare `signed_ota_verification:` block is valid: the default V2 + # scheme needs no keys (verify externally-signed binaries). + value = {} + return _validate_signed_ota_keys(_SIGNED_OTA_VERIFICATION_SCHEMA(value)) + + +def _validate_signed_ota_keys(config: ConfigType) -> ConfigType: + """Validate the signing/verification key combination for the selected scheme. + + A verification key is only used by the Secure Boot V1 scheme (ecdsa_v1): + the public key is compiled into the app so it can verify externally-signed + images. ESP-IDF's CONFIG_SECURE_BOOT_VERIFICATION_KEY only takes effect + when the V1 ECDSA scheme is selected and binaries are not signed during + the build (see SECURE_BOOT_VERIFICATION_KEY in the bootloader Kconfig). + + The V2 schemes (rsa3072, ecdsa256) embed the public key in the signature + block appended to each image, so verifying externally-signed binaries + needs no key in the config at all -- omitting both keys selects that + external-signing mode. + """ + has_signing_key = CONF_SIGNING_KEY in config + has_verification_key = CONF_VERIFICATION_KEY in config + scheme = config[CONF_SIGNING_SCHEME] + if has_signing_key and has_verification_key: + raise cv.Invalid( + f"Provide at most one of '{CONF_SIGNING_KEY}' and " + f"'{CONF_VERIFICATION_KEY}', not both.", + path=[CONF_VERIFICATION_KEY], + ) + if scheme == "ecdsa_v1": + if not has_signing_key and not has_verification_key: + raise cv.Invalid( + f"Signing scheme 'ecdsa_v1' requires either '{CONF_SIGNING_KEY}' " + f"(to sign binaries during the build) or '{CONF_VERIFICATION_KEY}' " + f"(to verify binaries signed externally).", + path=[CONF_SIGNING_KEY], + ) + elif has_verification_key: + raise cv.Invalid( + f"'{CONF_VERIFICATION_KEY}' is only used with signing scheme " + f"'ecdsa_v1'. With '{scheme}' the public key is embedded in each " + f"image's signature block, so no key file is needed to verify " + f"externally-signed binaries: remove '{CONF_VERIFICATION_KEY}', and " + f"set '{CONF_SIGNING_KEY}' only if binaries should be signed during " + f"the build.", + path=[CONF_VERIFICATION_KEY], + ) + return config + + def final_validate(config): # Imported locally to avoid circular import issues from esphome.components.psram import DOMAIN as PSRAM_DOMAIN from .gpio import final_validate_pins + # Remove before 2027.2.0 + if CORE.using_toolchain_platformio: + _LOGGER.warning( + "The 'platformio' toolchain for ESP32 is deprecated and will be removed " + "in ESPHome 2027.2.0. Please use 'toolchain: esp-idf' instead." + ) + errs = [] conf_fw = config[CONF_FRAMEWORK] advanced = conf_fw[CONF_ADVANCED] @@ -1299,10 +1443,41 @@ def final_validate(config): ) else: _LOGGER.info( - "Signed OTA verification is configured with a public verification key. " + "Signed OTA verification is enabled without a signing key. " "Binaries will NOT be signed automatically during build. " "You must sign them externally before flashing." ) + if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None: + variant = config[CONF_VARIANT] + if variant in NVS_ENCRYPTION_HMAC_VARIANTS: + _LOGGER.warning( + "NVS encryption will burn an HMAC key into eFuse key block %d on the " + "first boot of each device. This is PERMANENT and IRREVERSIBLE: " + "the block cannot be erased or reused afterwards. Enabling (or " + "later disabling) encryption also wipes any previously saved " + "preferences once, because the older data can no longer be read.", + nvs_enc[CONF_KEY_ID], + ) + else: + supported = ", ".join( + sorted(VARIANT_FRIENDLY[v] for v in NVS_ENCRYPTION_HMAC_VARIANTS) + ) + errs.append( + cv.Invalid( + f"NVS encryption (HMAC scheme) is not supported on " + f"{VARIANT_FRIENDLY[variant]} (it has no HMAC peripheral). " + f"Supported variants: {supported}.", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_ENCRYPTION], + ) + ) + if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]: + project = full_config[CONF_ESPHOME].get(CONF_PROJECT) + errs.extend( + _ota_downgrade_protection_errors( + project[CONF_VERSION] if project else None, + bool(advanced.get(CONF_SIGNED_OTA_VERIFICATION)), + ) + ) if errs: raise cv.MultipleInvalid(errs) @@ -1485,16 +1660,20 @@ FRAMEWORK_SCHEMA = cv.Schema( { cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO), cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_RELEASE): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_pio_platform_version, - cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { - cv.string_strict: cv.string_strict - }, + cv.Optional(CONF_RELEASE, visibility=cv.Visibility.YAML_ONLY): cv.string_strict, + cv.Optional(CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY): cv.string_strict, + cv.Optional( + CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY + ): _parse_pio_platform_version, + cv.Optional( + CONF_SDKCONFIG_OPTIONS, default={}, visibility=cv.Visibility.YAML_ONLY + ): {cv.string_strict: cv.string_strict}, cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( *LOG_LEVELS_IDF, upper=True ), - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + cv.Optional( + CONF_ADVANCED, default={}, visibility=cv.Visibility.YAML_ONLY + ): cv.Schema( { cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( *ASSERTION_LEVELS, upper=True @@ -1540,17 +1719,20 @@ FRAMEWORK_SCHEMA = cv.Schema( min=8192, max=32768 ), cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean, - cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All( - cv.Schema( - { - cv.Optional(CONF_SIGNING_KEY): cv.file_, - cv.Optional(CONF_VERIFICATION_KEY): cv.file_, - cv.Optional( - CONF_SIGNING_SCHEME, default="rsa3072" - ): cv.one_of(*SIGNING_SCHEMES, lower=True), - } - ), - cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY), + cv.Optional( + CONF_ENABLE_OTA_DOWNGRADE_PROTECTION, default=False + ): cv.boolean, + cv.Optional( + CONF_SIGNED_OTA_VERIFICATION + ): _validate_signed_ota_verification, + cv.Optional(CONF_NVS_ENCRYPTION): cv.Schema( + { + # eFuse key block (0-5) that stores the HMAC key from + # which the NVS encryption keys are derived. The block is + # written on first boot if empty -- an irreversible + # operation -- so it must be chosen explicitly. + cv.Required(CONF_KEY_ID): cv.int_range(min=0, max=5), + } ), cv.Optional( CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False @@ -1572,7 +1754,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean, } ), - cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( + cv.Optional( + CONF_COMPONENTS, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list( cv.All( cv.Any( cv.All(cv.string_strict, _parse_idf_component), @@ -1672,7 +1856,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_FREQUENCY): cv.one_of( *FLASH_FREQUENCIES, upper=True ), - cv.Optional(CONF_PARTITIONS): cv.Any( + cv.Optional(CONF_PARTITIONS, visibility=cv.Visibility.YAML_ONLY): cv.Any( cv.file_, cv.ensure_list( cv.All( @@ -1696,7 +1880,9 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, - cv.Optional(CONF_TOOLCHAIN): _validate_toolchain, + cv.Optional( + CONF_TOOLCHAIN, visibility=cv.Visibility.ADVANCED + ): _validate_toolchain, cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All( cv.positive_time_period_seconds, cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)), @@ -2358,6 +2544,16 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True) cg.add_define("USE_OTA_ROLLBACK") + # Enable software OTA downgrade protection. Embed the project version into + # the image's esp_app_desc_t so the OTA backend can compare it against the + # running version (final_validate guarantees a dotted-numeric project + # version and that signed OTA is enabled). + if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]: + project_version = CORE.config[CONF_ESPHOME][CONF_PROJECT][CONF_VERSION] + add_idf_sdkconfig_option("CONFIG_APP_PROJECT_VER_FROM_CONFIG", True) + add_idf_sdkconfig_option("CONFIG_APP_PROJECT_VER", project_version) + cg.add_define("USE_OTA_DOWNGRADE_PROTECTION") + # Enable signed app verification without hardware secure boot if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION): add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT", True) @@ -2375,15 +2571,33 @@ async def to_code(config): signed_ota[CONF_SIGNING_KEY].resolve().as_posix(), ) else: - # Public key mode — verification only, external signing required + # External signing mode — binaries must be signed after the build add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", False) - add_idf_sdkconfig_option( - "CONFIG_SECURE_BOOT_VERIFICATION_KEY", - signed_ota[CONF_VERIFICATION_KEY].resolve().as_posix(), - ) + if CONF_VERIFICATION_KEY in signed_ota: + # V1 ECDSA only: the public key is compiled into the app to + # verify externally-signed images. V2 schemes carry the public + # key in each image's signature block and need no key here. + add_idf_sdkconfig_option( + "CONFIG_SECURE_BOOT_VERIFICATION_KEY", + signed_ota[CONF_VERIFICATION_KEY].resolve().as_posix(), + ) cg.add_define("USE_OTA_SIGNED_VERIFICATION") + # Encrypt NVS using the HMAC peripheral scheme. The NVS encryption keys are + # derived at runtime from an HMAC key stored in the configured eFuse block + # (no flash encryption required). The HMAC key is generated and burned into + # the eFuse block on first boot if it is empty. With the scheme selected, + # nvs_sec_provider registers it at startup and the default nvs_flash_init() + # (used in esp32/preferences.cpp) transparently performs the secure init, so + # no C++ changes are needed. + if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None: + add_idf_sdkconfig_option("CONFIG_NVS_ENCRYPTION", True) + add_idf_sdkconfig_option("CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC", True) + add_idf_sdkconfig_option( + "CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID", nvs_enc[CONF_KEY_ID] + ) + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define( @@ -2858,19 +3072,23 @@ def copy_files(): def _decode_pc(config, addr): - # _decode_pc runs from the api log processor's asyncio callback, which - # only catches EsphomeError. Any other exception escaping here tears down - # the protocol and triggers an infinite reconnect/replay loop. Convert - # toolchain-resolution errors (e.g. missing build dir / cmake cache) into - # EsphomeError so the caller can disable decoding cleanly. + # Convert toolchain-resolution errors (e.g. missing build dir / cmake + # cache) into EsphomeError. The api log processor stops decoding on any + # exception, so this is about the message it reports rather than about + # catching it at all: EsphomeError carries an explanation worth showing + # the user, where a raw OSError repr does not. if CORE.using_toolchain_esp_idf: from esphome.espidf import toolchain as idf_toolchain try: addr2line_path = idf_toolchain.get_addr2line_path() firmware_elf_path = idf_toolchain.get_elf_path() - except RuntimeError as err: + except (RuntimeError, OSError) as err: + # OSError covers a missing build directory or a cmake that isn't + # on PATH; both surface from the subprocess call, not as RuntimeError. raise EsphomeError(f"ESP-IDF toolchain not available: {err}") from err + if not firmware_elf_path.is_file(): + raise EsphomeError(f"Firmware ELF not found: {firmware_elf_path}") else: from esphome.platformio import toolchain @@ -2901,9 +3119,10 @@ def _parse_register(config, regex, line): STACKTRACE_ESP32_PC_RE = re.compile(r".*PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7}).*") -STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r"EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r".*EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") STACKTRACE_ESP32_C3_PC_RE = re.compile(r"MEPC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") STACKTRACE_ESP32_C3_RA_RE = re.compile(r"RA\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_C3_MTVAL_RE = re.compile(r".*MTVAL\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") STACKTRACE_BAD_ALLOC_RE = re.compile( r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$" ) @@ -2921,9 +3140,10 @@ def process_stacktrace(config, line, backtrace_state): # ESP32 PC/EXCVADDR _parse_register(config, STACKTRACE_ESP32_PC_RE, line) _parse_register(config, STACKTRACE_ESP32_EXCVADDR_RE, line) - # ESP32-C3 PC/RA + # ESP32-C3 PC/RA/MTVAL _parse_register(config, STACKTRACE_ESP32_C3_PC_RE, line) _parse_register(config, STACKTRACE_ESP32_C3_RA_RE, line) + _parse_register(config, STACKTRACE_ESP32_C3_MTVAL_RE, line) # bad alloc match = re.match(STACKTRACE_BAD_ALLOC_RE, line) diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index 83fcfd233e..248f84c6bc 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -63,4 +63,10 @@ VARIANT_FRIENDLY = { VARIANT_ESP32S31: "ESP32-S31", } + +def variant_to_idf_target(variant: str) -> str: + """Map an esp32 variant name (e.g. "ESP32S3") to its ESP-IDF target name.""" + return variant.lower().replace("-", "") + + esp32_ns = cg.esphome_ns.namespace("esp32") diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index a7de48a6ee..4c0f430daf 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -122,7 +122,7 @@ static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *ou // Magic is second to validate the data. Remaining fields can change between versions. // Version is uint32_t because it would be padded to 4 bytes anyway before the next // uint32_t field, so we use the full width rather than wasting 3 bytes of padding. -static constexpr uint32_t CRASH_DATA_VERSION = 2; +static constexpr uint32_t CRASH_DATA_VERSION = 3; struct RawCrashData { uint32_t version; uint32_t magic; @@ -132,7 +132,8 @@ struct RawCrashData { uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG) uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic) uint32_t backtrace[MAX_BACKTRACE]; - uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V) + uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V) + uint32_t fault_addr; // Faulting memory address: excvaddr (Xtensa) or mtval (RISC-V) uint8_t crashed_core; #if SOC_CPU_CORES_NUM > 1 static_assert(SOC_CPU_CORES_NUM == 2, "Dual-core logic assumes exactly 2 cores"); @@ -240,6 +241,16 @@ static const char *get_exception_reason() { nullptr, "LoadProhibited", "StoreProhibited", + nullptr, + nullptr, + "Cp0Dis", + "Cp1Dis", + "Cp2Dis", + "Cp3Dis", + "Cp4Dis", + "Cp5Dis", + "Cp6Dis", + "Cp7Dis", }; uint32_t cause = s_raw_crash_data.cause; if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr) @@ -332,12 +343,24 @@ void crash_handler_log() { ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); const char *reason = get_exception_reason(); if (reason != nullptr) { - ESP_LOGE(TAG, " Reason: %s - %s", get_exception_type(), reason); + ESP_LOGE(TAG, " Reason: %s - %s (cause %" PRIu32 ")", get_exception_type(), reason, s_raw_crash_data.cause); } else { ESP_LOGE(TAG, " Reason: %s", get_exception_type()); } ESP_LOGE(TAG, " Crashed core: %d", s_raw_crash_data.crashed_core); ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc); + // Faulting memory address — only meaningful for real CPU faults, not + // aborts/watchdogs or SoC-level pseudo exceptions. Uses the same register + // name as ESP-IDF's live register dump for the architecture (EXCVADDR on + // Xtensa, MTVAL on RISC-V) so the CLI decodes it when it happens to be a + // code address. + if (s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause) { +#if CONFIG_IDF_TARGET_ARCH_XTENSA + ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32 " (faulting address)", s_raw_crash_data.fault_addr); +#elif CONFIG_IDF_TARGET_ARCH_RISCV + ESP_LOGE(TAG, " MTVAL: 0x%08" PRIX32 " (faulting address)", s_raw_crash_data.fault_addr); +#endif + } log_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, s_raw_crash_data.reg_frame_count); #if SOC_CPU_CORES_NUM > 1 @@ -382,6 +405,9 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { s_raw_crash_data.exception = (uint8_t) info->exception; s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0; s_raw_crash_data.crashed_core = (uint8_t) info->core; + // Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot + s_raw_crash_data.cause = 0; + s_raw_crash_data.fault_addr = 0; #if SOC_CPU_CORES_NUM > 1 s_raw_crash_data.other_backtrace_count = 0; s_raw_crash_data.other_reg_frame_count = 0; @@ -392,6 +418,7 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { if (info->frame != nullptr) { auto *xt_frame = (XtExcFrame *) info->frame; s_raw_crash_data.cause = xt_frame->exccause; + s_raw_crash_data.fault_addr = xt_frame->excvaddr; s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE); } @@ -414,6 +441,7 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { if (info->frame != nullptr) { auto *rv_frame = (RvExcFrame *) info->frame; s_raw_crash_data.cause = rv_frame->mcause; + s_raw_crash_data.fault_addr = rv_frame->mtval; s_raw_crash_data.backtrace_count = capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count); } diff --git a/esphome/components/esp32/preference_backend.h b/esphome/components/esp32/preference_backend.h index 893bc35f0c..b0771b3128 100644 --- a/esphome/components/esp32/preference_backend.h +++ b/esphome/components/esp32/preference_backend.h @@ -11,8 +11,11 @@ class ESP32PreferenceBackend final { bool save(const uint8_t *data, size_t len); bool load(uint8_t *data, size_t len); - uint32_t key; - uint32_t nvs_handle; + uint32_t key{0}; + uint32_t nvs_handle{0}; // NVS (flash) path + uint16_t rtc_offset{0}; // RTC path: word offset into the RTC storage region + uint8_t length_words{0}; // RTC path: data length in 32-bit words + bool in_flash{true}; // true: store in NVS (flash); false: store in RTC memory }; class ESP32Preferences; diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 09835385ac..dc2b40455c 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -3,7 +3,10 @@ #include "preferences.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/preferences_rtc.h" +#include #include +#include #include #include @@ -18,6 +21,48 @@ struct NVSData { static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// RTC memory backend for preferences requested with in_flash=false. Survives deep sleep and +// software/CPU resets, but not power loss; integrity is guarded by a per-record checksum so +// power-on garbage is detected on load. Keep this small: RTC memory is scarce and shared. +// +// Only compiled in when USE_ESP32_RTC_PREFERENCES_STORAGE is set (see preferences.h): the storage +// buffer reserves RTC memory, so it exists only when some config option actually selected RTC +// storage AND the variant has RTC memory (the ESP32-C2 and -C61 have none, so RTC_NOINIT_ATTR would +// have no section to land in and fail to link). Otherwise in_flash=false transparently falls back +// to NVS (see make_preference below). +// +// On variants with only RTC fast memory (C3/C6/H2/P4/C5/...) RTC_NOINIT_ATTR lands in RTC fast memory. +// This is still safe: the linker reserves .rtc_noinit ahead of any RTC-fast-as-heap pool +// (CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP), and IDF keeps the RTC fast power domain on in deep +// sleep (forced on whether or not it is used as heap), so the data is retained across both resets and +// deep sleep -- only power loss clears it. +#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE +static constexpr size_t RTC_PREF_SIZE_WORDS = 64; // 256 bytes +static constexpr size_t RTC_PREF_MAX_WORDS = 255; // length_words field is a uint8_t + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static RTC_NOINIT_ATTR uint32_t s_rtc_storage[RTC_PREF_SIZE_WORDS]; + +static bool save_to_rtc(uint16_t offset, uint32_t key, uint8_t length_words, const uint8_t *data, size_t len) { + if (rtc_pref_bytes_to_words(len) != length_words) + return false; + const size_t buffer_size = static_cast(length_words) + 1; + if (static_cast(offset) + buffer_size > RTC_PREF_SIZE_WORDS) + return false; + rtc_pref_encode(&s_rtc_storage[offset], key, length_words, data, len); + return true; +} + +static bool load_from_rtc(uint16_t offset, uint32_t key, uint8_t length_words, uint8_t *data, size_t len) { + if (rtc_pref_bytes_to_words(len) != length_words) + return false; + const size_t buffer_size = static_cast(length_words) + 1; + if (static_cast(offset) + buffer_size > RTC_PREF_SIZE_WORDS) + return false; + return rtc_pref_decode(&s_rtc_storage[offset], key, length_words, data, len); +} +#endif // USE_ESP32_RTC_PREFERENCES_STORAGE + // open() runs from app_main() before the logger is initialized, so any failure // must be deferred until after global_logger is set. This is emitted from the // first make_preference() call, which runs from the generated setup() after @@ -25,6 +70,10 @@ static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-n static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) { +#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE + if (!this->in_flash) + return save_to_rtc(this->rtc_offset, this->key, this->length_words, data, len); +#endif // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { @@ -41,6 +90,10 @@ bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) { } bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) { +#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE + if (!this->in_flash) + return load_from_rtc(this->rtc_offset, this->key, this->length_words, data, len); +#endif // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == this->key) { @@ -94,6 +147,26 @@ void ESP32Preferences::open() { } } +ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type, bool in_flash) { +#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE + if (!in_flash) + return this->make_rtc_preference_(length, type); +#else + if (!in_flash) { + // RTC storage is not compiled in (no config option selected it), so this request + // falls back to NVS -- the historic ESP32 behavior. Warn once so callers explicitly + // asking for RTC storage can discover the fallback. + static bool warned = false; + if (!warned) { + ESP_LOGW(TAG, "RTC preference storage not compiled in; using NVS (enable with 'preferences: rtc_storage: true')"); + warned = true; + } + } +#endif + // in_flash, or RTC storage not compiled in: fall back to NVS. + return this->make_preference(length, type); +} + ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) { if (s_open_err != ESP_OK) { if (this->nvs_handle == 0) { @@ -106,10 +179,34 @@ ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t ty auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) pref->nvs_handle = this->nvs_handle; pref->key = type; + pref->in_flash = true; return ESPPreferenceObject(pref); } +#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE +ESPPreferenceObject ESP32Preferences::make_rtc_preference_(size_t length, uint32_t type) { + const uint32_t length_words = rtc_pref_bytes_to_words(length); + if (length_words > RTC_PREF_MAX_WORDS) { + ESP_LOGE(TAG, "RTC preference too large: %" PRIu32 " words", length_words); + return {}; + } + const uint32_t total_words = length_words + 1; // +1 for checksum + if (static_cast(this->current_rtc_offset_) + total_words > RTC_PREF_SIZE_WORDS) { + ESP_LOGE(TAG, "RTC preference storage full, cannot allocate %" PRIu32 " words", total_words); + return {}; + } + auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->key = type; + pref->in_flash = false; + pref->rtc_offset = this->current_rtc_offset_; + pref->length_words = static_cast(length_words); + this->current_rtc_offset_ += static_cast(total_words); + + return ESPPreferenceObject(pref); +} +#endif // USE_ESP32_RTC_PREFERENCES_STORAGE + bool ESP32Preferences::sync() { if (s_pending_save.empty()) return true; @@ -186,6 +283,12 @@ bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, bool ESP32Preferences::reset() { ESP_LOGD(TAG, "Erasing storage"); s_pending_save.clear(); +#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE + // Invalidate RTC-backed preferences too (checksum will no longer match). current_rtc_offset_ is + // deliberately left alone: existing backends keep pointing at their allocated slots, and reset() + // is always followed by a restart (same reason nvs_handle is zeroed below). + memset(s_rtc_storage, 0, sizeof(s_rtc_storage)); +#endif nvs_flash_deinit(); nvs_flash_erase(); diff --git a/esphome/components/esp32/preferences.h b/esphome/components/esp32/preferences.h index 0e187d87a9..864d22312b 100644 --- a/esphome/components/esp32/preferences.h +++ b/esphome/components/esp32/preferences.h @@ -2,6 +2,15 @@ #ifdef USE_ESP32 #include "esphome/core/preference_backend.h" +#include + +// RTC-backed preference storage is compiled in only when a config option actually selects it +// (USE_ESP32_RTC_PREFERENCES, emitted during code generation) and the variant has RTC memory +// (SOC_RTC_MEM_SUPPORTED; the ESP32-C2 and -C61 have none). Otherwise in_flash=false falls +// back to NVS and no RTC memory is reserved. +#if defined(USE_ESP32_RTC_PREFERENCES) && SOC_RTC_MEM_SUPPORTED +#define USE_ESP32_RTC_PREFERENCES_STORAGE +#endif namespace esphome::esp32 { @@ -11,9 +20,8 @@ class ESP32Preferences final : public PreferencesMixin { public: using PreferencesMixin::make_preference; void open(); - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) { - return this->make_preference(length, type); - } + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash); + // Two-argument form defaults to NVS (flash) storage, preserving historic ESP32 behavior. ESPPreferenceObject make_preference(size_t length, uint32_t type); bool sync(); bool reset(); @@ -22,6 +30,13 @@ class ESP32Preferences final : public PreferencesMixin { protected: bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str); + +#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE + // RTC-backed storage (in_flash=false). + ESPPreferenceObject make_rtc_preference_(size_t length, uint32_t type); + // Next free word offset in the RTC storage region (bump allocated in make_preference order). + uint16_t current_rtc_offset_{0}; +#endif }; void setup_preferences(); diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index c9fb42fde4..c8613963b9 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -2,11 +2,19 @@ from collections.abc import Callable, MutableMapping from dataclasses import dataclass from enum import Enum import logging -import re from typing import Any from esphome import automation import esphome.codegen as cg + +# bt_uuid validation lives in the platform-neutral ble_device_base; re-exported +# here for backward compatibility. +from esphome.components.ble_device_base import ( # noqa: F401 # pylint: disable=unused-import + BT_UUID16_FORMAT as bt_uuid16_format, + BT_UUID32_FORMAT as bt_uuid32_format, + BT_UUID128_FORMAT as bt_uuid128_format, + bt_uuid, +) from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import ( add_idf_sdkconfig_option, @@ -28,6 +36,7 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv from esphome.types import ConfigType +AUTO_LOAD = ["ble_device_base"] # ble_uuid.h builds on the neutral ESPBTUUID DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] DOMAIN = "esp32_ble" @@ -372,43 +381,6 @@ def _validate_key_sizes(config: ConfigType) -> ConfigType: CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes) -bt_uuid16_format = "XXXX" -bt_uuid32_format = "XXXXXXXX" -bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" - - -def bt_uuid(value): - in_value = cv.string_strict(value) - value = in_value.upper() - - if len(value) == len(bt_uuid16_format): - pattern = re.compile("^[A-F0-9]{4,}$") - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" - ) - return value - if len(value) == len(bt_uuid32_format): - pattern = re.compile("^[A-F0-9]{8,}$") - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" - ) - return value - if len(value) == len(bt_uuid128_format): - pattern = re.compile( - "^[A-F0-9]{8,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{12,}$" - ) - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 128 UUID format: '{in_value}'" - ) - return value - raise cv.Invalid( - f"Bluetooth UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format" - ) - - def validate_variant(_): variant = get_esp32_variant() if variant in NO_BLUETOOTH_VARIANTS: diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 6bbf0d6a26..fb75e8837f 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -9,6 +9,8 @@ #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include #else +#include "esphome/components/watchdog/watchdog.h" +#include extern "C" { #include #include @@ -33,6 +35,19 @@ namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; +#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID +// Bringing up the remote BT controller issues synchronous RPCs to the +// co-processor with 5 second response timeouts, and the default task watchdog +// is also 5 seconds. If the co-processor firmware does not answer (for example +// factory firmware without Bluetooth support), the watchdog would reboot the +// device before the RPC could return an error, causing a boot loop. Raise the +// watchdog for the duration of the bring-up so failures surface as error +// returns instead. 60 seconds covers the worst case: transport reconnect +// (up to ~20s), version preflight (1s), controller init/enable (5s each) and +// the bluedroid host bring-up over the hosted HCI transport. +static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000; +#endif + // GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_ #define GAP_SCAN_COMPLETE_EVENTS \ case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \ @@ -43,6 +58,7 @@ static const char *const TAG = "esp32_ble"; case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \ case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \ case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \ + case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT: \ case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \ case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT @@ -164,6 +180,9 @@ void ESP32BLE::advertising_init_() { bool ESP32BLE::ble_setup_() { esp_err_t err; +#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID + watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS); +#endif #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { // start bt controller @@ -192,15 +211,35 @@ bool ESP32BLE::ble_setup_() { esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); #else - esp_hosted_connect_to_slave(); // NOLINT + if (esp_hosted_connect_to_slave() != ESP_OK) { // NOLINT + ESP_LOGE(TAG, "Co-processor transport failed; BLE disabled"); + return false; + } + + // Fast preflight (1 second RPC timeout): verifies the co-processor answers + // RPCs at all before the 5 second timeout BT controller RPCs below, and + // before hosted_hci_bluedroid_open(), which aborts if the transport is down. + esp_hosted_coprocessor_fwver_t fw_ver{}; + if (esp_hosted_get_coprocessor_fwversion(&fw_ver) != ESP_OK) { + ESP_LOGE(TAG, "Co-processor not responding; BLE disabled. Update its firmware with the esp32_hosted " + "update component"); + return false; + } + ESP_LOGD(TAG, "Co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32, fw_ver.major1, fw_ver.minor1, fw_ver.patch1); if (esp_hosted_bt_controller_init() != ESP_OK) { - ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed"); + ESP_LOGE(TAG, + "BT controller init failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32 + " may lack BT support. Update it with the esp32_hosted update component; BLE disabled", + fw_ver.major1, fw_ver.minor1, fw_ver.patch1); return false; } if (esp_hosted_bt_controller_enable() != ESP_OK) { - ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed"); + ESP_LOGE(TAG, + "BT controller enable failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32 + " may lack BT support. Update it with the esp32_hosted update component; BLE disabled", + fw_ver.major1, fw_ver.minor1, fw_ver.patch1); return false; } @@ -332,6 +371,10 @@ bool ESP32BLE::ble_setup_() { } bool ESP32BLE::ble_dismantle_() { +#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID + // Same 5 second RPCs as the bring-up path; see HOSTED_BT_WDT_TIMEOUT_MS + watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS); +#endif esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { // ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine @@ -377,12 +420,12 @@ bool ESP32BLE::ble_dismantle_() { } #else if (esp_hosted_bt_controller_disable() != ESP_OK) { - ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed"); + ESP_LOGE(TAG, "esp_hosted_bt_controller_disable failed"); return false; } if (esp_hosted_bt_controller_deinit(false) != ESP_OK) { - ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed"); + ESP_LOGE(TAG, "esp_hosted_bt_controller_deinit failed"); return false; } diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index 3cfa6f548a..6c8a97f453 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/defines.h" +#include "ble_uuid.h" #include #include @@ -15,8 +16,6 @@ namespace esphome::esp32_ble { -class ESPBTUUID; - class BLEAdvertising { public: BLEAdvertising(uint32_t advertising_cycle_time); diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index ba87fd8805..babfa937c7 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -207,7 +207,7 @@ class BLEEvent { StatusOnlyData scan_complete; // 1 byte // Advertising complete events all have same structure // Used by: esp32_ble_beacon, esp32_ble server components - // ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP + // ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, SCAN_RSP_DATA_RAW_SET, ADV_START, ADV_STOP StatusOnlyData adv_complete; // 1 byte // RSSI complete event // Used by: ble_client (ble_rssi_sensor component) @@ -324,6 +324,9 @@ class BLEEvent { case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: // Used by: esp32_ble_beacon this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status; break; + case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT: // Used by: raw advertisers with scan response + this->event_.gap.adv_complete.status = p->scan_rsp_data_raw_cmpl.status; + break; case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: // Used by: esp32_ble_beacon this->event_.gap.adv_complete.status = p->adv_start_cmpl.status; break; diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp deleted file mode 100644 index 886f8237ad..0000000000 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ /dev/null @@ -1,193 +0,0 @@ -#include "ble_uuid.h" - -#ifdef USE_ESP32 -#ifdef USE_ESP32_BLE_UUID - -#include -#include -#include -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -namespace esphome::esp32_ble { - -static const char *const TAG = "esp32_ble"; - -ESPBTUUID::ESPBTUUID() : uuid_() {} -ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = uuid; - return ret; -} -ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = uuid; - return ret; -} -ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_128; - memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128); - return ret; -} -ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_128; - for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[ESP_UUID_LEN_128 - 1 - i] = data[i]; - return ret; -} -ESPBTUUID ESPBTUUID::from_raw(const char *data, size_t length) { - ESPBTUUID ret; - if (length == 4) { - // 16-bit UUID as 4-character hex string - auto parsed = parse_hex(data, length); - if (parsed.has_value()) { - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = parsed.value(); - } - } else if (length == 8) { - // 32-bit UUID as 8-character hex string - auto parsed = parse_hex(data, length); - if (parsed.has_value()) { - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = parsed.value(); - } - } else if (length == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be - // investigated (lack of time) - ret.uuid_.len = ESP_UUID_LEN_128; - memcpy(ret.uuid_.uuid.uuid128, reinterpret_cast(data), 16); - } else if (length == 36) { - // If the length of the string is 36 bytes then we will assume it is a long hex string in - // UUID format. - ret.uuid_.len = ESP_UUID_LEN_128; - int n = 0; - for (size_t i = 0; i < length; i += 2) { - if (data[i] == '-') - i++; - uint8_t msb = data[i]; - uint8_t lsb = data[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F); - } - } else { - ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data); - } - return ret; -} -ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = uuid.len; - if (uuid.len == ESP_UUID_LEN_16) { - ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; - } else if (uuid.len == ESP_UUID_LEN_32) { - ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; - } else if (uuid.len == ESP_UUID_LEN_128) { - memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128); - } - return ret; -} -ESPBTUUID ESPBTUUID::as_128bit() const { - if (this->uuid_.len == ESP_UUID_LEN_128) { - return *this; - } - uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - uint32_t uuid32; - if (this->uuid_.len == ESP_UUID_LEN_32) { - uuid32 = this->uuid_.uuid.uuid32; - } else { - uuid32 = this->uuid_.uuid.uuid16; - } - for (uint16_t i = 0; i < this->uuid_.len; i++) { - data[12 + i] = ((uuid32 >> i * 8) & 0xFF); - } - return ESPBTUUID::from_raw(data); -} -bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { - if (this->uuid_.len == ESP_UUID_LEN_16) { - return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1; - } else if (this->uuid_.len == ESP_UUID_LEN_32) { - for (uint8_t i = 0; i < 3; i++) { - bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1; - bool b = ((this->uuid_.uuid.uuid32 >> (i + 1) * 8) & 0xFF) == data2; - if (a && b) - return true; - } - } else { - for (uint8_t i = 0; i < 15; i++) { - if (this->uuid_.uuid.uuid128[i] == data1 && this->uuid_.uuid.uuid128[i + 1] == data2) - return true; - } - } - return false; -} -bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { - if (this->uuid_.len == uuid.uuid_.len) { - switch (this->uuid_.len) { - case ESP_UUID_LEN_16: - return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16; - case ESP_UUID_LEN_32: - return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32; - case ESP_UUID_LEN_128: - return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0; - default: - return false; - } - } - return this->as_128bit() == uuid.as_128bit(); -} -esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } -const char *ESPBTUUID::to_str(std::span output) const { - char *pos = output.data(); - - switch (this->uuid_.len) { - case ESP_UUID_LEN_16: - *pos++ = '0'; - *pos++ = 'x'; - *pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 >> 12); - *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 8) & 0x0F); - *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F); - *pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F); - *pos = '\0'; - return output.data(); - - case ESP_UUID_LEN_32: - *pos++ = '0'; - *pos++ = 'x'; - for (int shift = 28; shift >= 0; shift -= 4) { - *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F); - } - *pos = '\0'; - return output.data(); - - default: - case ESP_UUID_LEN_128: - // Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX - for (int8_t i = 15; i >= 0; i--) { - uint8_t byte = this->uuid_.uuid.uuid128[i]; - *pos++ = format_hex_pretty_char(byte >> 4); - *pos++ = format_hex_pretty_char(byte & 0x0F); - if (i == 12 || i == 10 || i == 8 || i == 6) { - *pos++ = '-'; - } - } - *pos = '\0'; - return output.data(); - } -} -std::string ESPBTUUID::to_string() const { - char buf[UUID_STR_LEN]; - this->to_str(buf); - return std::string(buf); -} - -} // namespace esphome::esp32_ble - -#endif // USE_ESP32_BLE_UUID -#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 503fde6945..fd8da4baee 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -1,59 +1,21 @@ #pragma once #include "esphome/core/defines.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" #ifdef USE_ESP32 #ifdef USE_ESP32_BLE_UUID -#include -#include -#include -#include +// The BLE UUID type is owned by the platform-neutral ble_device_base layer; +// this header re-exports it under the historical esp32_ble name (esp32 only). +// The full historical API surface — including from_uuid()/get_uuid() with the +// ESP-IDF esp_bt_uuid_t type — is preserved on esp32 builds. + +#include "esphome/components/ble_device_base/ble_device.h" namespace esphome::esp32_ble { -/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0" -static constexpr size_t UUID_STR_LEN = 37; - -class ESPBTUUID { - public: - ESPBTUUID(); - - static ESPBTUUID from_uint16(uint16_t uuid); - - static ESPBTUUID from_uint32(uint32_t uuid); - - static ESPBTUUID from_raw(const uint8_t *data); - static ESPBTUUID from_raw_reversed(const uint8_t *data); - - static ESPBTUUID from_raw(const char *data, size_t length); - static ESPBTUUID from_raw(const char *data) { return from_raw(data, strlen(data)); } - static ESPBTUUID from_raw(const std::string &data) { return from_raw(data.c_str(), data.length()); } - static ESPBTUUID from_raw(std::initializer_list data) { - return from_raw(reinterpret_cast(data.begin()), data.size()); - } - - static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); - - ESPBTUUID as_128bit() const; - - bool contains(uint8_t data1, uint8_t data2) const; - - bool operator==(const ESPBTUUID &uuid) const; - bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } - - esp_bt_uuid_t get_uuid() const; - - // Remove before 2026.8.0 - ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0") - std::string to_string() const; // NOLINT - const char *to_str(std::span output) const; - - protected: - esp_bt_uuid_t uuid_; -}; +using ble_device_base::UUID_STR_LEN; +using ESPBTUUID = ble_device_base::ESPBTUUID; } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 3fb9632e9a..bd80f71a49 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -125,6 +125,9 @@ void BLEClientBase::connect() { } ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; + // A registration whose event never arrived must not block this connection's release. + this->services_released_ = false; + this->pending_notify_regs_ = 0; // Enable loop for state processing this->enable_loop(); // Immediately transition to CONNECTING to prevent duplicate connection attempts @@ -200,10 +203,26 @@ void BLEClientBase::release_services() { this->services_.clear(); #endif #ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH + // Only the cache clean makes the stack's database unsafe to walk. + this->services_released_ = true; esp_ble_gattc_cache_clean(this->remote_bda_); #endif } +esp_err_t BLEClientBase::register_for_notify(uint16_t char_handle) { + esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, char_handle); + if (err != ESP_OK) + return err; + if (this->pending_notify_regs_ == UINT8_MAX) { + // Saturating undercounts, so the release can run before the last registration completes. + // Wrapping to zero would undercount by the full range instead, which is worse. + this->log_warning_("Too many outstanding notify registrations to track"); + return err; + } + this->pending_notify_regs_++; + return err; +} + void BLEClientBase::log_event_(const char *name) { ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name); } @@ -498,12 +517,20 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { this->log_gattc_data_event_("REG_FOR_NOTIFY"); + // The event carries no conn_id, so this is the only place the request can be retired. + if (this->pending_notify_regs_ > 0) + this->pending_notify_regs_--; if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // Client is responsible for flipping the descriptor value // when using the cache break; } + if (this->services_released_) { + // The lookup below walks the freed GATT cache, and Bluedroid asserts on it rather than erroring. + this->log_warning_("REG_FOR_NOTIFY after services released, notifications not enabled"); + break; + } esp_gattc_descr_elem_t desc_result; uint16_t count = 1; esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle( diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 0291a4b993..0902aad924 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -44,6 +44,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void unconditional_disconnect(); void release_services(); + /// Register for notifications, holding the service release until the registration completes. + esp_err_t register_for_notify(uint16_t char_handle); + + /// True while a register_for_notify() request has not completed. + bool notify_registration_pending() const { return this->pending_notify_regs_ > 0; } + bool connected() { return this->state() == espbt::ClientState::ESTABLISHED; } void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; } @@ -125,9 +131,15 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; uint8_t connection_index_; uint8_t service_count_{0}; // ESP32 has max handles < 255, typical devices have < 50 services + // Outstanding register_for_notify() requests + // A count, not per-request state, so a raw esp_ble_gattc_register_for_notify() on the same client can retire one + // services_released_ is the backstop if that ever lets the release run early + uint8_t pending_notify_regs_{0}; bool auto_connect_{false}; bool paired_{false}; - // 6 bytes used, 2 bytes padding + // Set only when release_services() cleans the stack's GATT cache, which no API may then walk + bool services_released_{false}; + // 8 bytes used, no padding void log_event_(const char *name); void log_gattc_lifecycle_event_(const char *name); diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index 5ca80d6a7a..3dcac3691c 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -77,6 +77,10 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ case ESP_GATTS_WRITE_EVT: { if (this->handle_ != param->write.handle) break; + if (param->write.len > this->value_.attr_max_len) { + ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", param->write.len, this->value_.attr_max_len); + break; + } this->value_.attr_len = param->write.len; memcpy(this->value_.attr_value, param->write.value, param->write.len); if (this->on_write_callback_) { diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index e4139bed65..e462da1a49 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -5,7 +5,8 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components import esp32_ble, ota +from esphome.components import ble_device_base, esp32_ble, ota +from esphome.components.const import CONF_SCAN_PARAMETERS, CONF_WINDOW from esphome.components.esp32 import ( add_idf_sdkconfig_option, request_bluetooth, @@ -39,13 +40,11 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.enum import StrEnum from esphome.types import ConfigType -AUTO_LOAD = ["esp32_ble"] +AUTO_LOAD = ["ble_device_base", "esp32_ble"] DEPENDENCIES = ["esp32"] CODEOWNERS = ["@bdraco"] CONF_ESP32_BLE_ID = "esp32_ble_id" -CONF_SCAN_PARAMETERS = "scan_parameters" -CONF_WINDOW = "window" CONF_ON_SCAN_END = "on_scan_end" CONF_SOFTWARE_COEXISTENCE = "software_coexistence" @@ -93,6 +92,7 @@ def register_ble_features(features: set[BLEFeatures]) -> None: esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_( "ESP32BLETracker", + ble_device_base.BLEHub, cg.Component, cg.Parented.template(esp32_ble.ESP32BLE), ) @@ -153,26 +153,11 @@ def validate_max_connections_deprecated(config: ConfigType) -> ConfigType: return config -def as_hex(value): - return cg.RawExpression(f"0x{value}ULL") - - -def as_hex_array(value): - value = value.replace("-", "") - cpp_array = [ - f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] - ] - return cg.RawExpression(f"(uint8_t*)(const uint8_t[16]){{{','.join(cpp_array)}}}") - - -def as_reversed_hex_array(value): - value = value.replace("-", "") - cpp_array = [ - f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] - ] - return cg.RawExpression( - f"(uint8_t*)(const uint8_t[16]){{{','.join(reversed(cpp_array))}}}" - ) +# Codegen helpers are owned by ble_device_base; kept under the historical names +# here for the components that import them from this module. +as_hex = ble_device_base.as_hex +as_hex_array = ble_device_base.as_hex_array +as_reversed_hex_array = ble_device_base.as_reversed_hex_array CONFIG_SCHEMA = cv.All( @@ -254,6 +239,10 @@ async def to_code(config): # Register the loggers this component needs esp32_ble.register_bt_logger(BTLoggers.BLE_SCAN) + # Behavior parity with the pre-split tracker: IRK resolution is always + # available on esp32 (sensors with irk: worked without opting in). + ble_device_base.request_irk_support() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -346,6 +335,14 @@ async def to_code(config): async def _add_ble_features(): # Add feature-specific defines based on what's needed required_features = _get_required_features() + # Sensors registered through the neutral ble_device_base path (BLEHub) need + # the parsed-device pipeline compiled in, exactly like esp32-path listeners. + neutral_listener_count = ble_device_base.get_listener_count() + if neutral_listener_count > 0: + required_features.add(BLEFeatures.ESP_BT_DEVICE) + # StaticVector sizing for the neutral (BLEHub) listener list — same + # pattern as the esp32-path registration counts below. + cg.add_define("ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT", neutral_listener_count) if BLEFeatures.ESP_BT_DEVICE in required_features: cg.add_define("USE_ESP32_BLE_DEVICE") cg.add_define("USE_ESP32_BLE_UUID") diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index f57cb7f5dc..0c1a98c4be 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -27,15 +27,6 @@ #include #endif -#ifdef USE_ESP32_BLE_DEVICE -#ifdef USE_BLE_TRACKER_PSA_AES -#include -#else -#define MBEDTLS_AES_ALT -#include -#endif -#endif // USE_ESP32_BLE_DEVICE - // bt_trace.h #undef TAG @@ -43,9 +34,6 @@ namespace esphome::esp32_ble_tracker { static const char *const TAG = "esp32_ble_tracker"; -// BLE advertisement max: 31 bytes adv data + 31 bytes scan response -static constexpr size_t BLE_ADV_MAX_LOG_BYTES = 62; - ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) const char *client_state_to_string(ClientState state) { @@ -263,10 +251,14 @@ void ESP32BLETracker::start_scan_(bool first) { #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT for (auto *listener : this->listeners_) listener->on_scan_end(); +#endif +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + for (auto *listener : this->neutral_listeners_) + listener->on_scan_end(); #endif } #ifdef USE_ESP32_BLE_DEVICE - this->already_discovered_.clear(); + this->discovered_log_.clear(); #endif this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; @@ -304,6 +296,21 @@ void ESP32BLETracker::register_client(ESPBTClient *client) { #endif } +void ESP32BLETracker::register_listener(ble_device_base::ESPBTDeviceListener *listener) { +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + // Neutral BLEHub path (migrated sensors): parsed-advertisement consumers only. + this->neutral_listeners_.push_back(listener); + this->parse_advertisements_ = true; +#endif +} + +void ESP32BLETracker::get_adapter_mac(uint8_t out[6]) { + get_mac_address_raw(out); // WiFi base MAC, MSB-first + // BT MAC = base MAC + 2 on the last octet only, wrapping without carry — + // exactly ESP-IDF's esp_read_mac(ESP_MAC_BT): mac[5] += MAC_ADDR_UNIVERSE_BT_OFFSET. + out[5] += 2; +} + void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) { #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT listener->set_parent(this); @@ -315,6 +322,13 @@ void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) { void ESP32BLETracker::recalculate_advertisement_parser_types() { this->raw_advertisements_ = false; this->parse_advertisements_ = false; +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + // Neutral (BLEHub) listeners are parsed-advertisement consumers and are not in + // listeners_; without this, any later esp32-path registration (e.g. the proxy's + // GATT clients) would recompute the flags and silently drop parsed dispatch. + if (!this->neutral_listeners_.empty()) + this->parse_advertisements_ = true; +#endif #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT for (auto *listener : this->listeners_) { if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { @@ -434,270 +448,6 @@ void ESP32BLETracker::set_scanner_state_(ScannerState state) { } } -#ifdef USE_ESP32_BLE_DEVICE -ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } -optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { - if (!data.uuid.contains(0x4C, 0x00)) - return {}; - - if (data.data.size() != 23) - return {}; - return ESPBLEiBeacon(data.data.data()); -} - -void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { - this->scan_result_ = &scan_result; - for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) - this->address_[i] = scan_result.bda[i]; - this->address_type_ = static_cast(scan_result.ble_addr_type); - this->rssi_ = scan_result.rssi; - - // Parse advertisement data directly - uint8_t total_len = scan_result.adv_data_len + scan_result.scan_rsp_len; - this->parse_adv_(scan_result.ble_adv, total_len); - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - ESP_LOGVV(TAG, "Parse Result:"); - const char *address_type; - switch (this->address_type_) { - case BLE_ADDR_TYPE_PUBLIC: - address_type = "PUBLIC"; - break; - case BLE_ADDR_TYPE_RANDOM: - address_type = "RANDOM"; - break; - case BLE_ADDR_TYPE_RPA_PUBLIC: - address_type = "RPA_PUBLIC"; - break; - case BLE_ADDR_TYPE_RPA_RANDOM: - address_type = "RPA_RANDOM"; - break; - default: - address_type = "UNKNOWN"; - break; - } - ESP_LOGVV(TAG, " Address: %02X:%02X:%02X:%02X:%02X:%02X (%s)", this->address_[0], this->address_[1], - this->address_[2], this->address_[3], this->address_[4], this->address_[5], address_type); - - ESP_LOGVV(TAG, " RSSI: %d", this->rssi_); - ESP_LOGVV(TAG, " Name: '%s'", this->name_.c_str()); - for (auto &it : this->tx_powers_) { - ESP_LOGVV(TAG, " TX Power: %d", it); - } - if (this->appearance_.has_value()) { - ESP_LOGVV(TAG, " Appearance: %u", *this->appearance_); - } - if (this->ad_flag_.has_value()) { - ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_); - } - for (auto &uuid : this->service_uuids_) { - char uuid_buf[esp32_ble::UUID_STR_LEN]; - uuid.to_str(uuid_buf); - ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf); - } - char hex_buf[format_hex_pretty_size(BLE_ADV_MAX_LOG_BYTES)]; - for (auto &data : this->manufacturer_datas_) { - auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data); - if (ibeacon.has_value()) { - ESP_LOGVV(TAG, " Manufacturer iBeacon:"); - char uuid_buf[esp32_ble::UUID_STR_LEN]; - ibeacon.value().get_uuid().to_str(uuid_buf); - ESP_LOGVV(TAG, " UUID: %s", uuid_buf); - ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major()); - ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor()); - ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power()); - } else { - char uuid_buf[esp32_ble::UUID_STR_LEN]; - data.uuid.to_str(uuid_buf); - ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, - format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); - } - } - for (auto &data : this->service_datas_) { - ESP_LOGVV(TAG, " Service data:"); - char uuid_buf[esp32_ble::UUID_STR_LEN]; - data.uuid.to_str(uuid_buf); - ESP_LOGVV(TAG, " UUID: %s", uuid_buf); - ESP_LOGVV(TAG, " Data: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); - } - - ESP_LOGVV(TAG, " Adv data: %s", - format_hex_pretty_to(hex_buf, scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len)); -#endif -} - -void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { - size_t offset = 0; - - while (offset + 2 < len) { - const uint8_t field_length = payload[offset++]; // First byte is length of adv record - if (field_length == 0) { - continue; // Possible zero padded advertisement data - } - - // Validate field fits in remaining payload - if (offset + field_length > len) { - break; - } - - // first byte of adv record is adv record type - const uint8_t record_type = payload[offset++]; - const uint8_t *record = &payload[offset]; - const uint8_t record_length = field_length - 1; - offset += record_length; - - // See also Generic Access Profile Assigned Numbers: - // https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ See also ADVERTISING AND SCAN - // RESPONSE DATA FORMAT: https://www.bluetooth.com/specifications/bluetooth-core-specification/ (vol 3, part C, 11) - // See also Core Specification Supplement: https://www.bluetooth.com/specifications/bluetooth-core-specification/ - // (called CSS here) - - switch (record_type) { - case ESP_BLE_AD_TYPE_NAME_SHORT: - case ESP_BLE_AD_TYPE_NAME_CMPL: { - // CSS 1.2 LOCAL NAME - // "The Local Name data type shall be the same as, or a shortened version of, the local name assigned to the - // device." CSS 1: Optional in this context; shall not appear more than once in a block. - // SHORTENED LOCAL NAME - // "The Shortened Local Name data type defines a shortened version of the Local Name data type. The Shortened - // Local Name data type shall not be used to advertise a name that is longer than the Local Name data type." - if (record_length > this->name_.length()) { - this->name_ = std::string(reinterpret_cast(record), record_length); - } - break; - } - case ESP_BLE_AD_TYPE_TX_PWR: { - // CSS 1.5 TX POWER LEVEL - // "The TX Power Level data type indicates the transmitted power level of the packet containing the data type." - // CSS 1: Optional in this context (may appear more than once in a block). - this->tx_powers_.push_back(*record); - break; - } - case ESP_BLE_AD_TYPE_APPEARANCE: { - // CSS 1.12 APPEARANCE - // "The Appearance data type defines the external appearance of the device." - // See also https://www.bluetooth.com/specifications/gatt/characteristics/ - // CSS 1: Optional in this context; shall not appear more than once in a block and shall not appear in both - // the AD and SRD of the same extended advertising interval. - this->appearance_ = *reinterpret_cast(record); - break; - } - case ESP_BLE_AD_TYPE_FLAG: { - // CSS 1.3 FLAGS - // "The Flags data type contains one bit Boolean flags. The Flags data type shall be included when any of the - // Flag bits are non-zero and the advertising packet is connectable, otherwise the Flags data type may be - // omitted." - // CSS 1: Optional in this context; shall not appear more than once in a block. - this->ad_flag_ = *record; - break; - } - // CSS 1.1 SERVICE UUID - // The Service UUID data type is used to include a list of Service or Service Class UUIDs. - // There are six data types defined for the three sizes of Service UUIDs that may be returned: - // CSS 1: Optional in this context (may appear more than once in a block). - case ESP_BLE_AD_TYPE_16SRV_CMPL: - case ESP_BLE_AD_TYPE_16SRV_PART: { - // • 16-bit Bluetooth Service UUIDs - for (uint8_t i = 0; i < record_length / 2; i++) { - this->service_uuids_.push_back(ESPBTUUID::from_uint16(*reinterpret_cast(record + 2 * i))); - } - break; - } - case ESP_BLE_AD_TYPE_32SRV_CMPL: - case ESP_BLE_AD_TYPE_32SRV_PART: { - // • 32-bit Bluetooth Service UUIDs - for (uint8_t i = 0; i < record_length / 4; i++) { - this->service_uuids_.push_back(ESPBTUUID::from_uint32(*reinterpret_cast(record + 4 * i))); - } - break; - } - case ESP_BLE_AD_TYPE_128SRV_CMPL: - case ESP_BLE_AD_TYPE_128SRV_PART: { - // • Global 128-bit Service UUIDs - this->service_uuids_.push_back(ESPBTUUID::from_raw(record)); - break; - } - case ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE: { - // CSS 1.4 MANUFACTURER SPECIFIC DATA - // "The Manufacturer Specific data type is used for manufacturer specific data. The first two data octets shall - // contain a company identifier from Assigned Numbers. The interpretation of any other octets within the data - // shall be defined by the manufacturer specified by the company identifier." - // CSS 1: Optional in this context (may appear more than once in a block). - if (record_length < 2) { - ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE"); - break; - } - ServiceData data{}; - data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast(record)); - data.data.assign(record + 2UL, record + record_length); - this->manufacturer_datas_.push_back(data); - break; - } - - // CSS 1.11 SERVICE DATA - // "The Service Data data type consists of a service UUID with the data associated with that service." - // CSS 1: Optional in this context (may appear more than once in a block). - case ESP_BLE_AD_TYPE_SERVICE_DATA: { - // «Service Data - 16 bit UUID» - // Size: 2 or more octets - // The first 2 octets contain the 16 bit Service UUID fol- lowed by additional service data - if (record_length < 2) { - ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_SERVICE_DATA"); - break; - } - ServiceData data{}; - data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast(record)); - data.data.assign(record + 2UL, record + record_length); - this->service_datas_.push_back(data); - break; - } - case ESP_BLE_AD_TYPE_32SERVICE_DATA: { - // «Service Data - 32 bit UUID» - // Size: 4 or more octets - // The first 4 octets contain the 32 bit Service UUID fol- lowed by additional service data - if (record_length < 4) { - ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_32SERVICE_DATA"); - break; - } - ServiceData data{}; - data.uuid = ESPBTUUID::from_uint32(*reinterpret_cast(record)); - data.data.assign(record + 4UL, record + record_length); - this->service_datas_.push_back(data); - break; - } - case ESP_BLE_AD_TYPE_128SERVICE_DATA: { - // «Service Data - 128 bit UUID» - // Size: 16 or more octets - // The first 16 octets contain the 128 bit Service UUID followed by additional service data - if (record_length < 16) { - ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_128SERVICE_DATA"); - break; - } - ServiceData data{}; - data.uuid = ESPBTUUID::from_raw(record); - data.data.assign(record + 16UL, record + record_length); - this->service_datas_.push_back(data); - break; - } - case ESP_BLE_AD_TYPE_INT_RANGE: - // Avoid logging this as it's very verbose - break; - default: { - ESP_LOGV(TAG, "Unhandled type: advType: 0x%02x", record_type); - break; - } - } - } -} - -std::string ESPBTDevice::address_str() const { - char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - return this->address_str_to(buf); -} - -uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } -#endif // USE_ESP32_BLE_DEVICE - void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); ESP_LOGCONFIG(TAG, @@ -721,102 +471,12 @@ void ESP32BLETracker::dump_config() { #ifdef USE_ESP32_BLE_DEVICE void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { - const uint64_t address = device.address_uint64(); - for (auto &disc : this->already_discovered_) { - if (disc == address) - return; - } - this->already_discovered_.push_back(address); - - char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - ESP_LOGD(TAG, "Found device %s RSSI=%d", device.address_str_to(addr_buf), device.get_rssi()); - - const char *address_type_s; - switch (device.get_address_type()) { - case BLE_ADDR_TYPE_PUBLIC: - address_type_s = "PUBLIC"; - break; - case BLE_ADDR_TYPE_RANDOM: - address_type_s = "RANDOM"; - break; - case BLE_ADDR_TYPE_RPA_PUBLIC: - address_type_s = "RPA_PUBLIC"; - break; - case BLE_ADDR_TYPE_RPA_RANDOM: - address_type_s = "RPA_RANDOM"; - break; - default: - address_type_s = "UNKNOWN"; - break; - } - - ESP_LOGD(TAG, " Address Type: %s", address_type_s); - if (!device.get_name().empty()) { - ESP_LOGD(TAG, " Name: '%s'", device.get_name().c_str()); - } - for (auto &tx_power : device.get_tx_powers()) { - ESP_LOGD(TAG, " TX Power: %d", tx_power); - } + // Shared implementation in ble_device_base — identical output and per-period + // MAC dedup on every tracker backend. + this->discovered_log_.log_device(TAG, device); } -bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { - static constexpr size_t AES_BLOCK_SIZE = 16; - static constexpr size_t AES_KEY_BITS = 128; - - uint8_t ecb_key[AES_BLOCK_SIZE]; - uint8_t ecb_plaintext[AES_BLOCK_SIZE]; - uint8_t ecb_ciphertext[AES_BLOCK_SIZE]; - - uint64_t addr64 = esp32_ble::ble_addr_to_uint64(this->address_); - - memcpy(&ecb_key, irk, AES_BLOCK_SIZE); - memset(&ecb_plaintext, 0, AES_BLOCK_SIZE); - - ecb_plaintext[13] = (addr64 >> 40) & 0xff; - ecb_plaintext[14] = (addr64 >> 32) & 0xff; - ecb_plaintext[15] = (addr64 >> 24) & 0xff; - -#ifdef USE_BLE_TRACKER_PSA_AES - // Use PSA Crypto API (mbedtls 4.0 / IDF 6.0+) - psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; - psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); - psa_set_key_bits(&attributes, AES_KEY_BITS); - psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT); - psa_set_key_algorithm(&attributes, PSA_ALG_ECB_NO_PADDING); - - mbedtls_svc_key_id_t key_id; - if (psa_import_key(&attributes, ecb_key, AES_BLOCK_SIZE, &key_id) != PSA_SUCCESS) { - return false; - } - - size_t output_length; - psa_status_t status = psa_cipher_encrypt(key_id, PSA_ALG_ECB_NO_PADDING, ecb_plaintext, AES_BLOCK_SIZE, - ecb_ciphertext, AES_BLOCK_SIZE, &output_length); - psa_destroy_key(key_id); - if (status != PSA_SUCCESS || output_length != AES_BLOCK_SIZE) { - return false; - } -#else - // Use legacy mbedtls AES API (IDF < 6.0) - mbedtls_aes_context ctx = {0, 0, {0}}; - mbedtls_aes_init(&ctx); - - if (mbedtls_aes_setkey_enc(&ctx, ecb_key, AES_KEY_BITS) != 0) { - mbedtls_aes_free(&ctx); - return false; - } - - if (mbedtls_aes_crypt_ecb(&ctx, ESP_AES_ENCRYPT, ecb_plaintext, ecb_ciphertext) != 0) { - mbedtls_aes_free(&ctx); - return false; - } - - mbedtls_aes_free(&ctx); -#endif - - return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && - ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); -} +// resolve_irk() is provided by ble_device_base (portable software AES). #endif // USE_ESP32_BLE_DEVICE @@ -848,6 +508,12 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { found = true; } #endif +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + for (auto *listener : this->neutral_listeners_) { + if (listener->parse_device(device)) + found = true; + } +#endif #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT for (auto *client : this->clients_) { @@ -867,7 +533,7 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { ESP_LOGV(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : ""); #ifdef USE_ESP32_BLE_DEVICE - this->already_discovered_.clear(); + this->discovered_log_.clear(); #endif // Reset timeout state machine instead of cancelling scheduler timeout this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; @@ -876,6 +542,10 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { for (auto *listener : this->listeners_) listener->on_scan_end(); #endif +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + for (auto *listener : this->neutral_listeners_) + listener->on_scan_end(); +#endif this->set_scanner_state_(ScannerState::IDLE); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 3415196a11..b0357289f1 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -12,13 +12,6 @@ #ifdef USE_ESP32 -#include -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) -// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls AES API. -// Use the PSA Crypto API instead. -#define USE_BLE_TRACKER_PSA_AES -#endif - #include #include #include @@ -26,6 +19,8 @@ #include #include +#include "esphome/components/ble_device_base/ble_device.h" +#include "esphome/components/ble_device_base/ble_hub.h" #include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_scan_result.h" @@ -38,7 +33,7 @@ namespace esphome::esp32_ble_tracker { using namespace esp32_ble; -using adv_data_t = std::vector; +using adv_data_t = ble_device_base::adv_data_t; enum AdvertisementParserType { PARSED_ADVERTISEMENTS, @@ -46,105 +41,27 @@ enum AdvertisementParserType { }; #ifdef USE_ESP32_BLE_UUID -struct ServiceData { - ESPBTUUID uuid; - adv_data_t data; -}; +using ServiceData = ble_device_base::ServiceData; #endif #ifdef USE_ESP32_BLE_DEVICE -class ESPBLEiBeacon { - public: - ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } - ESPBLEiBeacon(const uint8_t *data); - static optional from_manufacturer_data(const ServiceData &data); - - uint16_t get_major() { return byteswap(this->beacon_data_.major); } - uint16_t get_minor() { return byteswap(this->beacon_data_.minor); } - int8_t get_signal_power() { return this->beacon_data_.signal_power; } - ESPBTUUID get_uuid() { return ESPBTUUID::from_raw_reversed(this->beacon_data_.proximity_uuid); } - - protected: - struct { - uint8_t sub_type; - uint8_t length; - uint8_t proximity_uuid[16]; - uint16_t major; - uint16_t minor; - int8_t signal_power; - } PACKED beacon_data_; -}; - -class ESPBTDevice { - public: - void parse_scan_rst(const BLEScanResult &scan_result); - - std::string address_str() const; - - /// Format MAC address into provided buffer, returns pointer to buffer for convenience - const char *address_str_to(std::span buf) const { - format_mac_addr_upper(this->address_, buf.data()); - return buf.data(); - } - - uint64_t address_uint64() const; - - const uint8_t *address() const { return address_; } - - esp_ble_addr_type_t get_address_type() const { return this->address_type_; } - int get_rssi() const { return rssi_; } - const std::string &get_name() const { return this->name_; } - - const std::vector &get_tx_powers() const { return tx_powers_; } - - const optional &get_appearance() const { return appearance_; } - const optional &get_ad_flag() const { return ad_flag_; } - const std::vector &get_service_uuids() const { return service_uuids_; } - - const std::vector &get_manufacturer_datas() const { return manufacturer_datas_; } - - const std::vector &get_service_datas() const { return service_datas_; } - - // Exposed through a function for use in lambdas - const BLEScanResult &get_scan_result() const { return *scan_result_; } - - bool resolve_irk(const uint8_t *irk) const; - - optional get_ibeacon() const { - for (auto &it : this->manufacturer_datas_) { - auto res = ESPBLEiBeacon::from_manufacturer_data(it); - if (res.has_value()) - return res; - } - return {}; - } - - protected: - void parse_adv_(const uint8_t *payload, uint8_t len); - - esp_bd_addr_t address_{ - 0, - }; - esp_ble_addr_type_t address_type_{BLE_ADDR_TYPE_PUBLIC}; - int rssi_{0}; - std::string name_{}; - std::vector tx_powers_{}; - optional appearance_{}; - optional ad_flag_{}; - std::vector service_uuids_{}; - std::vector manufacturer_datas_{}; - std::vector service_datas_{}; - const BLEScanResult *scan_result_{nullptr}; -}; +// The advertisement device types are owned by the platform-neutral +// ble_device_base layer; re-exported here (esp32 only) for backward +// compatibility. ESPBTDevice::parse_scan_rst() (esp32-only) adapts BLEScanResult. +using ESPBLEiBeacon = ble_device_base::ESPBLEiBeacon; +using ESPBTDevice = ble_device_base::ESPBTDevice; #endif // USE_ESP32_BLE_DEVICE class ESP32BLETracker; -class ESPBTDeviceListener { +// esp32-flavored listener: the neutral parse_device/on_scan_end come from +// ble_device_base; this subclass adds the esp32-only raw-advertisement path +// (BLEScanResult batches) and the tracker back-pointer. +class ESPBTDeviceListener : public ble_device_base::ESPBTDeviceListener { public: - virtual void on_scan_end() {} -#ifdef USE_ESP32_BLE_DEVICE - virtual bool parse_device(const ESPBTDevice &device) = 0; +#ifndef USE_ESP32_BLE_DEVICE + // Raw-only build: no parsed-device support is compiled in. + bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; } #endif virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; }; virtual AdvertisementParserType get_advertisement_parser_type() { @@ -295,6 +212,7 @@ class ESPBTClient : public ESPBTDeviceListener { }; class ESP32BLETracker final : public Component, + public ble_device_base::BLEHub, #ifdef USE_OTA_STATE_LISTENER public ota::OTAGlobalStateListener, #endif @@ -314,10 +232,23 @@ class ESP32BLETracker final : public Component, void loop() override; + // esp32-flavored path (unmigrated esp32 sensors; sets the tracker back-pointer). void register_listener(ESPBTDeviceListener *listener); void register_client(ESPBTClient *client); void recalculate_advertisement_parser_types(); + // ---- ble_device_base::BLEHub (the platform-neutral tracker contract) ---- + void register_listener(ble_device_base::ESPBTDeviceListener *listener) override; + void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override { + this->raw_advertisement_callback_ = callback; + } + ble_device_base::HubCapabilities get_capabilities() const override { + return {/* active_scan = */ true, /* merges_scan_response = */ true, /* gatt = */ true}; + } + void get_adapter_mac(uint8_t out[6]) override; + bool scan_running() override { return this->scanner_state_ == ScannerState::RUNNING; } + bool scan_active() override { return this->scan_active_; } + #ifdef USE_ESP32_BLE_DEVICE void print_bt_device_info(const ESPBTDevice &device); #endif @@ -405,9 +336,15 @@ class ESP32BLETracker final : public Component, StaticVector clients_; #endif std::vector scanner_state_listeners_; + // Parsed listeners registered through the neutral BLEHub contract (migrated + // sensors); dispatched alongside listeners_. +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + StaticVector neutral_listeners_; +#endif + ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{}; #ifdef USE_ESP32_BLE_DEVICE - /// Vector of addresses that have already been printed in print_bt_device_info - std::vector already_discovered_; + /// Per-period "Found device" DEBUG log with MAC dedup (shared ble_device_base impl) + ble_device_base::DiscoveredDeviceLog discovered_log_; #endif // Group 2: Structs (aligned to 4 bytes) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 7f420f27d8..16e9d49782 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -18,6 +18,8 @@ from esphome.const import ( from esphome.cpp_generator import add_define CODEOWNERS = ["@swoboda1337"] +# esp32_ble raises the task watchdog around the remote BT controller bring-up +AUTO_LOAD = ["watchdog"] CONF_ACTIVE_HIGH = "active_high" CONF_BUS_WIDTH = "bus_width" diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index e6fcc018d9..6e3a4ef526 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -7,6 +7,10 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_PROVISIONING +#include "esphome/components/provisioning/provisioning.h" +#endif + #ifdef USE_ESP32 namespace esphome::esp32_improv { @@ -41,6 +45,15 @@ void ESP32ImprovComponent::setup() { #endif global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); +#ifdef USE_PROVISIONING + if (provisioning::global_provisioning_manager != nullptr) { + provisioning::global_provisioning_manager->add_on_closed_callback([this]() { + ESP_LOGD(TAG, "Provisioning window closed; stopping Improv"); + this->stop(); + }); + } +#endif + // Start with loop disabled - will be enabled by start() when needed this->disable_loop(); } @@ -282,6 +295,15 @@ void ESP32ImprovComponent::start() { if (this->should_start_ || this->state_ != improv::STATE_STOPPED) return; +#ifdef USE_PROVISIONING + // Don't (re)start advertising once the provisioning window has closed - e.g. when + // wifi tries to restart Improv after the window expired at runtime. + if (provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed()) { + ESP_LOGD(TAG, "Provisioning window closed; not starting Improv"); + return; + } +#endif + ESP_LOGD(TAG, "Setting Improv to start"); this->should_start_ = true; this->enable_loop(); @@ -338,6 +360,15 @@ void ESP32ImprovComponent::process_incoming_data_() { this->incoming_data_.clear(); return; } +#ifdef USE_PROVISIONING + if (provisioning::global_provisioning_manager != nullptr && + provisioning::global_provisioning_manager->closed()) { + ESP_LOGW(TAG, "Provisioning window closed; refusing settings"); + this->set_error_(improv::ERROR_NOT_AUTHORIZED); + this->incoming_data_.clear(); + return; + } +#endif if (wifi::global_wifi_component->is_disabled()) { // Wi-Fi is disabled, so we can't provision. Respond immediately // instead of letting the client wait out its provisioning timeout. diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index b658feb76a..618bf775a0 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +import platform import re import subprocess @@ -20,9 +21,16 @@ from esphome.const import ( PLATFORM_ESP8266, ThreadModel, ) -from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority +from esphome.core import ( + CORE, + CoroPriority, + EsphomeError, + Lambda, + coroutine_with_priority, +) from esphome.core.config import BOARD_MAX_LENGTH -from esphome.helpers import copy_file_if_changed +from esphome.helpers import IS_MACOS, copy_file_if_changed +from esphome.platformio.toolchain import copy_ccache_script from esphome.types import ConfigType from .boards import BOARDS, ESP8266_LD_SCRIPTS @@ -202,8 +210,12 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional( + CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY + ): cv.string_strict, + cv.Optional( + CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY + ): _parse_platform_version, } ), _arduino_check_versions, @@ -233,6 +245,32 @@ CONFIG_SCHEMA = cv.All( ) +def check_rosetta() -> None: + """Fail fast when the x86_64 ESP8266 toolchain cannot run on this Mac. + + There is no native arm64 build of the xtensa-lx106 toolchain; on Apple + Silicon it runs under Rosetta 2, which macOS updates can remove. + """ + if not IS_MACOS or platform.machine() != "arm64": + return + try: + result = subprocess.run( + ["/usr/bin/arch", "-x86_64", "/usr/bin/true"], + capture_output=True, + close_fds=False, + check=False, + ) + except OSError: + return # arch(1) unavailable; let the build proceed + if result.returncode != 0: + raise EsphomeError( + "ESP8266 builds on Apple Silicon Macs use an Intel (x86_64) " + "compiler that requires Rosetta 2, which is not installed on " + "this system. Install it with:\n" + " softwareupdate --install-rosetta --agree-to-license" + ) + + @coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config): cg.add(esp8266_ns.setup_preferences()) @@ -257,6 +295,7 @@ async def to_code(config): ) extra_scripts = [ + "pre:ccache.py", "pre:testing_mode.py", "pre:exclude_updater.py", "pre:exclude_waveform.py", @@ -332,6 +371,12 @@ async def to_code(config): for symbol in ("vprintf", "printf", "fprintf"): cg.add_build_flag(f"-Wl,--wrap={symbol}") + # Wrap the lwIP2 glue's do-nothing dhcp_cleanup()/dhcp_release() stubs so the + # linker can drop their "STUB: ..." message strings from DRAM. + # See lwip_glue_stubs.cpp for implementation. + for symbol in ("dhcp_cleanup", "dhcp_release"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") + # Wrap Arduino's millis() so all callers (including Arduino libraries and ISR # handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply # implementation in the Arduino ESP8266 core. @@ -400,31 +445,18 @@ async def finalize_serial_config() -> None: # Called by writer.py def copy_files() -> None: dir = Path(__file__).parent - post_build_file = dir / "post_build.py.script" - copy_file_if_changed( - post_build_file, - CORE.relative_build_path("post_build.py"), - ) - testing_mode_file = dir / "testing_mode.py.script" - copy_file_if_changed( - testing_mode_file, - CORE.relative_build_path("testing_mode.py"), - ) - exclude_updater_file = dir / "exclude_updater.py.script" - copy_file_if_changed( - exclude_updater_file, - CORE.relative_build_path("exclude_updater.py"), - ) - exclude_waveform_file = dir / "exclude_waveform.py.script" - copy_file_if_changed( - exclude_waveform_file, - CORE.relative_build_path("exclude_waveform.py"), - ) - remove_float_scanf_file = dir / "remove_float_scanf.py.script" - copy_file_if_changed( - remove_float_scanf_file, - CORE.relative_build_path("remove_float_scanf.py"), - ) + for script in ( + "post_build", + "testing_mode", + "exclude_updater", + "exclude_waveform", + "remove_float_scanf", + ): + copy_file_if_changed( + dir / f"{script}.py.script", + CORE.relative_build_path(f"{script}.py"), + ) + copy_ccache_script() # ESP logs stack trace decoder, based on https://github.com/me-no-dev/EspExceptionDecoder diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp index aadfc31197..2e0a00325c 100644 --- a/esphome/components/esp8266/helpers.cpp +++ b/esphome/components/esp8266/helpers.cpp @@ -12,7 +12,7 @@ namespace esphome { uint32_t random_uint32() { return os_random(); } bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; } -// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2040) is set, +// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2) is set, // independent of the ESPHOME_THREAD_SINGLE thread model define. IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } diff --git a/esphome/components/esp8266/lwip_glue_stubs.cpp b/esphome/components/esp8266/lwip_glue_stubs.cpp new file mode 100644 index 0000000000..a86c8d75a2 --- /dev/null +++ b/esphome/components/esp8266/lwip_glue_stubs.cpp @@ -0,0 +1,35 @@ +/* + * Linker wrap stubs for the lwIP2 glue's dead DHCP entry points. + * + * The ESP8266 SDK blobs call dhcp_cleanup() and dhcp_release() when the + * station leaves an access point (cnx_sta_leave, wifi_station_dhcpc_stop). + * In the prebuilt lwIP2 glue (liblwip2-*.a, glue-esp/lwip-esp.c) these are + * stubs whose only effect is printing "STUB: dhcp_cleanup" and + * "STUB: dhcp_release"; the real DHCP teardown happens through lwIP2's + * renamed dhcp_cleanup_LWIP2()/dhcp_release_LWIP2() functions. + * + * On ESP8266 .rodata lives in DRAM, so those message strings waste scarce + * RAM. Wrapping the stubs with silent equivalents lets the linker garbage + * collect the glue stub bodies together with their strings. + * + * Saves 38 bytes of RAM and removes the "STUB:" log noise on Wi-Fi + * disconnect. Behavior is otherwise unchanged. + */ + +#if defined(USE_ESP8266) + +namespace esphome::esp8266 {} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +// The callers are closed-source SDK blobs; the netif argument is unused. +void __wrap_dhcp_cleanup(void * /*netif*/) {} + +// The glue stub returns ERR_ABRT (-8; lwIP 1.4 err_t is a signed char). +signed char __wrap_dhcp_release(void * /*netif*/) { return -8; } + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 696f83bce1..d954ae4a0f 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -8,6 +8,7 @@ extern "C" { #include "preferences.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/preferences_rtc.h" #include @@ -80,16 +81,6 @@ static uint32_t get_esp8266_flash_sector() { } static uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; } -static inline size_t bytes_to_words(size_t bytes) { return (bytes + 3) / 4; } - -template uint32_t calculate_crc(It first, It last, uint32_t type) { - uint32_t crc = type; - while (first != last) { - crc ^= (*first++ * 2654435769UL) >> 1; - } - return crc; -} - static bool save_to_flash(size_t offset, const uint32_t *data, size_t len) { for (uint32_t i = 0; i < len; i++) { uint32_t j = offset + i; @@ -137,21 +128,19 @@ static constexpr size_t PREF_MAX_BUFFER_WORDS = ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS; bool ESP8266PreferenceBackend::save(const uint8_t *data, size_t len) { - if (bytes_to_words(len) != this->length_words) + if (rtc_pref_bytes_to_words(len) != this->length_words) return false; const size_t buffer_size = static_cast(this->length_words) + 1; if (buffer_size > PREF_MAX_BUFFER_WORDS) return false; uint32_t buffer[PREF_MAX_BUFFER_WORDS]; - memset(buffer, 0, buffer_size * sizeof(uint32_t)); - memcpy(buffer, data, len); - buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type); + rtc_pref_encode(buffer, this->type, this->length_words, data, len); return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size) : save_to_rtc(this->offset, buffer, buffer_size); } bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) { - if (bytes_to_words(len) != this->length_words) + if (rtc_pref_bytes_to_words(len) != this->length_words) return false; const size_t buffer_size = static_cast(this->length_words) + 1; if (buffer_size > PREF_MAX_BUFFER_WORDS) @@ -161,10 +150,7 @@ bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) { : load_from_rtc(this->offset, buffer, buffer_size); if (!ret) return false; - if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type)) - return false; - memcpy(data, buffer, len); - return true; + return rtc_pref_decode(buffer, this->type, this->length_words, data, len); } void ESP8266Preferences::setup() { @@ -177,13 +163,13 @@ void ESP8266Preferences::setup() { } ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t type, bool in_flash) { - const uint32_t length_words = bytes_to_words(length); + const uint32_t length_words = rtc_pref_bytes_to_words(length); if (length_words > MAX_PREFERENCE_WORDS) { ESP_LOGE(TAG, "Preference too large: %u words", static_cast(length_words)); return {}; } - const uint32_t total_words = length_words + 1; // +1 for CRC + const uint32_t total_words = length_words + 1; // +1 for checksum uint16_t offset; if (in_flash) { diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 66a33e1935..d5f4efbc02 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -126,7 +126,7 @@ CONFIG_SCHEMA = cv.All( CONF_PORT, esp8266=8266, esp32=3232, - rp2040=2040, + rp2=2040, bk72xx=8892, ln882x=8820, rtl87xx=8892, diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index fb0cc2e56d..cab725f704 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -8,7 +8,7 @@ #include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_arduino_libretiny.h" -#include "esphome/components/ota/ota_backend_arduino_rp2040.h" +#include "esphome/components/ota/ota_backend_arduino_rp2.h" #include "esphome/components/ota/ota_backend_esp_idf.h" #include "esphome/core/application.h" #include "esphome/core/hal.h" @@ -94,11 +94,12 @@ void ESPHomeOTAComponent::setup() { } void ESPHomeOTAComponent::dump_config() { + char addr_buf[network::USE_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "Over-The-Air updates:\n" " Address: %s:%u\n" " Version: %d", - network::get_use_address(), this->port_, USE_OTA_VERSION); + network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION); #ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { ESP_LOGCONFIG(TAG, " Password configured"); diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 13f278d3bc..c6c90ed67a 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -41,7 +41,7 @@ DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action) ESPNowHandlerTrigger = automation.Trigger.template( ESPNowRecvInfoConstRef, cg.uint8.operator("const").operator("ptr"), - cg.uint8, + cg.uint16, ) OnUnknownPeerTrigger = espnow_ns.class_( @@ -56,6 +56,20 @@ OnBroadcastTrigger = espnow_ns.class_( CONF_AUTO_ADD_PEER = "auto_add_peer" +CONF_MAX_PAYLOAD_SIZE = "max_payload_size" + +# Payload limits of ESP-NOW v1 and v2 frames. The radio negotiates the +# protocol version per peer on its own; the option only sizes this device's +# packet buffers, whose static RAM cost is proportional to it (~8 KB at 250 +# bytes, ~44 KB at 1470). +ESPNOW_PAYLOAD_V1 = 250 +ESPNOW_PAYLOAD_V2 = 1470 + +# Config-time cap for action payloads. The per-device limit is the +# ``max_payload_size`` option, which the action schema cannot see; send() +# enforces it at runtime. +MAX_ESPNOW_PACKET_SIZE = ESPNOW_PAYLOAD_V2 + CONF_PEERS = "peers" CONF_ON_SENT = "on_sent" CONF_ON_UNKNOWN_PEER = "on_unknown_peer" @@ -63,7 +77,15 @@ CONF_ON_BROADCAST = "on_broadcast" CONF_CONTINUE_ON_ERROR = "continue_on_error" CONF_WAIT_FOR_SENT = "wait_for_sent" -MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes + +def _validate_max_payload_size(value: int) -> int: + if value > ESPNOW_PAYLOAD_V1: + return cv.require_framework_version( + esp_idf=cv.Version(5, 4, 0), + esp32_arduino=cv.Version(3, 2, 0), + extra_message="ESP-NOW v2 frames need an ESP-NOW v2 capable framework", + )(value) + return value def validate_channel(value): @@ -78,6 +100,9 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(ESPNowComponent), cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): validate_channel, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, + cv.Optional(CONF_MAX_PAYLOAD_SIZE, default=ESPNOW_PAYLOAD_V1): cv.All( + cv.int_range(min=1, max=ESPNOW_PAYLOAD_V2), _validate_max_payload_size + ), cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean, cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address), cv.Optional(CONF_ON_UNKNOWN_PEER): automation.validate_automation( @@ -113,7 +138,7 @@ async def _trigger_to_code(config): [ (ESPNowRecvInfoConstRef, "info"), (cg.uint8.operator("const").operator("ptr"), "data"), - (cg.uint8, "size"), + (cg.uint16, "size"), ], config, ) @@ -125,6 +150,7 @@ async def to_code(config): await cg.register_component(var, config) cg.add_define("USE_ESPNOW") + cg.add_define("USE_ESPNOW_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE]) if wifi_channel := config.get(CONF_CHANNEL): cg.add(var.set_wifi_channel(wifi_channel)) diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 5e995aff53..e4d01bb1a8 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -119,7 +119,7 @@ template class SetChannelAction final : public Action, pu } }; -class OnReceiveTrigger final : public Trigger, +class OnReceiveTrigger final : public Trigger, public ESPNowReceivedPacketHandler { public: explicit OnReceiveTrigger(std::array address) : has_address_(true) { @@ -128,7 +128,7 @@ class OnReceiveTrigger final : public Triggerhas_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0); if (!match) return false; @@ -141,15 +141,15 @@ class OnReceiveTrigger final : public Trigger, +class OnUnknownPeerTrigger final : public Trigger, public ESPNowUnknownPeerHandler { public: - bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override { + bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override { this->trigger(info, data, size); return false; // Return false to continue processing other internal handlers } }; -class OnBroadcastTrigger final : public Trigger, +class OnBroadcastTrigger final : public Trigger, public ESPNowBroadcastHandler { public: explicit OnBroadcastTrigger(std::array address) : has_address_(true) { @@ -157,7 +157,7 @@ class OnBroadcastTrigger final : public Triggerhas_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0); if (!match) return false; diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 91f2c067ca..f28d7f3354 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -4,6 +4,7 @@ #include "espnow_err.h" +#include #include #include "esphome/core/application.h" @@ -96,9 +97,9 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { // Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a // v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B), - // but our receive buffer is ESP_NOW_MAX_DATA_LEN (250 B); copying a larger - // frame would overflow packet_.receive.data. - if (size < 0 || size > ESP_NOW_MAX_DATA_LEN) { + // but the receive buffer only fits v2 frames with ``max_payload_size``; copying a + // larger frame would overflow packet_.receive.data. + if (size < 0 || size > ESPNOW_MAX_DATA_LEN) { global_esp_now->receive_packet_queue_.increment_dropped_count(); return; } @@ -285,11 +286,14 @@ void ESPNowComponent::loop() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + // Cap the hex dump at a v1 frame: a full v2 frame would need a + // ~4.4 KB stack buffer. char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)]; format_mac_addr_upper(info.src_addr, src_buf); format_mac_addr_upper(info.des_addr, dst_buf); ESP_LOGV(TAG, "<<< [%s -> %s] %s", src_buf, dst_buf, - format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size)); + format_hex_pretty_to(hex_buf, packet->packet_.receive.data, + std::min(packet->packet_.receive.size, ESP_NOW_MAX_DATA_LEN))); #endif if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { for (auto *handler : this->broadcast_handlers_) { @@ -362,7 +366,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl return ESP_ERR_ESPNOW_PEER_NOT_SET; } else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) { return ESP_ERR_ESPNOW_OWN_ADDRESS; - } else if (size > ESP_NOW_MAX_DATA_LEN) { + } else if (size > ESPNOW_MAX_DATA_LEN) { return ESP_ERR_ESPNOW_DATA_SIZE; } else if (!esp_now_is_peer_exist(peer_address)) { if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) { diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h index eacc3eb886..d95255c5df 100644 --- a/esphome/components/espnow/espnow_component.h +++ b/esphome/components/espnow/espnow_component.h @@ -62,7 +62,7 @@ class ESPNowUnknownPeerHandler { /// @param data Pointer to the received data payload /// @param size Size of the received data in bytes /// @return true if the packet was handled, false otherwise - virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; + virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0; }; /// Handler interface for receiving ESPNow packets @@ -74,7 +74,7 @@ class ESPNowReceivedPacketHandler { /// @param data Pointer to the received data payload /// @param size Size of the received data in bytes /// @return true if the packet was handled, false otherwise - virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; + virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0; }; /// Handler interface for receiving ESPNow broadcast packets /// Components should inherit from this class to handle incoming ESPNow data @@ -85,7 +85,7 @@ class ESPNowBroadcastHandler { /// @param data Pointer to the received data payload /// @param size Size of the received data in bytes /// @return true if the packet was handled, false otherwise - virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; + virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0; }; class ESPNowComponent final : public Component { diff --git a/esphome/components/espnow/espnow_packet.h b/esphome/components/espnow/espnow_packet.h index b6192a0d41..fb125864fb 100644 --- a/esphome/components/espnow/espnow_packet.h +++ b/esphome/components/espnow/espnow_packet.h @@ -19,6 +19,23 @@ namespace esphome::espnow { static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE}; +// Maximum payload this component sends and receives, from the +// ``max_payload_size`` option. The radio stack speaks ESP-NOW v2 regardless +// (negotiated per peer); payloads beyond the v1 limit (250 bytes) are opt-in +// because the packet pools are statically sized from this, so their RAM cost +// is proportional (~8 KB at 250 bytes, ~44 KB at the v2 limit of 1470). +#ifndef USE_ESPNOW_MAX_PAYLOAD_SIZE +#define USE_ESPNOW_MAX_PAYLOAD_SIZE ESP_NOW_MAX_DATA_LEN +#endif +static constexpr uint16_t ESPNOW_MAX_DATA_LEN = USE_ESPNOW_MAX_PAYLOAD_SIZE; +#ifdef ESP_NOW_MAX_DATA_LEN_V2 +static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN_V2, + "espnow max_payload_size cannot exceed the ESP-NOW v2 frame limit"); +#else +static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN, + "espnow max_payload_size beyond 250 bytes requires an ESP-IDF with ESP-NOW v2 support (5.4+)"); +#endif + struct WifiPacketRxControl { int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or @@ -78,10 +95,10 @@ class ESPNowPacket { union { // NOLINTNEXTLINE(readability-identifier-naming) struct received_data { - ESPNowRecvInfo info; // Information about the received packet - uint8_t data[ESP_NOW_MAX_DATA_LEN]; // Data received in the packet - uint8_t size; // Size of the received data - WifiPacketRxControl rx_ctrl; // Status of the received packet + ESPNowRecvInfo info; // Information about the received packet + uint8_t data[ESPNOW_MAX_DATA_LEN]; // Data received in the packet + uint16_t size; // Size of the received data + WifiPacketRxControl rx_ctrl; // Status of the received packet } receive; // NOLINTNEXTLINE(readability-identifier-naming) @@ -144,15 +161,15 @@ class ESPNowSendPacket { this->callback_ = nullptr; // Reset callback } - uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to - uint8_t data_[ESP_NOW_MAX_DATA_LEN]{0}; // Data to send - uint8_t size_{0}; // Size of the data to send, must be <= ESP_NOW_MAX_DATA_LEN - send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete + uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to + uint8_t data_[ESPNOW_MAX_DATA_LEN]{0}; // Data to send + uint16_t size_{0}; // Size of the data to send, must be <= ESPNOW_MAX_DATA_LEN + send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete private: void init_data_(const uint8_t *peer_address, const uint8_t *payload, size_t size) { memcpy(this->address_, peer_address, ESP_NOW_ETH_ALEN); - if (size > ESP_NOW_MAX_DATA_LEN) { + if (size > ESPNOW_MAX_DATA_LEN) { this->size_ = 0; return; } diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 1e37073321..b7686f23d6 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -42,8 +42,8 @@ void ESPNowTransport::send_packet(const std::vector &buf) const { return; } - if (buf.size() > ESP_NOW_MAX_DATA_LEN) { - ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN); + if (buf.size() > ESPNOW_MAX_DATA_LEN) { + ESP_LOGE(TAG, "Packet too large: %zu bytes (max %u)", buf.size(), (unsigned) ESPNOW_MAX_DATA_LEN); return; } @@ -55,8 +55,8 @@ void ESPNowTransport::send_packet(const std::vector &buf) const { }); } -bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { - ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0], +bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) { + ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size, info.src_addr[0], info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); if (data == nullptr || size == 0) { @@ -70,9 +70,9 @@ bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data return false; // Allow other handlers to run } -bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { - ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0], - info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); +bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) { + ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size, + info.src_addr[0], info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); if (data == nullptr || size == 0) { ESP_LOGW(TAG, "Received empty or null broadcast packet"); diff --git a/esphome/components/espnow/packet_transport/espnow_transport.h b/esphome/components/espnow/packet_transport/espnow_transport.h index 7e1d08618b..51069b6415 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.h +++ b/esphome/components/espnow/packet_transport/espnow_transport.h @@ -24,12 +24,12 @@ class ESPNowTransport final : public packet_transport::PacketTransport, } // ESPNow handler interface - bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; - bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; + bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override; + bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override; protected: void send_packet(const std::vector &buf) const override; - size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; } + size_t get_max_packet_size() override { return ESPNOW_MAX_DATA_LEN; } bool should_send() override; peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}}; diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 8f927cf3e9..d1d5e45c6b 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -4,7 +4,12 @@ import logging from esphome import automation, pins from esphome.automation import Condition import esphome.codegen as cg -from esphome.components.network import ip_address_literal +from esphome.components.network import ( + add_use_address, + get_network_priority, + get_priority_interfaces_from_full_config, + ip_address_literal, +) from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -50,7 +55,6 @@ from esphome.core import ( import esphome.final_validate as fv from esphome.types import ConfigType -CONFLICTS_WITH = ["wifi"] AUTO_LOAD = ["network"] LOGGER = logging.getLogger(__name__) @@ -179,9 +183,11 @@ _ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"} # ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver) SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"} -# RP2040-supported ethernet types (SPI and PIO QSPI) -RP2040_ETHERNET_TYPES = {"W5100", "W5500", "W6100", "W6300", "ENC28J60"} -_RP2040_SPI_LIBRARIES = { +# RP2-supported ethernet types (SPI and PIO QSPI). Applies to the whole +# RP2 family (RP2040 and RP2350); the chip-specific W5100 caveat in the +# comment above is about ESP-IDF driver coverage, not the RP2 platform. +RP2_ETHERNET_TYPES = {"W5100", "W5500", "W6100", "W6300", "ENC28J60"} +_RP2_SPI_LIBRARIES = { "W5100": "lwIP_w5100", "W5500": "lwIP_w5500", "ENC28J60": "lwIP_enc28j60", @@ -361,10 +367,10 @@ def _validate(config): f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported " f"on ESP32 classic and ESP32-P4, not {variant}" ) - elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_ETHERNET_TYPES: + elif CORE.is_rp2 and config[CONF_TYPE] not in RP2_ETHERNET_TYPES: raise cv.Invalid( - f"Only {', '.join(sorted(RP2040_ETHERNET_TYPES))} are supported on RP2040, " - f"not {config[CONF_TYPE]}" + f"Only {', '.join(sorted(RP2_ETHERNET_TYPES))} are supported on the RP2 " + f"platform, not {config[CONF_TYPE]}" ) return config @@ -431,37 +437,48 @@ GENERIC_SCHEMA = cv.All( cv.only_on([Platform.ESP32]), ) -SPI_SCHEMA = cv.All( - BASE_SCHEMA.extend( - cv.Schema( - { - cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number, - cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, - cv.SplitDefault(CONF_CLOCK_SPEED, esp32="26.67MHz"): cv.All( - cv.only_on_esp32, - cv.frequency, - cv.int_range(int(8e6), int(80e6)), - ), - cv.Optional(CONF_INTERFACE): cv.All( - cv.only_on_esp32, - cv.one_of(*SPI_INTERFACE_MAP.keys(), lower=True), - ), - # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() - cv.Optional(CONF_POLLING_INTERVAL): cv.All( - cv.only_on_esp32, - cv.positive_time_period_milliseconds, - cv.Range(min=TimePeriodMilliseconds(milliseconds=1)), - ), - } + +def _spi_schema(default_clock: str = "26.67MHz", max_clock: int = int(80e6)): + return cv.All( + BASE_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, + cv.Optional( + CONF_INTERRUPT_PIN + ): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.SplitDefault(CONF_CLOCK_SPEED, esp32=default_clock): cv.All( + cv.only_on_esp32, + cv.frequency, + cv.int_range(int(8e6), max_clock), + ), + cv.Optional(CONF_INTERFACE): cv.All( + cv.only_on_esp32, + cv.one_of(*SPI_INTERFACE_MAP.keys(), lower=True), + ), + # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() + cv.Optional(CONF_POLLING_INTERVAL): cv.All( + cv.only_on_esp32, + cv.positive_time_period_milliseconds, + cv.Range(min=TimePeriodMilliseconds(milliseconds=1)), + ), + } + ), ), - ), - cv.only_on([Platform.ESP32, Platform.RP2040]), - _validate_spi_interface, -) + cv.only_on([Platform.ESP32, Platform.RP2]), + _validate_spi_interface, + ) + + +SPI_SCHEMA = _spi_schema() + +# The ENC28J60's SCK maximum is 20 MHz, so the shared 26.67 MHz default is out +# of spec for it and makes the driver's CS hold time helper compute no hold +SPI_SCHEMA_ENC28J60 = _spi_schema(default_clock="20MHz", max_clock=int(20e6)) CONFIG_SCHEMA = cv.All( cv.typed_schema( @@ -473,13 +490,13 @@ CONFIG_SCHEMA = cv.All( "JL1101": RMII_SCHEMA, "KSZ8081": RMII_SCHEMA, "KSZ8081RNA": RMII_SCHEMA, - "W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])), + "W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])), "W5500": SPI_SCHEMA, "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, - "ENC28J60": SPI_SCHEMA, - "W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])), - "W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])), + "ENC28J60": SPI_SCHEMA_ENC28J60, + "W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])), + "W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])), "LAN8670": RMII_SCHEMA, "GENERIC": GENERIC_SCHEMA, "YT8531": GENERIC_SCHEMA, @@ -533,15 +550,23 @@ def phy_register(address: int, value: int, page: int): @coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + + # Apply network priority before register_component (which emits the user's + # explicit setup_priority: if set) so that, as in wifi, an explicit + # setup_priority: still wins over the network-priority-derived value. + prio = get_network_priority("ethernet") + if prio is not None: + cg.set_setup_priority(var, prio) + await cg.register_component(var, config) if CORE.is_esp32: await _to_code_esp32(var, config) - elif CORE.is_rp2040: + elif CORE.is_rp2: await _to_code_rp2040(var, config) cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) - cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) + add_use_address(var, config[CONF_USE_ADDRESS]) # enable_on_boot defaults to true in C++ - only set if false if not config[CONF_ENABLE_ON_BOOT]: cg.add(var.set_enable_on_boot(False)) @@ -642,8 +667,9 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: ) cg.add(var.add_phy_register(reg)) - # Register Ethernet with the esp32 sdkconfig reconciler, which disables the - # WiFi stack and WiFi/BT coexistence when Ethernet is used without WiFi. + # Register Ethernet with the esp32 sdkconfig reconciler. It disables the + # WiFi stack and WiFi/BT coexistence only when Ethernet runs without WiFi, + # so multi-interface configs (network: priority: with both) keep WiFi. request_ethernet() # Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time) @@ -670,7 +696,7 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) cg.add_define("USE_ETHERNET_SPI") - cg.add_library(_RP2040_SPI_LIBRARIES[config[CONF_TYPE]], None) + cg.add_library(_RP2_SPI_LIBRARIES[config[CONF_TYPE]], None) def _final_validate_rmii_pins(config: ConfigType) -> None: @@ -730,6 +756,22 @@ def _final_validate_rmii_pins(config: ConfigType) -> None: def _final_validate(config: ConfigType) -> ConfigType: """Final validation for Ethernet component.""" + # Allow ethernet + wifi coexistence only when both are declared in network: priority:. + if "wifi" in fv.full_config.get(): + priority_ifaces = get_priority_interfaces_from_full_config(fv.full_config.get()) + missing = [i for i in ("ethernet", "wifi") if i not in priority_ifaces] + if missing and priority_ifaces: + # A priority list exists but is incomplete: point at what to add. + raise cv.Invalid( + "When ethernet and wifi are used together, 'network: priority:' must " + f"list both interfaces; missing: {', '.join(missing)}" + ) + if missing: + raise cv.Invalid( + "Component ethernet cannot be used together with component wifi " + "unless both are listed under 'network: priority:'" + ) + _final_validate_spi(config) _final_validate_rmii_pins(config) return config @@ -752,7 +794,7 @@ _platform_filter = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO, }, - "ethernet_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "ethernet_component_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, "esp_eth_phy_jl1101.c": { PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO, diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index e0fe920ea1..9f4398c621 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -25,7 +25,7 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #endif #endif // USE_ESP32 -#ifdef USE_RP2040 +#ifdef USE_RP2 #if defined(USE_ETHERNET_W5500) #include #elif defined(USE_ETHERNET_W5100) @@ -112,8 +112,10 @@ enum class EthernetComponentState : uint8_t { // Platform-neutral duplex/speed types #ifndef USE_ESP32 +// NOLINTBEGIN(readability-identifier-naming) enum eth_duplex_t { ETH_DUPLEX_HALF, ETH_DUPLEX_FULL }; enum eth_speed_t { ETH_SPEED_10M, ETH_SPEED_100M }; +// NOLINTEND(readability-identifier-naming) #endif class EthernetComponent final : public Component { @@ -145,6 +147,8 @@ class EthernetComponent final : public Component { network::IPAddresses get_ip_addresses(); network::IPAddress get_dns_address(uint8_t num); + /// Returns nullptr when no explicit use_address is configured and the address is + /// derived at runtime from the device name (see network::get_use_address_to()). const char *get_use_address() const { return this->use_address_; } void set_use_address(const char *use_address) { this->use_address_ = use_address; } void get_eth_mac_address_raw(uint8_t *mac); @@ -182,14 +186,14 @@ class EthernetComponent final : public Component { #endif // USE_ETHERNET_SPI #endif // USE_ESP32 -#ifdef USE_RP2040 +#ifdef USE_RP2 void set_clk_pin(uint8_t clk_pin); void set_miso_pin(uint8_t miso_pin); void set_mosi_pin(uint8_t mosi_pin); void set_cs_pin(uint8_t cs_pin); void set_interrupt_pin(int8_t interrupt_pin); void set_reset_pin(int8_t reset_pin); -#endif // USE_RP2040 +#endif // USE_RP2 #ifdef USE_ETHERNET_IP_STATE_LISTENERS void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } @@ -272,7 +276,7 @@ class EthernetComponent final : public Component { esp_eth_phy_t *phy_{nullptr}; #endif // USE_ESP32 -#ifdef USE_RP2040 +#ifdef USE_RP2 static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls #if defined(USE_ETHERNET_W5100) static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time @@ -301,7 +305,7 @@ class EthernetComponent final : public Component { uint8_t cs_pin_; int8_t interrupt_pin_{-1}; int8_t reset_pin_{-1}; -#endif // USE_RP2040 +#endif // USE_RP2 // Common members #ifdef USE_ETHERNET_MANUAL_IP @@ -346,7 +350,7 @@ class EthernetComponent final : public Component { private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. - const char *use_address_{""}; + const char *use_address_{nullptr}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index 5ad1e7d483..94f4c23479 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -232,8 +232,10 @@ void EthernetComponent::ethernet_lazy_init_() { dm9051_config.poll_period_ms = this->polling_interval_; #endif #elif defined(USE_ETHERNET_ENC28J60) + // ENC28J60 does not support poll_period_ms. CS must stay asserted for the chip's CS hold + // time (t10, 210 ns) after the last clock or MAC/MII register reads fail ("wrong chip ID") + enc28j60_config.spi_devcfg->cs_ena_posttrans = enc28j60_cal_spi_cs_hold_time((this->clock_speed_ + 999999) / 1000000); enc28j60_config.int_gpio_num = this->interrupt_pin_; - // ENC28J60 does not support poll_period_ms #endif phy_config.phy_addr = this->phy_addr_spi_; diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2.cpp similarity index 97% rename from esphome/components/ethernet/ethernet_component_rp2040.cpp rename to esphome/components/ethernet/ethernet_component_rp2.cpp index 250297ddb5..4d6d6c4f5b 100644 --- a/esphome/components/ethernet/ethernet_component_rp2040.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2.cpp @@ -1,12 +1,12 @@ #include "ethernet_component.h" -#if defined(USE_ETHERNET) && defined(USE_RP2040) +#if defined(USE_ETHERNET) && defined(USE_RP2) #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/components/rp2040/gpio.h" +#include "esphome/components/rp2/gpio.h" #include #include @@ -29,7 +29,7 @@ void EthernetComponent::setup() { // Toggle reset pin if configured if (this->reset_pin_ >= 0) { - rp2040::RP2040GPIOPin reset_pin; + rp2::RP2GPIOPin reset_pin; reset_pin.set_pin(this->reset_pin_); reset_pin.set_flags(gpio::FLAG_OUTPUT); reset_pin.setup(); @@ -187,17 +187,18 @@ void EthernetComponent::loop() { } void EthernetComponent::dump_config() { - const char *type_str = "Unknown"; #if defined(USE_ETHERNET_W5500) - type_str = "W5500"; + const char *type_str = "W5500"; #elif defined(USE_ETHERNET_W5100) - type_str = "W5100"; + const char *type_str = "W5100"; #elif defined(USE_ETHERNET_W6100) - type_str = "W6100"; + const char *type_str = "W6100"; #elif defined(USE_ETHERNET_W6300) - type_str = "W6300"; + const char *type_str = "W6300"; #elif defined(USE_ETHERNET_ENC28J60) - type_str = "ENC28J60"; + const char *type_str = "ENC28J60"; +#else + const char *type_str = "Unknown"; #endif #if defined(USE_ETHERNET_W6300) // W6300 uses PIO QSPI with hardcoded pins — SPI pin fields are not used @@ -380,4 +381,4 @@ void EthernetComponent::disable() { } // namespace esphome::ethernet -#endif // USE_ETHERNET && USE_RP2040 +#endif // USE_ETHERNET && USE_RP2 diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 4cab1bff9b..e205e4b910 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -50,7 +50,9 @@ _EVENT_SCHEMA = ( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent), cv.GenerateID(): cv.declare_id(Event), - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): validate_device_class, cv.Optional(CONF_ON_EVENT): automation.validate_automation({}), } ) diff --git a/esphome/components/ezo_pmp/sensor.py b/esphome/components/ezo_pmp/sensor.py index a0473b292c..ed4efeeabc 100644 --- a/esphome/components/ezo_pmp/sensor.py +++ b/esphome/components/ezo_pmp/sensor.py @@ -23,8 +23,8 @@ CONF_PUMP_VOLTAGE = "pump_voltage" CONF_LAST_VOLUME_REQUESTED = "last_volume_requested" CONF_MAX_FLOW_RATE = "max_flow_rate" -UNIT_MILILITER = "ml" -UNIT_MILILITERS_PER_MINUTE = "ml/min" +UNIT_MILILITER = "mL" +UNIT_MILILITERS_PER_MINUTE = "mL/min" CONFIG_SCHEMA = cv.Schema( { diff --git a/esphome/components/factory_reset/factory_reset.cpp b/esphome/components/factory_reset/factory_reset.cpp index cd4134e9ae..bceaf6e40f 100644 --- a/esphome/components/factory_reset/factory_reset.cpp +++ b/esphome/components/factory_reset/factory_reset.cpp @@ -7,7 +7,7 @@ #include -#if !defined(USE_RP2040) && !defined(USE_HOST) +#if !defined(USE_RP2) && !defined(USE_HOST) namespace esphome::factory_reset { @@ -73,4 +73,4 @@ void FactoryResetComponent::setup() { } // namespace esphome::factory_reset -#endif // !defined(USE_RP2040) && !defined(USE_HOST) +#endif // !defined(USE_RP2) && !defined(USE_HOST) diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h index d80d2d2406..b0a899c719 100644 --- a/esphome/components/factory_reset/factory_reset.h +++ b/esphome/components/factory_reset/factory_reset.h @@ -3,7 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/core/preferences.h" -#if !defined(USE_RP2040) && !defined(USE_HOST) +#if !defined(USE_RP2) && !defined(USE_HOST) #ifdef USE_ESP32 #include @@ -32,4 +32,4 @@ class FactoryResetComponent final : public Component { } // namespace esphome::factory_reset -#endif // !defined(USE_RP2040) && !defined(USE_HOST) +#endif // !defined(USE_RP2) && !defined(USE_HOST) diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index 0fa69a23b4..da4dbf2ed7 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY) #include "fastled_light.h" #include "esphome/core/log.h" diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index 1261b742a1..9f903b4530 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY) #include "esphome/core/component.h" #include "esphome/core/helpers.h" diff --git a/esphome/components/file/__init__.py b/esphome/components/file/__init__.py new file mode 100644 index 0000000000..f70ffa9520 --- /dev/null +++ b/esphome/components/file/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/file/image.py b/esphome/components/file/image.py new file mode 100644 index 0000000000..9a7c762a79 --- /dev/null +++ b/esphome/components/file/image.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +import contextlib +import hashlib +import io +import logging +from pathlib import Path +import re + +from PIL import Image, UnidentifiedImageError + +from esphome import core, external_files +import esphome.codegen as cg +from esphome.components.const import CONF_BYTE_ORDER +from esphome.components.image import ( + CONF_INVERT_ALPHA, + CONF_OPAQUE, + CONF_TRANSPARENCY, + DOMAIN, + IMAGE_TYPE, + Image_, + ImageEncoder, + add_metadata, + get_image_type_enum, + get_transparency_enum, + is_svg_file, + validate_settings, + validate_transparency, + validate_type, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_DITHER, + CONF_FILE, + CONF_ICON, + CONF_ID, + CONF_PATH, + CONF_RAW_DATA_ID, + CONF_RESIZE, + CONF_SOURCE, + CONF_TYPE, + CONF_URL, +) +from esphome.core import CORE, HexInt +from esphome.cpp_generator import MockObj, MockObjClass +from esphome.types import ConfigType + +CODEOWNERS = ["@esphome/core"] + +_LOGGER = logging.getLogger(__name__) + +# If the MDI file cannot be downloaded within this time, abort. +IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds + +SOURCE_LOCAL = "local" +SOURCE_WEB = "web" + +SOURCE_MDI = "mdi" +SOURCE_MDIL = "mdil" +SOURCE_MEMORY = "memory" + +MDI_SOURCES = { + SOURCE_MDI: "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/", + SOURCE_MDIL: "https://raw.githubusercontent.com/Pictogrammers/MaterialDesignLight/refs/heads/master/svg/", + SOURCE_MEMORY: "https://raw.githubusercontent.com/Pictogrammers/Memory/refs/heads/main/src/svg/", +} + + +def compute_local_image_path(value) -> Path: + url = value[CONF_URL] if isinstance(value, dict) else value + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + # Downloaded files are cached under the shared `image` domain directory so + # the cache location is unaffected by which platform requested the file. + base_dir = external_files.compute_local_file_dir(DOMAIN) + return base_dir / key + + +def local_path(value): + value = value[CONF_PATH] if isinstance(value, dict) else value + return str(CORE.relative_config_path(value)) + + +def download_file(url, path): + external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) + return str(path) + + +def download_gh_svg(value, source): + mdi_id = value[CONF_ICON] if isinstance(value, dict) else value + base_dir = external_files.compute_local_file_dir(DOMAIN) / source + path = base_dir / f"{mdi_id}.svg" + + url = MDI_SOURCES[source] + mdi_id + ".svg" + return download_file(url, path) + + +def download_image(value): + value = value[CONF_URL] if isinstance(value, dict) else value + return download_file(value, compute_local_image_path(value)) + + +def validate_file_shorthand(value): + value = cv.string_strict(value) + parts = value.strip().split(":") + if len(parts) == 2 and parts[0] in MDI_SOURCES: + match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1]) + if match is None: + raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.") + return download_gh_svg(parts[1], parts[0]) + + if value.startswith(("http://", "https://")): + return download_image(value) + + value = cv.file_(value) + return local_path(value) + + +LOCAL_SCHEMA = cv.All( + { + cv.Required(CONF_PATH): cv.file_, + }, + local_path, +) + + +def mdi_schema(source): + def validate_mdi(value): + return download_gh_svg(value, source) + + return cv.All( + cv.Schema( + { + cv.Required(CONF_ICON): cv.string, + } + ), + validate_mdi, + ) + + +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.string, + }, + download_image, +) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + SOURCE_LOCAL: LOCAL_SCHEMA, + SOURCE_WEB: WEB_SCHEMA, + } + | {source: mdi_schema(source) for source in MDI_SOURCES}, + key=CONF_SOURCE, +) + + +OPTIONS_SCHEMA = { + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( + "NONE", "FLOYDSTEINBERG", upper=True + ), + cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean, + cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True), + cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(), +} + + +def image_schema(class_: MockObjClass = Image_) -> cv.Schema: + """Build the validation schema for a single file-backed image entry. + + Shared by the built-in ``file`` image platform and the ``animation`` + platform (which extends it). Platforms that source their pixels elsewhere + (e.g. ``online_image``) provide their own schema instead. + + :param class_: The declared C++ class for the generated image instance. + """ + return cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(class_), + cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + **OPTIONS_SCHEMA, + cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE), + } + ) + + +def validate_image_final(config: ConfigType) -> ConfigType: + """Per-entry final validation, shared by file-backed image platforms. + + For LVGL 9 the default byte order for RGB565 images is little-endian, so + fill in that default when the user did not specify a byte order and warn + when big-endian was explicitly requested. + """ + if byte_order := config.get(CONF_BYTE_ORDER): + if byte_order == "BIG_ENDIAN": + _LOGGER.warning( + "The image '%s' is configured with big-endian byte order, little-endian is expected", + config.get(CONF_FILE), + ) + else: + config[CONF_BYTE_ORDER] = "LITTLE_ENDIAN" + return config + + +async def new_image(config: ConfigType) -> MockObj: + """Generate a single file-backed ``image::Image`` instance. + + Used by the built-in ``file`` platform; encodes the image data, registers + the C++ variable and records its metadata for other components to consume. + """ + prog_arr, width, height, image_type, trans_value, _ = await write_image(config) + var = cg.new_Pvariable( + config[CONF_ID], prog_arr, width, height, image_type, trans_value + ) + add_metadata( + config[CONF_ID], width, height, config[CONF_TYPE], config[CONF_TRANSPARENCY] + ) + return var + + +async def write_image(config, all_frames=False): + path = Path(config[CONF_FILE]) + if not path.is_file(): + raise core.EsphomeError(f"Could not load image file {path}") + + resize = config.get(CONF_RESIZE) + try: + if is_svg_file(path): + import resvg_py + + resize = resize or (None, None) + image_data = resvg_py.svg_to_bytes( + svg_path=str(path), width=resize[0], height=resize[1], dpi=100 + ) + + # Convert bytes to Pillow Image + image = Image.open(io.BytesIO(image_data)) + width, height = image.size + + else: + image = Image.open(path) + width, height = image.size + if resize: + # Preserve aspect ratio + new_width_max = min(width, resize[0]) + new_height_max = min(height, resize[1]) + ratio = min(new_width_max / width, new_height_max / height) + width, height = int(width * ratio), int(height * ratio) + except (OSError, UnidentifiedImageError, ValueError) as exc: + raise core.EsphomeError(f"Could not read image file {path}: {exc}") from exc + + if not resize and (width > 500 or height > 500): + _LOGGER.warning( + 'The image "%s" you requested is very big. Please consider' + " using the resize parameter.", + path, + ) + + dither = ( + Image.Dither.NONE + if config[CONF_DITHER] == "NONE" + else Image.Dither.FLOYDSTEINBERG + ) + type = config[CONF_TYPE] + transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE) + invert_alpha = config[CONF_INVERT_ALPHA] + frame_count = 1 + if all_frames: + with contextlib.suppress(AttributeError): + frame_count = image.n_frames + if frame_count <= 1: + _LOGGER.warning("Image file %s has no animation frames", path) + + # Encode each frame with its own encoder and concatenate. This keeps every + # frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane] + # per frame) so animation frame stepping in image.cpp / animation.cpp stays + # correct without needing to know the total frame count. + byte_order = config.get(CONF_BYTE_ORDER) + combined_data: list[int] = [] + encoder: ImageEncoder | None = None + for frame_index in range(frame_count): + image.seek(frame_index) + encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha) + if byte_order is not None: + # Check for valid type has already been done in validate_settings + encoder.set_big_endian(byte_order == "BIG_ENDIAN") + pixels = encoder.convert(image.resize((width, height)), path).getdata() + for row in range(height): + for col in range(width): + encoder.encode(pixels[row * width + col]) + encoder.end_row() + encoder.end_image() + combined_data.extend(encoder.data) + + rhs = [HexInt(x) for x in combined_data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + image_type = get_image_type_enum(type) + trans_value = get_transparency_enum(encoder.transparency) + + return prog_arr, width, height, image_type, trans_value, frame_count + + +# The built-in static-image platform: pixels embedded at compile time from a +# local file, a downloaded web image, or a Material Design Icon. +CONFIG_SCHEMA = cv.All(image_schema(Image_), validate_settings) + +FINAL_VALIDATE_SCHEMA = validate_image_final + + +async def to_code(config: ConfigType) -> None: + await new_image(config) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 2f1aa936a3..43358baedb 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -47,7 +47,7 @@ CONFIG_SCHEMA = ( host=True, ln882x=False, nrf52=True, - rp2040=True, + rp2=True, rtl87xx=False, ): cv.boolean, cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum( diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index fc35271017..d2102496a2 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -7,7 +7,6 @@ namespace esphome::growatt_solar { static const char *const TAG = "growatt_solar"; -static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion void GrowattSolar::loop() { @@ -31,11 +30,12 @@ void GrowattSolar::update() { } this->waiting_to_update_ = false; - this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]); + this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]); this->last_send_ = millis(); } -void GrowattSolar::on_modbus_data(const std::vector &data) { +void GrowattSolar::on_response(std::span request_pdu, std::span response_pdu) { + auto data = modbus::helpers::server_pdu_payload(response_pdu); // Other components might be sending commands to our device. But we don't get called with enough // context to know what is what. So if we didn't do a send, we ignore the data. if (!this->last_send_) diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index 18a7c917d5..a172f49001 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -4,7 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" -#include +#include namespace esphome::growatt_solar { @@ -69,7 +69,7 @@ class GrowattSolar final : public PollingComponent, public modbus::ModbusClientD public: void loop() override; void update() override; - void on_modbus_data(const std::vector &data) override; + void on_response(std::span request_pdu, std::span response_pdu) override; void dump_config() override; void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; } diff --git a/esphome/components/gsl3670/__init__.py b/esphome/components/gsl3670/__init__.py new file mode 100644 index 0000000000..c58ce8a01e --- /dev/null +++ b/esphome/components/gsl3670/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/gsl3670/gsl3670_touchscreen.cpp b/esphome/components/gsl3670/gsl3670_touchscreen.cpp new file mode 100644 index 0000000000..9115130f4a --- /dev/null +++ b/esphome/components/gsl3670/gsl3670_touchscreen.cpp @@ -0,0 +1,167 @@ +#include "gsl3670_touchscreen.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome::gsl3670 { + +static const char *const TAG = "gsl3670.touchscreen"; +static const size_t MAX_TOUCHES = 3; +// --------------------------------------------------------------------------- +// setup() – mirrors esp_lcd_touch_gsl3670_init() in the Seeed BSP: +// clear_reg → reset → load_fw → startup_chip → reset → startup_chip +// --------------------------------------------------------------------------- +void GSL3670Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up GSL3670 touchscreen..."); + + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + } + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = this->display_->get_native_height(); + } + + this->clear_reg_(); + this->reset_(); + this->load_firmware_(); + this->startup_chip_(); + this->reset_(); + this->startup_chip_(); + + ESP_LOGCONFIG(TAG, "GSL3670 initialised OK"); +} + +void GSL3670Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, + "GSL3670 Touchscreen:\n" + " X-raw-max: %d\n" + " Y-raw-max: %d\n", + this->x_raw_max_, this->y_raw_max_); + LOG_I2C_DEVICE(this); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + ESP_LOGCONFIG(TAG, " Firmware records: %zu", this->firmware_len_); +} + +// --------------------------------------------------------------------------- +// update_touches() – mirrors esp_lcd_touch_gsl3670_read_data() in Seeed BSP +// --------------------------------------------------------------------------- +void GSL3670Touchscreen::update_touches() { + uint8_t buf[44] = {}; + auto err = this->read_register(0x80, buf, sizeof(buf)); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "I2C read failed (%d)", err); + return; + } + uint8_t finger_num = clamp_at_most(buf[0], MAX_TOUCHES); + + // Build gsl_touch_info exactly as the Seeed driver does + for (uint8_t j = 0; j != finger_num; j++) { + // buf[(j+1)*4 + 0..3]: byte0=y_lo, byte1=y_hi, byte2=x_lo, byte3=id|x_hi + auto x = (uint16_t) (((buf[(j + 1) * 4 + 3] & 0x0f) << 8) | buf[(j + 1) * 4 + 2]); + auto y = (uint16_t) ((buf[(j + 1) * 4 + 1] << 8) | buf[(j + 1) * 4 + 0]); + auto id = (buf[(j + 1) * 4 + 3] >> 4) & 0x0f; + ESP_LOGV(TAG, "Touch id=%u, x=%u y=%u", id, x, y); + if (x <= 8192 && y <= 8192) + this->add_raw_touch_position_(id, x, y); + } +} + +// --------------------------------------------------------------------------- +// clear_reg_() – mirrors esp_lcd_touch_gsl3670_clear_reg() +// GPIO reset → write 0x01 to 0x88 → write 0x04 to 0xe4 → write 0x00 to 0xe0 +// --------------------------------------------------------------------------- +void GSL3670Touchscreen::clear_reg_() { + ESP_LOGD(TAG, "clear_reg"); + + // GPIO reset pulse + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(1); + this->reset_pin_->digital_write(true); + delay(5); + } + + this->write_reg8_(0x88, 0x01); + // delay(5); + this->write_reg8_(0xe4, 0x04); + // delay(5); + this->write_reg8_(0xe0, 0x00); + // delay(5); +} + +// --------------------------------------------------------------------------- +// reset_() – mirrors touch_gsl3670_reset() +// GPIO reset → write 0x04 to 0xe4 → write 4×0x00 to 0xbc +// --------------------------------------------------------------------------- +void GSL3670Touchscreen::reset_() { + ESP_LOGD(TAG, "reset"); + + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(1); + this->reset_pin_->digital_write(true); + delay(5); + } + + this->write_reg8_(0xe4, 0x04); + + uint8_t zeros[4] = {0, 0, 0, 0}; + this->write_reg_(0xbc, zeros, 4); +} + +void GSL3670Touchscreen::load_firmware_() { + if (firmware_ == nullptr || firmware_len_ == 0) { + ESP_LOGW(TAG, "No firmware supplied – skipping"); + return; + } + + ESP_LOGD(TAG, "Loading firmware (%zu blocks)...", firmware_len_); + + static constexpr size_t FW_BLK_SIZE = 128 + 4; + + for (size_t i = 0; i != this->firmware_len_; i++) { + auto offset = i * FW_BLK_SIZE; + uint8_t val = this->firmware_[offset + 0]; + ESP_LOGV(TAG, "Firmware address 0x%02X", val); + this->write_reg_(0xf0, &val, 1); + this->write_reg_(0, this->firmware_ + offset + 4, 128); + } + ESP_LOGD(TAG, "Firmware load complete"); +} + +// --------------------------------------------------------------------------- +// startup_chip_() – mirrors esp_lcd_touch_gsl3670_startup_chip() +// write 0x00 to 0xe0 +// --------------------------------------------------------------------------- +void GSL3670Touchscreen::startup_chip_() { + ESP_LOGD(TAG, "startup_chip"); + this->write_reg8_(0xe0, 0x00); + delay(5); +} + +// --------------------------------------------------------------------------- +// I2C helpers +// --------------------------------------------------------------------------- + +bool GSL3670Touchscreen::write_reg_(uint8_t reg, const uint8_t *data, size_t len) { + auto err = this->write_register(reg, data, len); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "I2C write reg 0x%02X len %zu failed (%d)", reg, len, err); + return false; + } + return true; +} + +bool GSL3670Touchscreen::write_reg8_(uint8_t reg, uint8_t val) { return write_reg_(reg, &val, 1); } + +} // namespace esphome::gsl3670 diff --git a/esphome/components/gsl3670/gsl3670_touchscreen.h b/esphome/components/gsl3670/gsl3670_touchscreen.h new file mode 100644 index 0000000000..3cce074f9b --- /dev/null +++ b/esphome/components/gsl3670/gsl3670_touchscreen.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::gsl3670 { + +// --------------------------------------------------------------------------- +// GSL3670 touchscreen ESPHome component +// --------------------------------------------------------------------------- +class GSL3670Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + /// Supply the firmware table (generated by codegen from the YAML) + void set_firmware(const uint8_t *fw, size_t len) { + this->firmware_ = fw; + this->firmware_len_ = len; + } + + void set_interrupt_pin(InternalGPIOPin *pin) { interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { reset_pin_ = pin; } + + // touchscreen::Touchscreen / Component interface + void setup() override; + void dump_config() override; + + protected: + void update_touches() override; + + private: + // ---------- init steps (mirrors esp_lcd_touch_gsl3670_init) ---------- + void clear_reg_(); // GPIO reset + 0x88/0xe4/0xe0 sequence + void reset_(); // GPIO reset + 0xe4/0xbc sequence + void load_firmware_(); // write GSLX670_FW table + void startup_chip_(); // 0x00→0xe0 + gsl_DataInit + + // ---------- I2C helpers ---------- + bool write_reg_(uint8_t reg, const uint8_t *data, size_t len); + bool write_reg8_(uint8_t reg, uint8_t val); + + InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + + const uint8_t *firmware_{nullptr}; + size_t firmware_len_{0}; +}; + +} // namespace esphome::gsl3670 diff --git a/esphome/components/gsl3670/touchscreen.py b/esphome/components/gsl3670/touchscreen.py new file mode 100644 index 0000000000..fc0318f076 --- /dev/null +++ b/esphome/components/gsl3670/touchscreen.py @@ -0,0 +1,224 @@ +"""ESPHome codegen for the gsl3670 touchscreen sub-platform.""" + +import hashlib +import logging +from pathlib import Path + +from esphome import external_files, pins +import esphome.codegen as cg +from esphome.components import i2c, touchscreen +from esphome.components.const import CONF_SHA256 +from esphome.components.touchscreen import ( + CONF_X_MAX, + CONF_X_MIN, + CONF_Y_MAX, + CONF_Y_MIN, + option_with_default, + touchscreen_schema, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_FILE, + CONF_ID, + CONF_INTERRUPT_PIN, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_RESET_PIN, + CONF_SWAP_XY, + CONF_URL, +) +from esphome.core import ID + +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["touchscreen"] +LOGGER = logging.getLogger(__name__) + +DOMAIN = "gsl3670" + +gsl3670_ns = cg.esphome_ns.namespace("gsl3670") +GSL3670Touchscreen = gsl3670_ns.class_( + "GSL3670Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CONF_FIRMWARE = "firmware" + +# Firmware blobs are published as release assets of the companion repository +# rather than vendored into the ESPHome source tree. The default URL/SHA-256 +# for each model point at a pinned release artifact; users may override them +# (or supply a local file via `firmware: { file: ... }`). +FIRMWARE_RELEASE = "v1.0.0" +FIRMWARE_BASE_URL = f"https://github.com/esphome-libs/gsl3670-firmware/releases/download/{FIRMWARE_RELEASE}" + +MODELS = { + "SEEED-RETERMINAL-D1001": { + CONF_SWAP_XY: True, + CONF_MIRROR_X: True, + CONF_MIRROR_Y: True, + CONF_X_MIN: 20, + CONF_Y_MIN: 20, + CONF_X_MAX: 872, + CONF_Y_MAX: 1644, + CONF_RESET_PIN: {"xl9535": None, "number": 14}, + CONF_INTERRUPT_PIN: 16, + CONF_FIRMWARE: { + CONF_URL: f"{FIRMWARE_BASE_URL}/seeed-d1001-fw.bin", + CONF_SHA256: "2e50501ad83656fb6fa3d92591f9f31add4d442c8e8a79f29f5c4d335bd127a4", + }, + }, + "GUITION-JC8012P4A1": { + CONF_SWAP_XY: True, + CONF_MIRROR_X: True, + CONF_MIRROR_Y: False, + CONF_X_MIN: 20, + CONF_Y_MIN: 20, + CONF_X_MAX: 880, + CONF_Y_MAX: 1648, + CONF_RESET_PIN: 22, + CONF_INTERRUPT_PIN: 21, + CONF_FIRMWARE: { + CONF_URL: f"{FIRMWARE_BASE_URL}/seeed-d1001-fw.bin", + CONF_SHA256: "2e50501ad83656fb6fa3d92591f9f31add4d442c8e8a79f29f5c4d335bd127a4", + }, + }, + "CUSTOM": {}, +} + +_FW_BLK_SIZE = 128 + 4 + + +def _validate_firmware_data(data: bytes, source: str) -> None: + """Validate the structure of a decoded GSL3670 firmware blob.""" + blk_cnt = len(data) // _FW_BLK_SIZE + if blk_cnt == 0 or blk_cnt * _FW_BLK_SIZE != len(data): + raise cv.Invalid(f"Firmware file length is incorrect: {source}") + for i in range(0, len(data), _FW_BLK_SIZE): + if data[i] > 0xEF or data[i + 1] != 1 or data[i + 2] != 2 or data[i + 3] != 3: + raise cv.Invalid( + f"Corrupted firmware at block {i // _FW_BLK_SIZE} in: {source}" + ) + + +def _cache_path(url: str) -> Path: + """Cache path for a downloaded firmware blob, keyed by URL.""" + key = hashlib.sha256(url.encode()).hexdigest()[:8] + return external_files.compute_local_file_dir(DOMAIN) / key + + +def firmware_path(firmware: dict) -> Path: + """Return the path the firmware bytes will be read from at codegen time.""" + if path := firmware.get(CONF_FILE): + return path + return _cache_path(firmware[CONF_URL]) + + +def _validate_firmware(firmware: dict) -> dict: + """Require a single source, download (with caching), verify and validate.""" + if (CONF_FILE in firmware) == (CONF_URL in firmware): + raise cv.Invalid( + f"Exactly one of '{CONF_URL}' or '{CONF_FILE}' must be provided" + ) + + if path := firmware.get(CONF_FILE): + _validate_firmware_data(path.read_bytes(), str(path.absolute())) + return firmware + + url = firmware[CONF_URL] + data = external_files.download_content(url, _cache_path(url)) + + if expected := firmware.get(CONF_SHA256): + actual = hashlib.sha256(data).hexdigest() + if actual.lower() != expected.lower(): + raise cv.Invalid( + f"Firmware SHA-256 mismatch for {url}: " + f"expected {expected.lower()}, got {actual}", + [CONF_SHA256], + ) + else: + LOGGER.warning( + "No SHA256 provided for gsl3670 firmware - firmware integrity can not be checked" + ) + _validate_firmware_data(data, url) + return firmware + + +FIRMWARE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_URL): cv.url, + cv.Optional(CONF_SHA256): cv.string_strict, + cv.Optional(CONF_FILE): cv.file_, + } + ), + _validate_firmware, +) + + +def _config_schema(config): + model_option = { + cv.Optional(CONF_MODEL, default="CUSTOM"): cv.one_of(*MODELS, upper=True) + } + config = cv.Schema(model_option, extra=True)(config) + defaults = MODELS[config[CONF_MODEL]] + schema = ( + touchscreen_schema(cv.UNDEFINED, False, defaults) + .extend( + { + cv.GenerateID(): cv.declare_id(GSL3670Touchscreen), + option_with_default( + CONF_INTERRUPT_PIN, defaults + ): pins.internal_gpio_input_pin_schema, + option_with_default( + CONF_RESET_PIN, defaults + ): pins.gpio_output_pin_schema, + **model_option, + option_with_default( + CONF_FIRMWARE, defaults, required=True + ): FIRMWARE_SCHEMA, + } + ) + .extend(i2c.i2c_device_schema(0x40)) + .extend(cv.COMPONENT_SCHEMA) + ) + return schema(config) + + +CONFIG_SCHEMA = _config_schema + + +def _read_firmware(config) -> bytes: + path = firmware_path(config[CONF_FIRMWARE]) + data = path.read_bytes() + LOGGER.info( + "Read gsl3670 touchscreen firmware file %s: %d bytes, %d blocks", + path.absolute(), + len(data), + len(data) // _FW_BLK_SIZE, + ) + return data + + +# --------------------------------------------------------------------------- +# Code generation +# --------------------------------------------------------------------------- +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_INTERRUPT_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(pin)) + + if CONF_RESET_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(pin)) + + # Firmware table + data = _read_firmware(config) + fw_array = cg.progmem_array( + ID(config[CONF_ID].id + "_fw", type=cg.uint8), list(data) + ) + cg.add(var.set_firmware(fw_array, len(data) // _FW_BLK_SIZE)) diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 74a218263d..294aa53b03 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -132,7 +132,7 @@ void HaierClimateBase::save_settings() { } bool HaierClimateBase::get_display_state() const { - return (this->display_status_ == SwitchState::ON) || (this->display_status_ == SwitchState::PENDING_ON); + return (this->display_status_ == SwitchState::SWITCH_ON) || (this->display_status_ == SwitchState::PENDING_ON); } void HaierClimateBase::set_display_state(bool state) { @@ -144,7 +144,7 @@ void HaierClimateBase::set_display_state(bool state) { } bool HaierClimateBase::get_health_mode() const { - return (this->health_mode_ == SwitchState::ON) || (this->health_mode_ == SwitchState::PENDING_ON); + return (this->health_mode_ == SwitchState::SWITCH_ON) || (this->health_mode_ == SwitchState::PENDING_ON); } void HaierClimateBase::set_health_mode(bool state) { diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 13e8d7548d..db4c1abceb 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -147,8 +147,8 @@ class HaierClimateBase : public esphome::Component, esphome::optional message; }; enum class SwitchState { - OFF = 0b00, - ON = 0b01, + SWITCH_OFF = 0b00, + SWITCH_ON = 0b01, PENDING_OFF = 0b10, PENDING_ON = 0b11, }; @@ -157,8 +157,8 @@ class HaierClimateBase : public esphome::Component, esphome::optional action_request_; uint8_t fan_mode_speed_; uint8_t other_modes_fan_speed_; - SwitchState display_status_{SwitchState::ON}; - SwitchState health_mode_{SwitchState::OFF}; + SwitchState display_status_{SwitchState::SWITCH_ON}; + SwitchState health_mode_{SwitchState::SWITCH_OFF}; bool force_send_control_; bool forced_request_status_; bool reset_protocol_request_; diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index f68404afd9..881a2328cb 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -50,7 +50,7 @@ void HonClimate::set_quiet_mode_state(bool state) { this->quiet_mode_state_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; this->force_send_control_ = true; } else { - this->quiet_mode_state_ = state ? SwitchState::ON : SwitchState::OFF; + this->quiet_mode_state_ = state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF; } this->settings_.quiet_mode_state = state; #ifdef USE_SWITCH @@ -63,7 +63,7 @@ void HonClimate::set_quiet_mode_state(bool state) { } bool HonClimate::get_quiet_mode_state() const { - return (this->quiet_mode_state_ == SwitchState::ON) || (this->quiet_mode_state_ == SwitchState::PENDING_ON); + return (this->quiet_mode_state_ == SwitchState::SWITCH_ON) || (this->quiet_mode_state_ == SwitchState::PENDING_ON); } esphome::optional HonClimate::get_vertical_airflow() const { @@ -513,7 +513,7 @@ void HonClimate::initialization() { } this->current_vertical_swing_ = this->settings_.last_vertiacal_swing; this->current_horizontal_swing_ = this->settings_.last_horizontal_swing; - this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::ON : SwitchState::OFF; + this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF; } haier_protocol::HaierMessage HonClimate::get_control_message() { @@ -825,7 +825,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * #ifdef USE_SENSOR this->update_sub_sensor_(SubSensorType::INDOOR_COIL_TEMPERATURE, bd_packet->indoor_coil_temperature / 2.0 - 20); this->update_sub_sensor_(SubSensorType::OUTDOOR_COIL_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64); - this->update_sub_sensor_(SubSensorType::OUTDOOR_DEFROST_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64); + this->update_sub_sensor_(SubSensorType::OUTDOOR_DEFROST_TEMPERATURE, bd_packet->outdoor_defrost_temperature - 64); this->update_sub_sensor_(SubSensorType::OUTDOOR_IN_AIR_TEMPERATURE, bd_packet->outdoor_in_air_temperature - 64); this->update_sub_sensor_(SubSensorType::OUTDOOR_OUT_AIR_TEMPERATURE, bd_packet->outdoor_out_air_temperature - 64); this->update_sub_sensor_(SubSensorType::POWER, encode_uint16(bd_packet->power[0], bd_packet->power[1])); @@ -939,14 +939,14 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * // AC just turned on from remote need to turn off display this->force_send_control_ = true; } else if ((((uint8_t) this->display_status_) & 0b10) == 0) { - this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF; + this->display_status_ = disp_status ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF; } } } // Health mode if ((((uint8_t) this->health_mode_) & 0b10) == 0) { bool old_health_mode = this->get_health_mode(); - this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF; + this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF; should_publish = should_publish || (old_health_mode != this->get_health_mode()); } { @@ -1008,7 +1008,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * // In proper mode and not in pending state bool new_quiet_mode = packet.control.quiet_mode != 0; if (new_quiet_mode != this->get_quiet_mode_state()) { - this->quiet_mode_state_ = new_quiet_mode ? SwitchState::ON : SwitchState::OFF; + this->quiet_mode_state_ = new_quiet_mode ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF; this->settings_.quiet_mode_state = new_quiet_mode; #ifdef USE_SWITCH if (this->quiet_mode_switch_ != nullptr) { diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index ba36e6a8fb..a34b4422c6 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -197,7 +197,7 @@ class HonClimate final : public HaierClimateBase { esphome::optional current_horizontal_swing_{}; HonSettings settings_{}; ESPPreferenceObject hon_rtc_; - SwitchState quiet_mode_state_{SwitchState::OFF}; + SwitchState quiet_mode_state_{SwitchState::SWITCH_OFF}; }; } // namespace esphome::haier diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index a013371649..fdb3b779e2 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -464,14 +464,14 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin // AC just turned on from remote need to turn off display this->force_send_control_ = true; } else if ((((uint8_t) this->health_mode_) & 0b10) == 0) { - this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF; + this->display_status_ = disp_status ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF; } } } // Health mode if ((((uint8_t) this->health_mode_) & 0b10) == 0) { bool old_health_mode = this->get_health_mode(); - this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF; + this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF; should_publish = should_publish || (old_health_mode != this->get_health_mode()); } { diff --git a/esphome/components/havells_solar/havells_solar.cpp b/esphome/components/havells_solar/havells_solar.cpp index 9257a37fd9..6af72c352b 100644 --- a/esphome/components/havells_solar/havells_solar.cpp +++ b/esphome/components/havells_solar/havells_solar.cpp @@ -7,10 +7,10 @@ namespace esphome::havells_solar { static const char *const TAG = "havells_solar"; -static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x03; static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers -void HavellsSolar::on_modbus_data(const std::vector &data) { +void HavellsSolar::on_response(std::span request_pdu, std::span response_pdu) { + auto data = modbus::helpers::server_pdu_payload(response_pdu); if (data.size() < MODBUS_REGISTER_COUNT * 2) { ESP_LOGW(TAG, "Invalid size for HavellsSolar!"); return; @@ -121,7 +121,7 @@ void HavellsSolar::on_modbus_data(const std::vector &data) { this->dci_of_t_sensor_->publish_state(dci_of_t); } -void HavellsSolar::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void HavellsSolar::update() { this->read_holding_registers(0, MODBUS_REGISTER_COUNT); } void HavellsSolar::dump_config() { ESP_LOGCONFIG(TAG, "HAVELLS Solar:\n" diff --git a/esphome/components/havells_solar/havells_solar.h b/esphome/components/havells_solar/havells_solar.h index 02e999c56c..ed5d13b8b6 100644 --- a/esphome/components/havells_solar/havells_solar.h +++ b/esphome/components/havells_solar/havells_solar.h @@ -4,7 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" -#include +#include namespace esphome::havells_solar { @@ -77,7 +77,7 @@ class HavellsSolar final : public PollingComponent, public modbus::ModbusClientD void update() override; - void on_modbus_data(const std::vector &data) override; + void on_response(std::span request_pdu, std::span response_pdu) override; void dump_config() override; diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index cd1b7d2bb0..21f7ea6393 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_VISUAL, ) from esphome.core import CORE +from esphome.types import ConfigType CODEOWNERS = ["@rob-deutsch"] @@ -97,6 +98,18 @@ VERTICAL_DIRECTIONS = { "down": VerticalDirections.VERTICAL_DIRECTION_DOWN, } + +def _default_visual(config: ConfigType) -> ConfigType: + # Seed the visual min/max from the required min/max_temperature so the entity + # reports the configured range in Home Assistant instead of the ClimateIR + # 0-100 default. Done during validation so the effective range is visible in + # the dumped config and set before new_climate_ir() reads CONF_VISUAL. + visual = config.setdefault(CONF_VISUAL, {}) + visual.setdefault(CONF_MAX_TEMPERATURE, config[CONF_MAX_TEMPERATURE]) + visual.setdefault(CONF_MIN_TEMPERATURE, config[CONF_MIN_TEMPERATURE]) + return config + + CONFIG_SCHEMA = cv.All( climate_ir.climate_ir_with_receiver_schema(HeatpumpIRClimate).extend( { @@ -108,18 +121,12 @@ CONFIG_SCHEMA = cv.All( } ), cv.Any(cv.only_with_arduino, cv.only_on_esp32), + _default_visual, ) async def to_code(config): var = await climate_ir.new_climate_ir(config) - if CONF_VISUAL not in config: - config[CONF_VISUAL] = {} - visual = config[CONF_VISUAL] - if CONF_MAX_TEMPERATURE not in visual: - visual[CONF_MAX_TEMPERATURE] = config[CONF_MAX_TEMPERATURE] - if CONF_MIN_TEMPERATURE not in visual: - visual[CONF_MIN_TEMPERATURE] = config[CONF_MIN_TEMPERATURE] cg.add(var.set_protocol(config[CONF_PROTOCOL])) cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT])) cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index 7a0dc0690c..964d26dfbc 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -242,7 +242,7 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) { return; } - if (data[1] != HlkFm22xResult::SUCCESS) { + if (data[1] != HlkFm22xResult::SUCCEEDED) { ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]); switch (expected) { case HlkFm22xCommand::ENROLL: diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 34246f52f0..3bdf6e2c71 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -41,7 +41,7 @@ enum HlkFm22xNoteType { }; enum HlkFm22xResult { - SUCCESS = 0x00, + SUCCEEDED = 0x00, REJECTED = 0x01, ABORTED = 0x02, FAILED4_CAMERA = 0x04, diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index f46a6b8580..02c6e75146 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -61,7 +61,7 @@ void HM3301Component::update() { int16_t aqi_value = -1; if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { aqi::AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); - aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); + aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value, /*extended_range=*/false); } if (pm_1_0_value != -1) { diff --git a/esphome/components/hmac_sha256/hmac_sha256.cpp b/esphome/components/hmac_sha256/hmac_sha256.cpp index c113cb48a6..d8e1f059a6 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.cpp +++ b/esphome/components/hmac_sha256/hmac_sha256.cpp @@ -1,6 +1,6 @@ #include #include "hmac_sha256.h" -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_HOST) #include "esphome/core/helpers.h" namespace esphome::hmac_sha256 { diff --git a/esphome/components/hmac_sha256/hmac_sha256.h b/esphome/components/hmac_sha256/hmac_sha256.h index 22129b1182..74ac4c23de 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.h +++ b/esphome/components/hmac_sha256/hmac_sha256.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/defines.h" -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_HOST) #include diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 50deb1acf6..795c1a556d 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( ThreadModel, ) from esphome.core import CORE +from esphome.platformio.toolchain import copy_ccache_script from .const import KEY_HOST @@ -49,3 +50,9 @@ async def to_code(config): cg.add_platformio_option("platform", "platformio/native") cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_compat_mode", "strict") + cg.add_platformio_option("extra_scripts", ["pre:ccache.py"]) + + +# Called by writer.py +def copy_files() -> None: + copy_ccache_script() diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index fd033dac7f..54d7f5c77b 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -73,7 +73,7 @@ def validate_url(value): def validate_ssl_verification(config): error_message = "" - if CORE.is_rp2040 and config[CONF_VERIFY_SSL]: + if CORE.is_rp2 and config[CONF_VERIFY_SSL]: error_message = "ESPHome does not support certificate verification on RP2040" if ( @@ -96,7 +96,7 @@ def _declare_request_class(value): return cv.declare_id(HttpRequestHost)(value) if CORE.is_esp32: return cv.declare_id(HttpRequestIDF)(value) - if CORE.is_esp8266 or CORE.is_rp2040: + if CORE.is_esp8266 or CORE.is_rp2: return cv.declare_id(HttpRequestArduino)(value) return NotImplementedError @@ -118,7 +118,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, cv.Optional(CONF_WATCHDOG_TIMEOUT): cv.All( - cv.Any(cv.only_on_esp32, cv.only_on_rp2040), + cv.Any(cv.only_on_esp32, cv.only_on_rp2), cv.positive_not_null_time_period, cv.positive_time_period_milliseconds, ), @@ -144,7 +144,7 @@ CONFIG_SCHEMA = cv.All( esp8266_arduino=cv.Version(2, 5, 1), esp32_arduino=cv.Version(0, 0, 0), esp_idf=cv.Version(0, 0, 0), - rp2040_arduino=cv.Version(0, 0, 0), + rp2_arduino=cv.Version(0, 0, 0), host=cv.Version(0, 0, 0), ), validate_ssl_verification, @@ -204,7 +204,7 @@ async def to_code(config): ) if CORE.is_esp8266: cg.add_library("ESP8266HTTPClient", None) - if CORE.is_rp2040 and CORE.using_arduino: + if CORE.is_rp2 and CORE.using_arduino: cg.add_library("HTTPClient", None) if CORE.is_host: if IS_MACOS: @@ -368,7 +368,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( "http_request_host.cpp": {PlatformFramework.HOST_NATIVE}, "http_request_arduino.cpp": { PlatformFramework.ESP8266_ARDUINO, - PlatformFramework.RP2040_ARDUINO, + PlatformFramework.RP2_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 5025a5c12d..4471dffdc2 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -488,10 +488,10 @@ template class HttpRequestSendAction final : public Actionbody_.value(x...); } if (!this->json_.empty()) { - body = json::build_json([this, x...](JsonObject root) { this->encode_json_(x..., root); }); + body = json::build_json([this, x...](JsonObject root) mutable { this->encode_json_(x..., root); }); } if (this->json_func_ != nullptr) { - body = json::build_json([this, x...](JsonObject root) { this->json_func_(x..., root); }); + body = json::build_json([this, x...](JsonObject root) mutable { this->json_func_(x..., root); }); } std::vector
request_headers; request_headers.reserve(this->request_headers_.size()); @@ -510,9 +510,9 @@ template class HttpRequestSendAction final : public Actionmax_response_buffer_size_; #ifdef USE_HTTP_REQUEST_RESPONSE if (this->capture_response_.value(x...)) { + size_t max_length = this->max_response_buffer_size_; std::string response_body; RAMAllocator allocator; uint8_t *buf = allocator.allocate(max_length); diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index bb5e9427dd..84333e7169 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -1,6 +1,6 @@ #include "http_request_arduino.h" -#if defined(USE_ARDUINO) && !defined(USE_ESP32) +#if defined(USE_ARDUINO) && !defined(USE_ESP32) && !defined(USE_LIBRETINY) #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" @@ -72,7 +72,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur bool status = container->client_.begin(*stream_ptr, url.c_str()); -#elif defined(USE_RP2040) +#elif defined(USE_RP2) if (secure) { container->client_.setInsecure(); } diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index 8da40798ec..028b9f44a1 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -2,9 +2,9 @@ #include "http_request.h" -#if defined(USE_ARDUINO) && !defined(USE_ESP32) +#if defined(USE_ARDUINO) && !defined(USE_ESP32) && !defined(USE_LIBRETINY) -#if defined(USE_RP2040) +#if defined(USE_RP2) #include #include #endif diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 3e341395a4..a437540241 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -19,11 +19,6 @@ namespace esphome::http_request { static const char *const TAG = "http_request.idf"; static constexpr uint32_t ERROR_DURATION_MS = 1000; -struct UserData { - const std::vector &lower_case_collect_headers; - std::vector
&response_headers; -}; - void HttpRequestIDF::dump_config() { HttpRequestComponent::dump_config(); ESP_LOGCONFIG(TAG, @@ -34,15 +29,15 @@ void HttpRequestIDF::dump_config() { } esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { - UserData *user_data = (UserData *) evt->user_data; + auto *container = (HttpContainerIDF *) evt->user_data; switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: { const std::string header_name = str_lower_case(evt->header_key); // NOLINT - if (should_collect_header(user_data->lower_case_collect_headers, header_name)) { + if (should_collect_header(container->collect_headers_, header_name)) { const std::string header_value = evt->header_value; ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - user_data->response_headers.push_back({header_name, header_value}); + container->response_headers_.push_back({header_name, header_value}); } break; } @@ -124,8 +119,8 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->set_secure(secure); - auto user_data = UserData{lower_case_collect_headers, container->response_headers_}; - esp_http_client_set_user_data(client, static_cast(&user_data)); + container->collect_headers_ = lower_case_collect_headers; + esp_http_client_set_user_data(client, static_cast(container.get())); for (const auto &header : request_headers) { esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 8a803b5469..16a5b6a161 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -24,6 +24,8 @@ class HttpContainerIDF : public HttpContainer { protected: friend class HttpRequestIDF; esp_http_client_handle_t client_; + // Owned copy (not a reference): must outlive perform() for the response-header event handler + std::vector collect_headers_; }; class HttpRequestIDF final : public HttpRequestComponent { diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py index 1bb54599dc..b7026e0f55 100644 --- a/esphome/components/http_request/ota/__init__.py +++ b/esphome/components/http_request/ota/__init__.py @@ -36,7 +36,7 @@ CONFIG_SCHEMA = cv.All( esp8266_arduino=cv.Version(2, 5, 1), esp32_arduino=cv.Version(0, 0, 0), esp_idf=cv.Version(0, 0, 0), - rp2040_arduino=cv.Version(0, 0, 0), + rp2_arduino=cv.Version(0, 0, 0), ), ) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index eec2211a96..7b163d065e 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -49,7 +49,7 @@ from esphome.const import ( PLATFORM_ESP8266, PLATFORM_HOST, PLATFORM_NRF52, - PLATFORM_RP2040, + PLATFORM_RP2, PlatformFramework, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -130,7 +130,7 @@ def validate_config(config): return cv.require_framework_version( esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) )(config) - if CORE.is_rp2040: + if CORE.is_rp2: sda_controller = _rp2040_i2c_controller(config[CONF_SDA]) scl_controller = _rp2040_i2c_controller(config[CONF_SCL]) if sda_controller != scl_controller: @@ -171,7 +171,7 @@ CONFIG_SCHEMA = cv.All( CONF_SDA, esp32="SDA", esp8266="SDA", - rp2040="SDA", + rp2="SDA", nrf52="SDA", ): pins.internal_gpio_pin_number, cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All( @@ -181,7 +181,7 @@ CONFIG_SCHEMA = cv.All( CONF_SCL, esp32="SCL", esp8266="SCL", - rp2040="SCL", + rp2="SCL", nrf52="SCL", ): pins.internal_gpio_pin_number, cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All( @@ -191,7 +191,7 @@ CONFIG_SCHEMA = cv.All( CONF_FREQUENCY, esp32="50kHz", esp8266="50kHz", - rp2040="50kHz", + rp2="50kHz", nrf52="100kHz", host="50kHz", ): cv.All( @@ -219,7 +219,7 @@ CONFIG_SCHEMA = cv.All( [ PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_NRF52, PLATFORM_HOST, ] @@ -233,7 +233,7 @@ def _final_validate(config): full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") - if CORE.is_rp2040: + if CORE.is_rp2: if len(full_config) > 2: raise cv.Invalid( "The maximum number of I2C interfaces for RP2040/RP2350 is 2" @@ -443,7 +443,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( { "i2c_bus_arduino.cpp": { PlatformFramework.ESP8266_ARDUINO, - PlatformFramework.RP2040_ARDUINO, + PlatformFramework.RP2_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 47a06abe9e..cc036b12c3 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -1,4 +1,4 @@ -#if defined(USE_ARDUINO) && !defined(USE_ESP32) +#if defined(USE_ARDUINO) && !defined(USE_ESP32) && !defined(USE_LIBRETINY) #include "i2c_bus_arduino.h" #include @@ -19,7 +19,7 @@ void ArduinoI2CBus::setup() { #if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) -#elif defined(USE_RP2040) +#elif defined(USE_RP2) // Select Wire instance based on pin assignment, not definition order. // I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1 // RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf @@ -41,7 +41,7 @@ void ArduinoI2CBus::setup() { } void ArduinoI2CBus::set_pins_and_clock_() { -#ifdef USE_RP2040 +#ifdef USE_RP2 wire_->setSDA(this->sda_pin_); wire_->setSCL(this->scl_pin_); wire_->begin(); @@ -52,7 +52,7 @@ void ArduinoI2CBus::set_pins_and_clock_() { #if defined(USE_ESP8266) // https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h wire_->setClockStretchLimit(timeout_); // unit: us -#elif defined(USE_RP2040) +#elif defined(USE_RP2) // https://github.com/earlephilhower/ArduinoCore-API/blob/e37df85425e0ac020bfad226d927f9b00d2e0fb7/api/Stream.h wire_->setTimeout(timeout_ / 1000); // unit: ms #endif @@ -70,7 +70,7 @@ void ArduinoI2CBus::dump_config() { if (timeout_ > 0) { #if defined(USE_ESP8266) ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_); -#elif defined(USE_RP2040) +#elif defined(USE_RP2) ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); #endif } diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index ded28dd80c..71e91e770b 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -1,6 +1,6 @@ #pragma once -#if defined(USE_ARDUINO) && !defined(USE_ESP32) +#if defined(USE_ARDUINO) && !defined(USE_ESP32) && !defined(USE_LIBRETINY) #include #include "esphome/core/component.h" diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 66ca32b830..7b074b2e8f 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -245,7 +245,7 @@ void I2SAudioMicrophone::mic_task(void *params) { while (!(xEventGroupGetBits(this_microphone->event_group_) & MicrophoneEventGroupBits::COMMAND_STOP)) { if (this_microphone->data_callbacks_.size() > 0) { samples.resize(bytes_to_read); - size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * pdMS_TO_TICKS(READ_DURATION_MS)); + size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * READ_DURATION_MS); samples.resize(bytes_read); if (this_microphone->correct_dc_offset_) { this_microphone->fix_dc_offset_(samples); @@ -318,12 +318,11 @@ void I2SAudioMicrophone::fix_dc_offset_(std::vector &data) { } } -size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) { +size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, uint32_t timeout_ms) { size_t bytes_read = 0; - // i2s_channel_read expects the timeout value in ms, not ticks - esp_err_t err = i2s_channel_read(this->rx_handle_, buf, len, &bytes_read, pdTICKS_TO_MS(ticks_to_wait)); - if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (ticks_to_wait != 0))) { - // Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call + esp_err_t err = i2s_channel_read(this->rx_handle_, buf, len, &bytes_read, timeout_ms); + if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (timeout_ms != 0))) { + // Ignore ESP_ERR_TIMEOUT if timeout_ms = 0, as it will read the data on the next call if (!this->status_has_warning()) { // Avoid spamming the logs with the error message if its repeated ESP_LOGW(TAG, "Read error: %s", esp_err_to_name(err)); @@ -331,7 +330,7 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w this->status_set_warning(); return 0; } - if ((bytes_read == 0) && (ticks_to_wait > 0)) { + if ((bytes_read == 0) && (timeout_ms > 0)) { this->status_set_warning(); return 0; } diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 65ad7df1af..2c6528d8bf 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -42,7 +42,7 @@ class I2SAudioMicrophone final : public I2SAudioIn, public microphone::Microphon /// @param data void fix_dc_offset_(std::vector &data); - size_t read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait); + size_t read_(uint8_t *buf, size_t len, uint32_t timeout_ms); /// @brief Sets the Microphone ``audio_stream_info_`` member variable to the configured I2S settings. void configure_stream_settings_(); diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 5f8e5ca132..37a9afb84d 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,38 +1,27 @@ from __future__ import annotations -import contextlib +from collections.abc import Callable from dataclasses import dataclass -import hashlib -import io import logging from pathlib import Path -import re from PIL import Image, UnidentifiedImageError -from esphome import core, external_files import esphome.codegen as cg from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA import esphome.config_validation as cv -from esphome.const import ( - CONF_DEFAULTS, - CONF_DITHER, - CONF_FILE, - CONF_ICON, - CONF_ID, - CONF_PATH, - CONF_RAW_DATA_ID, - CONF_RESIZE, - CONF_SOURCE, - CONF_TYPE, - CONF_URL, -) -from esphome.core import CORE, HexInt +from esphome.const import CONF_DEFAULTS, CONF_FILE, CONF_ID, CONF_PLATFORM, CONF_TYPE +from esphome.core import CORE +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) DOMAIN = "image" DEPENDENCIES = ["display"] +IS_PLATFORM_COMPONENT = True + +# Name of the built-in static-image platform (local file / web / MDI sources). +PLATFORM_FILE = "file" image_ns = cg.esphome_ns.namespace("image") @@ -135,17 +124,6 @@ class ImageEncoder: """ return False - @classmethod - def get_options(cls) -> list[str]: - """ - Get the available options for this image encoder - """ - options = [*OPTIONS] - if not cls.is_endian(): - options.remove(CONF_BYTE_ORDER) - options.append(CONF_RAW_DATA_ID) - return options - def is_alpha_only(image: Image): """ @@ -338,60 +316,11 @@ TransparencyType = image_ns.enum("TransparencyType") CONF_TRANSPARENCY = "transparency" -# If the MDI file cannot be downloaded within this time, abort. -IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds - -SOURCE_LOCAL = "local" -SOURCE_WEB = "web" - -SOURCE_MDI = "mdi" -SOURCE_MDIL = "mdil" -SOURCE_MEMORY = "memory" - -MDI_SOURCES = { - SOURCE_MDI: "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/", - SOURCE_MDIL: "https://raw.githubusercontent.com/Pictogrammers/MaterialDesignLight/refs/heads/master/svg/", - SOURCE_MEMORY: "https://raw.githubusercontent.com/Pictogrammers/Memory/refs/heads/main/src/svg/", -} - Image_ = image_ns.class_("Image") INSTANCE_TYPE = Image_ -def compute_local_image_path(value) -> Path: - url = value[CONF_URL] if isinstance(value, dict) else value - h = hashlib.new("sha256") - h.update(url.encode()) - key = h.hexdigest()[:8] - base_dir = external_files.compute_local_file_dir(DOMAIN) - return base_dir / key - - -def local_path(value): - value = value[CONF_PATH] if isinstance(value, dict) else value - return str(CORE.relative_config_path(value)) - - -def download_file(url, path): - external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) - return str(path) - - -def download_gh_svg(value, source): - mdi_id = value[CONF_ICON] if isinstance(value, dict) else value - base_dir = external_files.compute_local_file_dir(DOMAIN) / source - path = base_dir / f"{mdi_id}.svg" - - url = MDI_SOURCES[source] + mdi_id + ".svg" - return download_file(url, path) - - -def download_image(value): - value = value[CONF_URL] if isinstance(value, dict) else value - return download_file(value, compute_local_image_path(value)) - - def is_svg_file(file): if not file: return False @@ -399,62 +328,6 @@ def is_svg_file(file): return " 500 or height > 500): - _LOGGER.warning( - 'The image "%s" you requested is very big. Please consider' - " using the resize parameter.", - path, - ) - - dither = ( - Image.Dither.NONE - if config[CONF_DITHER] == "NONE" - else Image.Dither.FLOYDSTEINBERG - ) - type = config[CONF_TYPE] - transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE) - invert_alpha = config[CONF_INVERT_ALPHA] - frame_count = 1 - if all_frames: - with contextlib.suppress(AttributeError): - frame_count = image.n_frames - if frame_count <= 1: - _LOGGER.warning("Image file %s has no animation frames", path) - - # Encode each frame with its own encoder and concatenate. This keeps every - # frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane] - # per frame) so animation frame stepping in image.cpp / animation.cpp stays - # correct without needing to know the total frame count. - byte_order = config.get(CONF_BYTE_ORDER) - combined_data: list[int] = [] - encoder: ImageEncoder | None = None - for frame_index in range(frame_count): - image.seek(frame_index) - encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha) - if byte_order is not None: - # Check for valid type has already been done in validate_settings - encoder.set_big_endian(byte_order == "BIG_ENDIAN") - pixels = encoder.convert(image.resize((width, height)), path).getdata() - for row in range(height): - for col in range(width): - encoder.encode(pixels[row * width + col]) - encoder.end_row() - encoder.end_image() - combined_data.extend(encoder.data) - - rhs = [HexInt(x) for x in combined_data] - prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - image_type = get_image_type_enum(type) - trans_value = get_transparency_enum(encoder.transparency) - - return prog_arr, width, height, image_type, trans_value, frame_count - - def add_metadata(id: str, width: int, height: int, image_type: str, transparency): all_metadata = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {}) all_metadata[str(id)] = ImageMetaData( @@ -780,17 +388,10 @@ def add_metadata(id: str, width: int, height: int, image_type: str, transparency ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: + # Base platform-component codegen: each entry is generated by its platform's + # own ``to_code``; here we only need the feature define to be present. cg.add_define("USE_IMAGE") - # By now the config will be a simple list. - for entry in config: - prog_arr, width, height, image_type, trans_value, _ = await write_image(entry) - cg.new_Pvariable( - entry[CONF_ID], prog_arr, width, height, image_type, trans_value - ) - add_metadata( - entry[CONF_ID], width, height, entry[CONF_TYPE], entry[CONF_TRANSPARENCY] - ) def get_all_image_metadata() -> dict[str, ImageMetaData]: @@ -801,3 +402,198 @@ def get_all_image_metadata() -> dict[str, ImageMetaData]: def get_image_metadata(image_id: str) -> ImageMetaData | None: """Get image metadata by ID for use by other components.""" return get_all_image_metadata().get(image_id) + + +# --------------------------------------------------------------------------- +# Legacy top-level component -> `image:` platform deprecation helpers +# -- REMOVE after 2027.1.0 together with the `animation:`/`online_image:` shims. +# +# `animation:` and `online_image:` used to be standalone top-level components and +# are now platforms of `image:`. Their deprecated top-level shims use this helper +# to (1) record each raw entry as it is validated and (2) print a single, +# pasteable migrated `image:` block once every entry has been seen. The block is +# emitted from FINAL_VALIDATE_SCHEMA, which always runs after every per-entry +# CONFIG_SCHEMA step, so all entries are captured before it fires. +# --------------------------------------------------------------------------- + + +def legacy_platform_migration_warning( + domain: str, platform: str, removal_version: str +) -> tuple[ + Callable[[ConfigType], ConfigType], + Callable[[ConfigType], ConfigType], +]: + """Build the per-entry capture and one-shot warning validators for a + deprecated top-level component that is now an ``image:`` platform. + + Returns ``(capture, finalize)``: + * ``capture`` is a ``CONFIG_SCHEMA`` validator placed *before* the real + schema so it sees the raw user entry; it records a copy of each entry. + * ``finalize`` is a ``FINAL_VALIDATE_SCHEMA`` validator that warns exactly + once with the migrated, pasteable ``image:`` block. + """ + entries_key = "legacy_entries" + shown_key = "legacy_warning_shown" + + def capture(config: ConfigType) -> ConfigType: + data = CORE.data.setdefault(domain, {}) + data.setdefault(entries_key, []).append(dict(config)) + return config + + def finalize(config: ConfigType) -> ConfigType: + data = CORE.data.setdefault(domain, {}) + if not data.get(shown_key): + data[shown_key] = True + + from esphome import yaml_util + + migrated = [ + {CONF_PLATFORM: platform, **entry} + for entry in data.get(entries_key, []) + ] + _LOGGER.warning( + "The top-level '%s:' configuration is deprecated and will be " + "removed in ESPHome %s. '%s' is now a platform of the 'image' " + "component. Replace your '%s:' block with:\n\n%s", + domain, + removal_version, + domain, + domain, + yaml_util.dump({DOMAIN: migrated}), + ) + return config + + return capture, finalize + + +# --------------------------------------------------------------------------- +# Legacy `image:` config migration -- REMOVE after 2027.1.0 +# +# Before `image` became a platform component, its top-level config was either a +# bare list of image dicts, a single image dict, or a dict with `defaults:`, +# `images:` and per-type group keys. This block transparently rewrites those +# forms into the new ``platform: file`` list and prints the migrated YAML. +# It is intentionally self-contained so it can be deleted in one piece together +# with the ``LEGACY_CONFIG_MIGRATE`` assignment below. +# --------------------------------------------------------------------------- + +LEGACY_REMOVAL_VERSION = "2027.1.0" + + +def _is_new_image_format(config: object) -> bool: + """True when the config is already the new ``platform:``-tagged list.""" + return isinstance(config, list) and all( + isinstance(entry, dict) and CONF_PLATFORM in entry for entry in config + ) + + +def _is_legacy_image_format(config: object) -> bool: + """True when ``config`` matches a shape the pre-platform schema accepted. + + Only these shapes are migrated. Anything else -- a list containing a + non-dict (or already platform-tagged) entry, or a dict with no recognised + image keys -- is left untouched so the platform validation surfaces a + proper error instead of the migration silently dropping the input. + """ + if isinstance(config, list): + # A bare list of (not-yet-platform-tagged) image dicts. + return bool(config) and all( + isinstance(entry, dict) and CONF_PLATFORM not in entry for entry in config + ) + if not isinstance(config, dict): + return False + # A single image dict, or the grouped `defaults:`/`images:`/type-key form. + return ( + CONF_ID in config + or CONF_FILE in config + or any( + key in (CONF_DEFAULTS, CONF_IMAGES) or key.upper() in IMAGE_TYPE + for key in config + ) + ) + + +def _flatten_legacy_image_config(config: object) -> list[dict]: + """Structurally flatten a legacy ``image:`` config into image dicts. + + No validation or file IO is performed -- the ``file`` platform schema + validates the resulting entries. Unrecognised shapes yield no entries so the + normal platform validation surfaces the error. + """ + if isinstance(config, list): + return [dict(entry) for entry in config if isinstance(entry, dict)] + if not isinstance(config, dict): + return [] + if CONF_ID in config or CONF_FILE in config: + return [dict(config)] + + defaults = config.get(CONF_DEFAULTS) or {} + result: list[dict] = [] + + def _add(entry: dict, extra: dict) -> None: + merged = {**defaults, **extra, **entry} + # The legacy `defaults:`/type-grouped forms only applied `byte_order` to + # types that support it. Replicate that so an endian default merged into + # e.g. a binary image stays valid. + type_class = IMAGE_TYPE.get(str(merged.get(CONF_TYPE, "")).upper()) + if ( + CONF_BYTE_ORDER in merged + and isinstance(type_class, type) + and issubclass(type_class, ImageEncoder) + and not type_class.is_endian() + ): + del merged[CONF_BYTE_ORDER] + result.append(merged) + + def _add_entries(entries: object, extra: dict) -> None: + # `entries` may be a single image dict or a list of them; non-dict + # members are silently skipped, mirroring the old `ensure_list` leniency. + for entry in [entries] if isinstance(entries, dict) else entries: + if isinstance(entry, dict): + _add(entry, extra) + + _add_entries(config.get(CONF_IMAGES, []), {}) + + for key, value in config.items(): + if key in (CONF_DEFAULTS, CONF_IMAGES) or key.upper() not in IMAGE_TYPE: + continue + type_extra = {CONF_TYPE: key} + if isinstance(value, dict) and ( + transparency_keys := [k for k in value if k in TRANSPARENCY_TYPES] + ): + for trans in transparency_keys: + _add_entries(value[trans], {**type_extra, CONF_TRANSPARENCY: trans}) + elif isinstance(value, (list, dict)): + _add_entries(value, type_extra) + return result + + +def _migrate_legacy_image_config(config: object) -> list[dict] | None: + """Rewrite a legacy ``image:`` config into the ``platform: file`` list. + + Returns None for the already-migrated platform form and for any shape the + pre-platform schema never accepted, so normal platform validation can + surface a proper error instead of the migration silently discarding input. + """ + if _is_new_image_format(config) or not _is_legacy_image_format(config): + return None + migrated = [ + {CONF_PLATFORM: PLATFORM_FILE, **entry} + for entry in _flatten_legacy_image_config(config) + ] + + from esphome import yaml_util + + _LOGGER.warning( + "The 'image:' configuration format is deprecated and will be removed in " + "ESPHome %s. Images are now platforms of the 'image' component. Replace " + "your 'image:' block with:\n\n%s", + LEGACY_REMOVAL_VERSION, + yaml_util.dump({DOMAIN: migrated}), + ) + return migrated + + +LEGACY_CONFIG_MIGRATE = _migrate_legacy_image_config + +# --------------------------- end legacy migration -------------------------- diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py index e175aa2220..5929f2b60a 100644 --- a/esphome/components/improv_base/__init__.py +++ b/esphome/components/improv_base/__init__.py @@ -42,4 +42,4 @@ async def setup_improv_core(var: MockObj, config: ConfigType, component: str): cg.add(var.set_next_url(_process_next_url(next_url))) cg.add_define(f"USE_{component.upper()}_NEXT_URL") - cg.add_library("improv/Improv", "1.2.4") + cg.add_library("improv/Improv", "1.2.6") diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 4ee703f363..a191889138 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -16,6 +16,7 @@ void ImprovSerialComponent::setup() { global_improv_serial_component = this; #ifdef USE_ESP32 this->uart_num_ = logger::global_logger->get_uart_num(); + this->uart_selection_ = logger::global_logger->get_uart(); #elif defined(USE_ARDUINO) this->hw_serial_ = logger::global_logger->get_hw_serial(); #endif @@ -30,21 +31,23 @@ void ImprovSerialComponent::setup() { } void ImprovSerialComponent::loop() { - if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { + const uint32_t now = App.get_loop_component_start_time(); + if (this->last_read_byte_ && (now - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { this->last_read_byte_ = 0; this->rx_buffer_.clear(); ESP_LOGV(TAG, "Timeout"); } - auto byte = this->read_byte_(); - while (byte.has_value()) { + while (true) { + auto byte = this->read_byte_(); + if (!byte.has_value()) + break; if (this->parse_improv_serial_byte_(byte.value())) { - this->last_read_byte_ = millis(); + this->last_read_byte_ = now; } else { this->last_read_byte_ = 0; this->rx_buffer_.clear(); } - byte = this->read_byte_(); } if (this->state_ == improv::STATE_PROVISIONING) { @@ -63,53 +66,6 @@ void ImprovSerialComponent::loop() { void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } -optional ImprovSerialComponent::read_byte_() { - optional byte; - uint8_t data = 0; -#ifdef USE_ESP32 - switch (logger::global_logger->get_uart()) { - case logger::UART_SELECTION_UART0: - case logger::UART_SELECTION_UART1: -#if defined(USE_ESP32_VARIANT_ESP32) - case logger::UART_SELECTION_UART2: -#endif - if (this->uart_num_ >= 0) { - size_t available; - uart_get_buffered_data_len(this->uart_num_, &available); - if (available) { - uart_read_bytes(this->uart_num_, &data, 1, 0); - byte = data; - } - } - break; -#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) - case logger::UART_SELECTION_USB_CDC: - if (esp_usb_console_available_for_read()) { - esp_usb_console_read_buf((char *) &data, 1); - byte = data; - } - break; -#endif // USE_LOGGER_USB_CDC -#ifdef USE_LOGGER_USB_SERIAL_JTAG - case logger::UART_SELECTION_USB_SERIAL_JTAG: { - if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) { - byte = data; - } - break; - } -#endif // USE_LOGGER_USB_SERIAL_JTAG - default: - break; - } -#elif defined(USE_ARDUINO) - if (this->hw_serial_->available()) { - this->hw_serial_->readBytes(&data, 1); - byte = data; - } -#endif - return byte; -} - void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) { // First, set length field this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1; @@ -133,7 +89,7 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) this->tx_header_[TX_CHECKSUM_IDX] = checksum; #ifdef USE_ESP32 - switch (logger::global_logger->get_uart()) { + switch (this->uart_selection_) { case logger::UART_SELECTION_UART0: case logger::UART_SELECTION_UART1: #if defined(USE_ESP32_VARIANT_ESP32) diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 4df6f6df2d..00c40c4c7e 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/components/improv_base/improv_base.h" +#include "esphome/components/logger/logger.h" #include "esphome/components/wifi/wifi_component.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" @@ -65,7 +66,52 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv std::vector build_rpc_settings_response_(improv::Command command); std::vector build_version_info_(); - optional read_byte_(); + ESPHOME_ALWAYS_INLINE optional read_byte_() { + optional byte; + uint8_t data = 0; +#ifdef USE_ESP32 + switch (this->uart_selection_) { + case logger::UART_SELECTION_UART0: + case logger::UART_SELECTION_UART1: +#if defined(USE_ESP32_VARIANT_ESP32) + case logger::UART_SELECTION_UART2: +#endif + if (this->uart_num_ >= 0) { + size_t available; + uart_get_buffered_data_len(this->uart_num_, &available); + if (available) { + uart_read_bytes(this->uart_num_, &data, 1, 0); + byte = data; + } + } + break; +#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) + case logger::UART_SELECTION_USB_CDC: + if (esp_usb_console_available_for_read()) { + esp_usb_console_read_buf((char *) &data, 1); + byte = data; + } + break; +#endif +#ifdef USE_LOGGER_USB_SERIAL_JTAG + case logger::UART_SELECTION_USB_SERIAL_JTAG: { + if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) { + byte = data; + } + break; + } +#endif + default: + break; + } +#elif defined(USE_ARDUINO) + if (this->hw_serial_->available()) { + this->hw_serial_->readBytes(&data, 1); + byte = data; + } +#endif + return byte; + } void write_data_(const uint8_t *data = nullptr, size_t size = 0); uint8_t tx_header_[TX_BUFFER_SIZE] = { @@ -85,6 +131,7 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv #ifdef USE_ESP32 uart_port_t uart_num_; + logger::UARTSelection uart_selection_{logger::UART_SELECTION_UART0}; #elif defined(USE_ARDUINO) Stream *hw_serial_{nullptr}; #endif diff --git a/esphome/components/internal_temperature/internal_temperature_esp32.cpp b/esphome/components/internal_temperature/internal_temperature_esp32.cpp index 1c44a9a238..64fe3707b1 100644 --- a/esphome/components/internal_temperature/internal_temperature_esp32.cpp +++ b/esphome/components/internal_temperature/internal_temperature_esp32.cpp @@ -3,17 +3,16 @@ #include "esphome/core/log.h" #include "internal_temperature.h" +#include + #if defined(USE_ESP32_VARIANT_ESP32) // there is no official API available on the original ESP32 extern "C" { uint8_t temprature_sens_read(); } -#elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \ - defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \ - defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) +#elif SOC_TEMP_SENSOR_SUPPORTED #include "driver/temperature_sensor.h" -#endif // USE_ESP32_VARIANT +#endif namespace esphome::internal_temperature { @@ -27,10 +26,7 @@ void InternalTemperatureSensor::update() { ESP_LOGV(TAG, "Raw temperature value: %d", raw); temperature = (raw - 32) / 1.8f; success = (raw != 128); -#elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \ - defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \ - defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) +#elif SOC_TEMP_SENSOR_SUPPORTED esp_err_t result = temperature_sensor_get_celsius(this->tsens_, &temperature); success = (result == ESP_OK); if (!success) { @@ -49,9 +45,7 @@ void InternalTemperatureSensor::update() { } void InternalTemperatureSensor::setup() { -#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ - defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ - defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if SOC_TEMP_SENSOR_SUPPORTED temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); esp_err_t result = temperature_sensor_install(&tsens_config, &this->tsens_); diff --git a/esphome/components/internal_temperature/internal_temperature_rp2040.cpp b/esphome/components/internal_temperature/internal_temperature_rp2.cpp similarity index 85% rename from esphome/components/internal_temperature/internal_temperature_rp2040.cpp rename to esphome/components/internal_temperature/internal_temperature_rp2.cpp index 66dee9faf7..11f8e27fc3 100644 --- a/esphome/components/internal_temperature/internal_temperature_rp2040.cpp +++ b/esphome/components/internal_temperature/internal_temperature_rp2.cpp @@ -1,4 +1,4 @@ -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/log.h" #include "internal_temperature.h" @@ -7,7 +7,7 @@ namespace esphome::internal_temperature { -static const char *const TAG = "internal_temperature.rp2040"; +static const char *const TAG = "internal_temperature.rp2"; void InternalTemperatureSensor::update() { float temperature = NAN; @@ -28,4 +28,4 @@ void InternalTemperatureSensor::update() { } // namespace esphome::internal_temperature -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py index 02730b6862..805138071e 100644 --- a/esphome/components/internal_temperature/sensor.py +++ b/esphome/components/internal_temperature/sensor.py @@ -10,7 +10,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_LN882X, PLATFORM_NRF52, - PLATFORM_RP2040, + PLATFORM_RP2, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, PlatformFramework, @@ -34,7 +34,7 @@ CONFIG_SCHEMA = cv.All( cv.only_on( [ PLATFORM_ESP32, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_BK72XX, PLATFORM_NRF52, PLATFORM_LN882X, @@ -58,7 +58,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, - "internal_temperature_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "internal_temperature_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, "internal_temperature_bk72xx.cpp": { PlatformFramework.BK72XX_ARDUINO, }, diff --git a/esphome/components/kamstrup_kmp/sensor.py b/esphome/components/kamstrup_kmp/sensor.py index 134ac245bf..75ec432ad9 100644 --- a/esphome/components/kamstrup_kmp/sensor.py +++ b/esphome/components/kamstrup_kmp/sensor.py @@ -21,6 +21,7 @@ from esphome.const import ( UNIT_EMPTY, UNIT_KELVIN, UNIT_KILOWATT, + UNIT_LITRE_PER_HOUR, ) CODEOWNERS = ["@cfeenstra1024"] @@ -37,7 +38,6 @@ CONF_TEMP2 = "temp2" CONF_TEMP_DIFF = "temp_diff" UNIT_GIGA_JOULE = "GJ" -UNIT_LITRE_PER_HOUR = "l/h" # Note: The sensor units are set automatically based un the received data from the meter CONFIG_SCHEMA = ( diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp index 6df114e93c..c47a80777c 100644 --- a/esphome/components/kuntze/kuntze.cpp +++ b/esphome/components/kuntze/kuntze.cpp @@ -7,13 +7,13 @@ namespace esphome::kuntze { static const char *const TAG = "kuntze"; -static const uint8_t CMD_READ_REG = 0x03; static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832}; // Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5) static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8; -void Kuntze::on_modbus_data(const std::vector &data) { +void Kuntze::on_response(std::span request_pdu, std::span response_pdu) { + auto data = modbus::helpers::server_pdu_payload(response_pdu); auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); }; this->waiting_ = false; @@ -76,7 +76,7 @@ void Kuntze::loop() { if (this->waiting_ || (this->state_ == 0)) return; this->last_send_ = now; - send(CMD_READ_REG, REGISTER[this->state_ - 1], 2); + this->read_holding_registers(REGISTER[this->state_ - 1], 2); this->waiting_ = true; } diff --git a/esphome/components/kuntze/kuntze.h b/esphome/components/kuntze/kuntze.h index 46681843d2..28c8089748 100644 --- a/esphome/components/kuntze/kuntze.h +++ b/esphome/components/kuntze/kuntze.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" +#include + namespace esphome::kuntze { class Kuntze final : public PollingComponent, public modbus::ModbusClientDevice { @@ -19,7 +21,7 @@ class Kuntze final : public PollingComponent, public modbus::ModbusClientDevice void loop() override; void update() override; - void on_modbus_data(const std::vector &data) override; + void on_response(std::span request_pdu, std::span response_pdu) override; void dump_config() override; diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 079bb32aab..7dbce7a07c 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -26,6 +26,7 @@ from esphome.const import ( from esphome.core import CORE from esphome.core.config import BOARD_MAX_LENGTH from esphome.helpers import copy_file_if_changed +from esphome.platformio.toolchain import copy_ccache_script from esphome.storage_json import StorageJSON from . import gpio # noqa: F401 @@ -76,12 +77,27 @@ _BLE5_BK_SYS_CONFIG_OPTIONS = [ "CFG_SUPPORT_BLE=0", ] +# Board ids upstream LibreTiny renamed; configs written against the old id +# keep validating and building against the new one (with a warning). +# generic-ln882hki -> generic-ln882h: LibreTiny v1.13.0. +_RENAMED_BOARDS = { + "generic-ln882hki": "generic-ln882h", +} + def _detect_variant(value): if KEY_LIBRETINY not in CORE.data: raise cv.Invalid("Family component didn't populate core data properly!") component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] board = value[CONF_BOARD] + if board not in component.boards and (renamed := _RENAMED_BOARDS.get(board)): + _LOGGER.warning( + "Board '%s' was renamed to '%s'; please update your configuration", + board, + renamed, + ) + value = value.copy() + value[CONF_BOARD] = board = renamed # read board-default family if not specified if board not in component.boards: if CONF_FAMILY not in value: @@ -257,7 +273,10 @@ FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, + # Raw PlatformIO package source — build internal, not a UI field. + cv.Optional( + CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY + ): cv.string_strict, cv.Optional(CONF_LOGLEVEL, default="warn"): ( cv.one_of(*LT_LOGLEVELS, upper=True) ), @@ -488,6 +507,7 @@ async def component_to_code(config): # it for project source files only. GCC uses the last -O flag. build_src_flags += " -Os" cg.add_platformio_option("build_src_flags", build_src_flags) + cg.add_platformio_option("extra_scripts", ["pre:ccache.py"]) # IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops). # On other families, patch_linker.py routes .sram.text into the right # RAM-executable output section and prints a post-link placement summary. @@ -562,8 +582,13 @@ async def component_to_code(config): cg.add_platformio_option("custom_fw_name", "esphome") cg.add_platformio_option("custom_fw_version", __version__) - # Apply chip-specific SDK options to save RAM/Flash - if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238): + # Apply chip-specific SDK options to save RAM/Flash. + # Skipped when bk72xx_ble is configured: add_platformio_option APPENDS list + # values (it never replaces), so emitting the disable here as well would put + # both CFG_SUPPORT_BLE=0 and =1 into the generated sys_config.h and rely on + # last-wins emission order. Skipping keeps it a single unambiguous define. + ble_requested = "bk72xx_ble" in CORE.config + if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238) and not ble_requested: cg.add_platformio_option( "custom_options.sys_config#h", _BLE5_BK_SYS_CONFIG_OPTIONS ) @@ -587,3 +612,4 @@ def copy_files() -> None: patch_linker_file, CORE.relative_build_path("patch_linker.py"), ) + copy_ccache_script() diff --git a/esphome/components/libretiny/gpio_arduino.cpp b/esphome/components/libretiny/gpio_arduino.cpp index 1af0dce16d..b1a37cb225 100644 --- a/esphome/components/libretiny/gpio_arduino.cpp +++ b/esphome/components/libretiny/gpio_arduino.cpp @@ -5,7 +5,7 @@ namespace esphome::libretiny { -static const char *const TAG = "lt.gpio"; +static const char *const TAG = "libretiny.gpio"; static int IRAM_ATTR flags_to_mode(gpio::Flags flags) { if (flags == gpio::FLAG_INPUT) { diff --git a/esphome/components/libretiny/hal.cpp b/esphome/components/libretiny/hal.cpp index 67e902024d..01b276005d 100644 --- a/esphome/components/libretiny/hal.cpp +++ b/esphome/components/libretiny/hal.cpp @@ -44,7 +44,7 @@ void arch_init() { void arch_restart() { lt_reboot(); - while (1) { + while (true) { } } diff --git a/esphome/components/libretiny/hal.h b/esphome/components/libretiny/hal.h index 01a7b5450b..48b94a5214 100644 --- a/esphome/components/libretiny/hal.h +++ b/esphome/components/libretiny/hal.h @@ -44,6 +44,7 @@ // it is callable from Thumb code via interworking. The MRS CPSR instruction // is ARM-only and user code here may be built in Thumb, so in_isr_context() // defers to this port helper on BK72xx instead of reading CPSR inline. +// NOLINTNEXTLINE(readability-redundant-declaration) extern "C" uint32_t platform_is_in_interrupt_context(void); #endif @@ -59,9 +60,11 @@ extern "C" void delayMicroseconds(unsigned int us); // Forward decls from libretiny's family for the inline arch_* // wrappers below. Pulling the full header would drag in the rest of the // LibreTiny C API. +// NOLINTBEGIN(readability-redundant-declaration) extern "C" void lt_wdt_feed(void); extern "C" uint32_t lt_cpu_get_cycle_count(void); extern "C" uint32_t lt_cpu_get_freq(void); +// NOLINTEND(readability-redundant-declaration) namespace esphome::libretiny {} diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp index c01661b3a6..0ab064e3e1 100644 --- a/esphome/components/libretiny/lt_component.cpp +++ b/esphome/components/libretiny/lt_component.cpp @@ -6,21 +6,21 @@ namespace esphome::libretiny { -static const char *const TAG = "lt.component"; +static const char *const TAG = "libretiny"; void LTComponent::dump_config() { ESP_LOGCONFIG(TAG, "LibreTiny:\n" " Version: %s\n" " Loglevel: %u", - LT_BANNER_STR + 10, LT_LOGLEVEL); + <_BANNER_STR[10], LT_LOGLEVEL); #if defined(__OPTIMIZE_SIZE__) && __OPTIMIZE_LEVEL__ > 0 && __OPTIMIZE_LEVEL__ <= 3 ESP_LOGCONFIG(TAG, " Optimization: -Os, SDK: -O" STRINGIFY_MACRO(__OPTIMIZE_LEVEL__)); #endif #ifdef USE_TEXT_SENSOR if (this->version_ != nullptr) { - this->version_->publish_state(LT_BANNER_STR + 10); + this->version_->publish_state(<_BANNER_STR[10]); } #endif // USE_TEXT_SENSOR } diff --git a/esphome/components/libretiny/preference_backend.h b/esphome/components/libretiny/preference_backend.h index 66b6847bee..f7f8279ac0 100644 --- a/esphome/components/libretiny/preference_backend.h +++ b/esphome/components/libretiny/preference_backend.h @@ -5,8 +5,10 @@ #include // Forward declare FlashDB types to avoid pulling in flashdb.h +// NOLINTBEGIN(readability-identifier-naming) struct fdb_kvdb; struct fdb_blob; +// NOLINTEND(readability-identifier-naming) namespace esphome::libretiny { diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 7b28065e4e..4251565e85 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -213,18 +213,29 @@ LightColorValues LightCall::validate_() { // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. bool explicit_turn_off_request = this->has_state() && !this->state_; - // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). - if (this->has_brightness() && this->brightness_ == 0.0f) { + // Treat zero brightness as an implicit turn-off when no state was explicitly requested. + if (this->has_brightness() && this->brightness_ == 0.0f && !this->has_state()) { this->state_ = false; this->set_flag_(FLAG_HAS_STATE); - if (color_mode & ColorCapability::BRIGHTNESS) { - // Reset brightness so the light has nonzero brightness when turned back on. - this->brightness_ = 1.0f; - } else { - // Light doesn't support brightness; clear the flag to avoid a spurious - // "brightness not supported" warning during capability validation. - this->clear_flag_(FLAG_HAS_BRIGHTNESS); - } + } + + // A light without brightness control has no way to represent "on but dark", so zero + // brightness -- how effects encode their dark phase -- means the light is off. Clear the + // brightness as well, so a zero can't linger in remote_values and leave the light stuck + // off: a later turn-on can't heal it, because the capability check below drops any + // brightness this mode doesn't support. explicit_turn_off_request was captured above, so + // a running effect is not stopped by this. + if (this->has_brightness() && this->brightness_ == 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { + this->state_ = false; + this->set_flag_(FLAG_HAS_STATE); + this->clear_flag_(FLAG_HAS_BRIGHTNESS); + } + + // Make sure a simple (no specific brightness) turn-on makes the light visible + if (this->has_state() && this->state_ && (color_mode & ColorCapability::BRIGHTNESS) && !this->has_brightness() && + this->parent_->remote_values.get_brightness() == 0.0f) { + this->brightness_ = 1.0f; + this->set_flag_(FLAG_HAS_BRIGHTNESS); } // Set color brightness to 100% if currently zero and a color is set. diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index bd778926d5..9d0181a05c 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -71,6 +71,14 @@ void LightState::setup() { break; } + // A light coming up on boot must never end up on-but-invisible: if the resolved restore + // state is on but its brightness is zero (e.g. a stale/persisted value from before a + // forced-on restore mode, or an inverted restore flipping a dim-to-0 off state to on), + // reset it to full brightness. + if (recovered.state && recovered.brightness == 0.0f) { + recovered.brightness = 1.0f; + } + call.set_color_mode_if_supported(recovered.color_mode); call.set_state(recovered.state); call.set_brightness_if_supported(recovered.brightness); diff --git a/esphome/components/ln882h_ble/__init__.py b/esphome/components/ln882h_ble/__init__.py new file mode 100644 index 0000000000..f499dbc6fd --- /dev/null +++ b/esphome/components/ln882h_ble/__init__.py @@ -0,0 +1,48 @@ +"""LN882H BLE — BLE controller support for the LN882H LibreTiny chips. + +The platform analog of esp32_ble / rp2040_ble: owns the LN882H BLE stack +bring-up and the controller BLE address. Consumers (ln882h_ble_tracker) build +on this component and contain no SDK calls of their own. + +The BLE stack is compiled and linked by the LibreTiny lightning-ln882h builder +when CFG_SUPPORT_BLE=1 is set via custom_options.proj_config#h, which this +component does (LibreTiny v1.13.0+). +""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID +from esphome.types import ConfigType + +DEPENDENCIES = ["ln882x"] +CODEOWNERS = ["@Bl00d-B0b"] + +ln882h_ble_ns = cg.esphome_ns.namespace("ln882h_ble") +LN882HBLE = ln882h_ble_ns.class_("LN882HBLE", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LN882HBLE), + # Default off: on the single-core LN882H, bringing the BLE stack up during + # boot competes with the WiFi connection handshake. Consumers enable the + # stack lazily on first use (e.g. the tracker's first scan start). + cv.Optional(CONF_ENABLE_ON_BOOT, default=False): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + + # Enable the BLE stack in the build. The LibreTiny lightning-ln882h builder + # gates the BLE stack sources and libraries on CFG_SUPPORT_BLE, read from + # the custom option key "proj_config#h" (the '#h' maps to proj_config.h) + # before the stock header's default of 0. A list is used so any other + # component adding to this key concatenates with it rather than replacing + # it (add_platformio_option only merges when both values are lists). + cg.add_platformio_option("custom_options.proj_config#h", ["CFG_SUPPORT_BLE=1"]) + + cg.add_define("USE_LN882H_BLE") diff --git a/esphome/components/ln882h_ble/ln882h_ble.cpp b/esphome/components/ln882h_ble/ln882h_ble.cpp new file mode 100644 index 0000000000..dd8fee390d --- /dev/null +++ b/esphome/components/ln882h_ble/ln882h_ble.cpp @@ -0,0 +1,251 @@ +// ln882h_ble.cpp +// +// BLE controller support for the LN882H (LibreTiny lightning-ln882h family) — +// the platform analog of esp32_ble / rp2040_ble. Owns the one-time stack +// bring-up (rw_init + the ln_* app init sequence) and the controller BLE +// address (persistent KV entry, WiFi-MAC-derived once). Consumers +// (ln882h_ble_tracker) build on this component and contain no SDK calls of +// their own. +// +// BLE stack init and scan lifecycle mirror the SDK's ble_app usage. The BLE +// stack itself is compiled and linked by the LibreTiny lightning-ln882h builder +// (CFG_SUPPORT_BLE=1 via custom_options.proj_config#h; prebuilt +// libln882h_ble_full_stack.a). + +#include "ln882h_ble.h" // pulls esphome/core/defines.h for USE_LN882H_BLE + +#ifdef USE_LN882H_BLE + +#include +#include +#include + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" // get_mac_address_raw() +#include "esphome/core/log.h" + +// --------------------------------------------------------------------------- +// LN882H BLE SDK — forward declarations +// --------------------------------------------------------------------------- +extern "C" { + +struct ln_bd_addr_v_t { // NOLINT(readability-identifier-naming) - mirrors the SDK type name + uint8_t addr[6]; +}; // ABI-identical to ln_bd_addr_t +void ln_kv_ble_app_init(void); +struct ln_bd_addr_v_t *ln_kv_ble_pub_addr_get(void); +int ln_kv_ble_addr_store(struct ln_bd_addr_v_t addr); +void soc_module_clk_gate_enable(uint32_t clk); + +void rw_init(uint8_t mac[6]); +void ln_gap_app_init(void); +void ln_gatt_app_init(void); +void ln_ble_conn_mgr_init(void); +void ln_ble_evt_mgr_init(void); +void ln_ble_smp_init(void); +void ln_ble_scan_mgr_init(void); +void ln_rw_app_task_init(void); +void ln_gap_reset(void); + +void ln_ble_scan_actv_creat(void); +void ln_ble_scan_start(void *scan_param); +void ln_ble_scan_stop(void); + +} // extern "C" + +// ln_bd_addr_v_t mirrors the SDK's ln_bd_addr_t (ln_ble_app_defines.h) and is +// passed to ln_kv_ble_addr_store() by value, so its size and alignment are part +// of the calling convention. +static_assert(sizeof(struct ln_bd_addr_v_t) == 6, "ln_bd_addr_v_t must match the SDK's ln_bd_addr_t layout"); +static_assert(alignof(struct ln_bd_addr_v_t) == 1, "ln_bd_addr_v_t must stay byte-aligned like the SDK type"); + +// --------------------------------------------------------------------------- +// LN882H SDK constants +// CLK_G_BLE — hal/hal_clock.h clock gate bit for the BLE block +// BLE_EVT_ID_SCAN_REPORT — ble/ble_evt.h event id for scan reports +// GAPM_* — ble/mac/ble/hl/api/gapm_task.h, enums gapm_scan_type / +// gapm_dup_filter_pol / gapm_scan_prop +// --------------------------------------------------------------------------- +static constexpr uint32_t CLK_G_BLE = 1u << 0; + +// WiFi/BLE packet-traffic-indication (PTI) arbitration register. The LN882H SDK +// exposes no symbolic name for this register; the address and value replicate +// the SDK reference bring-up. 0x003F sets all six PTI priority bits so the +// arbiter can pre-empt WiFi for BLE traffic. +static constexpr uint32_t BLE_COEX_PTI_REG_ADDR = 0x400121F8; +static constexpr uint32_t BLE_COEX_PTI_ENABLE_ALL = 0x003F; + +// ble_app_default_cfg.h BLE_DEFAULT_PUBLIC_ADDR, in the SDK's ln_bd_addr_t +// array order — least-significant octet first, the BLE/HCI convention (the +// SDK's own AT commands print addr[5]..addr[0]). Printable form: +// 00:FF:03:12:34:56. +static constexpr uint8_t BLE_DEFAULT_ADDR[6] = {0x56, 0x34, 0x12, 0x03, 0xFF, 0x00}; + +// The SDK's KV loader treats an all-zero address as unset and substitutes the +// default; resolve_mac_() applies the same rule to a stored entry. +static bool is_unset_addr(const uint8_t (&addr)[6]) { + return std::all_of(std::begin(addr), std::end(addr), [](uint8_t b) { return b == 0; }); +} + +// gapm_scan_type: GEN_DISC = 0, LIM_DISC = 1, OBSERVER = 2. Observer reports every +// advertisement without filtering — what a tracker wants. +static constexpr uint8_t GAPM_SCAN_TYPE_OBSERVER = 2; +static constexpr uint8_t GAPM_DUP_FILT_DIS = 0; +// gapm_scan_prop bits: PHY_1M = 1<<0, PHY_CODED = 1<<1, ACTIVE_1M = 1<<2, ACTIVE_CODED = 1<<3. +static constexpr uint8_t GAPM_SCAN_PROP_PHY_1M_BIT = 1 << 0; + +// Scan parameter block passed to ln_ble_scan_start(); mirrors the SDK layout, +// with the pad byte explicit so the whole block zero-initialises. +struct le_scan_parameters_t { // NOLINT(readability-identifier-naming) - mirrors the SDK type name + uint8_t type; + uint8_t prop; + uint8_t dup_filt_pol; + uint8_t pad; + uint16_t scan_intv; + uint16_t scan_wd; +}; +// Pin the compiler's layout decisions for the hand-mirrored SDK struct: it is +// passed to ln_ble_scan_start() as void*, so a padding drift would silently +// feed garbage scan parameters to the controller. +static_assert(sizeof(le_scan_parameters_t) == 8, "le_scan_parameters_t must match the SDK layout"); +static_assert(offsetof(le_scan_parameters_t, scan_intv) == 4, "unexpected padding in le_scan_parameters_t"); + +// --------------------------------------------------------------------------- +// __sprintf weak stub +// +// The LN882H BLE SDK objects reference __sprintf (a Beken/LN libc alias) that +// LibreTiny's newlib does not provide. Supply a weak fallback so linking +// succeeds; a real definition, if one is ever provided, takes precedence. +// --------------------------------------------------------------------------- +#include +#include +extern "C" __attribute__((weak)) int +__sprintf( // NOLINT(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + char *str, const char *format, ...) { + va_list args; + va_start(args, format); + int ret = vsprintf(str, format, args); // NOLINT + va_end(args); + return ret; +} + +namespace esphome::ln882h_ble { + +static const char *const TAG = "ln882h_ble"; + +// --------------------------------------------------------------------------- +// Component lifecycle +// --------------------------------------------------------------------------- + +void LN882HBLE::setup() { + // Resolve the MAC early so get_mac_lsb_first() is valid for consumers before + // the stack is up. The KV load also happens here (no stack dependency). + this->resolve_mac_(); + if (this->enable_on_boot_) { + this->enable(); + } +} + +// AFTER_WIFI, not BLUETOOTH: replicates the proven pre-split timing — the LN +// SDK's KV subsystem is first touched only once WiFi is up; earlier access +// destabilized the device in hardware testing. +float LN882HBLE::get_setup_priority() const { return setup_priority::AFTER_WIFI; } + +void LN882HBLE::enable() { + if (this->state_ != BLEComponentState::STATE_OFF) + return; + this->state_ = BLEComponentState::ENABLING; + + *reinterpret_cast(BLE_COEX_PTI_REG_ADDR) = BLE_COEX_PTI_ENABLE_ALL; + soc_module_clk_gate_enable(CLK_G_BLE); + + rw_init(this->ble_mac_); + ln_gap_app_init(); + ln_gatt_app_init(); + ln_ble_conn_mgr_init(); + ln_ble_evt_mgr_init(); + ln_ble_smp_init(); + ln_ble_scan_mgr_init(); + ln_rw_app_task_init(); + ln_gap_reset(); + + delay(100); // NOLINT — one-time BLE stack init; SDK requires this settle time + + ln_ble_scan_actv_creat(); + delay(10); + + // Prime the scan activity with a short probe start/stop — the SDK's scan + // manager completes activity creation on the first start. + // static: its address is handed to ln_ble_scan_start(void *), which may + // retain it past this call. + static le_scan_parameters_t probe_p{}; + probe_p.type = GAPM_SCAN_TYPE_OBSERVER; + probe_p.prop = GAPM_SCAN_PROP_PHY_1M_BIT; + probe_p.dup_filt_pol = GAPM_DUP_FILT_DIS; + probe_p.scan_intv = 160; + probe_p.scan_wd = 16; + ln_ble_scan_start(&probe_p); + delay(10); + ln_ble_scan_stop(); + + this->state_ = BLEComponentState::ACTIVE; + ESP_LOGD(TAG, "BLE stack initialised"); +} + +void LN882HBLE::get_mac_lsb_first(uint8_t out[6]) const { memcpy(out, this->ble_mac_, sizeof(this->ble_mac_)); } + +void LN882HBLE::dump_config() { + ESP_LOGCONFIG(TAG, + "LN882H BLE:\n" + " MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n" + " Active: %s", + this->ble_mac_[5], this->ble_mac_[4], this->ble_mac_[3], this->ble_mac_[2], this->ble_mac_[1], + this->ble_mac_[0], YESNO(this->is_active())); +} + +// --------------------------------------------------------------------------- +// MAC resolution +// --------------------------------------------------------------------------- + +void LN882HBLE::resolve_mac_() { + ln_kv_ble_app_init(); + + // ln_kv_ble_app_init() loads the persistent address from the "2_ble_addr" KV + // entry, falling back to BLE_DEFAULT_ADDR when nothing is stored. A stored + // address is preferred so boot does not write flash. Order is LSB-first + // throughout (BLE/HCI convention); consumers reverse for printable form. + ln_bd_addr_v_t bt_addr{}; + bool have_unique_addr = false; + if (const ln_bd_addr_v_t *stored = ln_kv_ble_pub_addr_get(); stored != nullptr) { + bt_addr = *stored; + // All-zero is "unset", not a unique address: ln_kv_ble_addr_load() itself + // substitutes the default for it, so programming it verbatim would give the + // controller a null address. Treat it like the default and derive instead. + have_unique_addr = + memcmp(bt_addr.addr, BLE_DEFAULT_ADDR, sizeof(bt_addr.addr)) != 0 && !is_unset_addr(bt_addr.addr); + } else { + // KV subsystem down (wrong partition layout, corrupted region): derive + // below instead of dereferencing null and boot-looping. + ESP_LOGW(TAG, "BLE address KV unavailable; deriving address from WiFi MAC"); + } + if (!have_unique_addr) { + uint8_t wifi_mac[6] = {0}; + get_mac_address_raw(wifi_mac); // MSB-first + // Reverse into controller (LSB-first) order, then BLE = WiFi + 1: increment + // the NIC low byte (addr[0] once reversed), no carry, OUI unchanged — the + // Beken/Tuya factory pairing the bk72xx sibling also uses. + for (int i = 0; i < 6; i++) + bt_addr.addr[i] = wifi_mac[5 - i]; + bt_addr.addr[0] = static_cast(bt_addr.addr[0] + 1); + if (int err = ln_kv_ble_addr_store(bt_addr); err != 0) { + ESP_LOGW(TAG, "Failed to persist derived BLE address (err %d); will re-derive next boot", err); + } else { + ESP_LOGD(TAG, "MAC derived (WiFi+1) and stored"); + } + } + memcpy(this->ble_mac_, bt_addr.addr, 6); +} + +} // namespace esphome::ln882h_ble + +#endif // USE_LN882H_BLE diff --git a/esphome/components/ln882h_ble/ln882h_ble.h b/esphome/components/ln882h_ble/ln882h_ble.h new file mode 100644 index 0000000000..0e3e311341 --- /dev/null +++ b/esphome/components/ln882h_ble/ln882h_ble.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_LN882H_BLE + +#include "esphome/core/component.h" + +#include + +namespace esphome::ln882h_ble { + +enum class BLEComponentState : uint8_t { + STATE_OFF = 0, + ENABLING, + ACTIVE, +}; + +class LN882HBLE final : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + /// Bring up the LN882H BLE stack (one-time; the SDK has no teardown path). + /// Requires setup() to have run: rw_init() is handed the address resolved + /// there. Blocks ~120 ms across the SDK's settle points, so calling it from + /// loop() (enable_on_boot: false) trips the loop-blocking warning once. + void enable(); + bool is_active() const { return this->state_ == BLEComponentState::ACTIVE; } + + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + + /// Controller BLE address in the SDK's ln_bd_addr_t order: least-significant + /// octet first (the BLE/HCI convention). Reverse for printable form — the + /// byte order is in the name so platform analogs cannot be confused + /// (the bk72xx sibling exposes the same accessor). + void get_mac_lsb_first(uint8_t out[6]) const; + + protected: + void resolve_mac_(); + + uint8_t ble_mac_[6]{0}; // controller (LSB-first) order, as ln_bd_addr_t stores it + BLEComponentState state_{BLEComponentState::STATE_OFF}; + bool enable_on_boot_{false}; +}; + +} // namespace esphome::ln882h_ble + +#endif // USE_LN882H_BLE diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 9629dce0bf..77a875dd8f 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -54,7 +54,7 @@ from esphome.const import ( PLATFORM_ESP8266, PLATFORM_LN882X, PLATFORM_NRF52, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_RTL87XX, PlatformFramework, ) @@ -154,7 +154,7 @@ HARDWARE_UART_TO_SERIAL = { UART2: cg.global_ns.Serial2, DEFAULT: cg.global_ns.Serial, }, - PLATFORM_RP2040: { + PLATFORM_RP2: { UART0: cg.global_ns.Serial1, UART1: cg.global_ns.Serial2, USB_CDC: cg.global_ns.Serial, @@ -171,7 +171,7 @@ def uart_selection(value): return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) if CORE.is_esp8266: return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) - if CORE.is_rp2040: + if CORE.is_rp2: return cv.one_of(*UART_SELECTION_RP2040, upper=True)(value) if CORE.is_libretiny: family = get_libretiny_family() @@ -282,7 +282,7 @@ CONFIG_SCHEMA = cv.All( esp32_s2=USB_CDC, esp32_s3=USB_SERIAL_JTAG, esp32_s31=USB_SERIAL_JTAG, - rp2040=USB_CDC, + rp2=USB_CDC, bk72xx=DEFAULT, ln882x=DEFAULT, rtl87xx=DEFAULT, @@ -292,7 +292,7 @@ CONFIG_SCHEMA = cv.All( [ PLATFORM_ESP8266, PLATFORM_ESP32, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX, @@ -417,11 +417,7 @@ async def _late_logger_init(config: ConfigType) -> None: cg.add_define("USE_ESP8266_LOGGER_SERIAL1") enable_serial1() - if ( - (CORE.is_esp8266 or CORE.is_rp2040) - and has_serial_logging - and is_at_least_verbose - ): + if (CORE.is_esp8266 or CORE.is_rp2) and has_serial_logging and is_at_least_verbose: debug_serial_port = HARDWARE_UART_TO_SERIAL[CORE.target_platform][ config.get(CONF_HARDWARE_UART) ] @@ -605,7 +601,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( }, "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "logger_host.cpp": {PlatformFramework.HOST_NATIVE}, - "logger_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "logger_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, "logger_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 684da0202e..6527b6aa8c 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -206,7 +206,7 @@ void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } #endif -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) UARTSelection Logger::get_uart() const { return this->uart_; } #endif diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 784cbea67e..69d8e6d32a 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -23,10 +23,10 @@ #if defined(USE_ESP8266) #include #endif // USE_ESP8266 -#ifdef USE_RP2040 +#ifdef USE_RP2 #include #include -#endif // USE_RP2040 +#endif // USE_RP2 #endif // USE_ARDUINO #ifdef USE_ESP32 @@ -96,7 +96,7 @@ struct CStrCompare { // macOS allows up to 64 bytes, Linux up to 16 static constexpr size_t THREAD_NAME_BUF_SIZE = 64; -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * * Advanced configuration (pin selection, etc) is not supported. @@ -122,7 +122,7 @@ enum UARTSelection : uint8_t { UART_SELECTION_UART0_SWAP, #endif // USE_ESP8266 }; -#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY || USE_ZEPHYR +#endif // USE_ESP32 || USE_ESP8266 || USE_RP2 || USE_LIBRETINY || USE_ZEPHYR /** * @brief Logger component for all ESPHome logging. @@ -160,7 +160,7 @@ class Logger final : public Component { #ifdef USE_HOST void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } #endif -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. UARTSelection get_uart() const; @@ -351,7 +351,7 @@ class Logger final : public Component { #endif // Group smaller types together at the end uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_ZEPHYR) UARTSelection uart_{UART_SELECTION_UART0}; #endif #ifdef USE_LIBRETINY @@ -505,8 +505,8 @@ class LoggerMessageTrigger final : public Triggerbaud_rate_ > 0) { static const struct device *uart_dev = nullptr; switch (this->uart_) { - case UART_SELECTION_UART0: + case UART_SELECTION_UART0: // NOLINT(bugprone-branch-clone) uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0)); break; case UART_SELECTION_UART1: diff --git a/esphome/components/logger/task_log_buffer_libretiny.cpp b/esphome/components/logger/task_log_buffer_libretiny.cpp index b6d6b22ab5..5cde18d19e 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.cpp +++ b/esphome/components/logger/task_log_buffer_libretiny.cpp @@ -20,7 +20,7 @@ TaskLogBuffer::~TaskLogBuffer() { } } -size_t TaskLogBuffer::available_contiguous_space() const { +size_t TaskLogBuffer::available_contiguous_space_() const { if (this->head_ >= this->tail_) { // head is ahead of or equal to tail // Available space is from head to end, plus from start to tail @@ -81,7 +81,7 @@ void TaskLogBuffer::release_message_main_loop() { this->tail_ = 0; } - this->message_count_--; + this->message_count_ = this->message_count_ - 1; this->current_message_size_ = 0; xSemaphoreGive(this->mutex_); @@ -117,7 +117,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin } // Check if we have enough contiguous space - size_t contiguous = this->available_contiguous_space(); + size_t contiguous = this->available_contiguous_space_(); if (contiguous < total_size) { // Not enough contiguous space at end @@ -128,9 +128,9 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin } // Need at least enough space to safely write padding marker (level field is at end of struct) - constexpr size_t PADDING_MARKER_MIN_SPACE = offsetof(LogMessage, level) + 1; + constexpr size_t padding_marker_min_space = offsetof(LogMessage, level) + 1; - if (space_at_start >= total_size && this->head_ > 0 && contiguous >= PADDING_MARKER_MIN_SPACE) { + if (space_at_start >= total_size && this->head_ > 0 && contiguous >= padding_marker_min_space) { // Add padding marker (set level field to indicate this is padding, not a real message) LogMessage *padding = reinterpret_cast(this->storage_ + this->head_); padding->level = PADDING_MARKER_LEVEL; @@ -180,7 +180,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin this->head_ = 0; } - this->message_count_++; + this->message_count_ = this->message_count_ + 1; xSemaphoreGive(this->mutex_); return true; diff --git a/esphome/components/logger/task_log_buffer_libretiny.h b/esphome/components/logger/task_log_buffer_libretiny.h index b42894502a..ce469a1a18 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.h +++ b/esphome/components/logger/task_log_buffer_libretiny.h @@ -84,7 +84,7 @@ class TaskLogBuffer { static inline size_t message_total_size(size_t text_length) { return sizeof(LogMessage) + text_length + 1; } // Calculate available contiguous space at write position - size_t available_contiguous_space() const; + size_t available_contiguous_space_() const; uint8_t storage_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) size_t head_{0}; // Write position diff --git a/esphome/components/ltr501/sensor.py b/esphome/components/ltr501/sensor.py index cca9330e76..c1fa9009b3 100644 --- a/esphome/components/ltr501/sensor.py +++ b/esphome/components/ltr501/sensor.py @@ -15,7 +15,7 @@ from esphome.const import ( CONF_NAME, CONF_REPEAT, CONF_TYPE, - DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, ICON_BRIGHTNESS_5, ICON_BRIGHTNESS_6, @@ -159,7 +159,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, @@ -169,7 +169,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_7, accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, @@ -179,7 +179,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_PROXIMITY, accuracy_decimals=0, - device_class=DEVICE_CLASS_DISTANCE, + device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, @@ -188,7 +188,7 @@ CONFIG_SCHEMA = cv.All( sensor.sensor_schema( icon=ICON_GAIN, accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 9137412abe..bfe91eedd5 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,4 +1,3 @@ -import functools import importlib from pathlib import Path import pkgutil @@ -52,9 +51,11 @@ from esphome.writer import clean_build from esphome.yaml_util import load_yaml from . import defines as df, lv_validation as lvalid, widgets +from .animation import ANIMATION_SCHEMA, add_animation_triggers, animations_to_code from .automation import layers_to_code, lvgl_update from .defines import ( CONF_ALIGN_TO_LAMBDA_ID, + CONF_ANIMATIONS, LOGGER, add_lv_use, get_focused_widgets, @@ -84,7 +85,7 @@ from .schemas import ( any_widget_schema, container_schema, container_schema_value, - obj_dict, + theme_schema, ) from .styles import styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code @@ -146,6 +147,8 @@ SIMPLE_TRIGGERS = ( df.CONF_ON_RESUME, df.CONF_ON_DRAW_START, df.CONF_ON_DRAW_END, + df.CONF_ON_LANDSCAPE, + df.CONF_ON_PORTRAIT, ) @@ -211,6 +214,18 @@ def multi_conf_validate(configs: list[dict]): raise cv.Invalid( f"'{item}' must have an explicit group set when using multiple LVGL instances" ) + # The hidden styles a `theme:` block creates are tracked in a single map shared + # by all LVGL instances (keyed only by widget type, not by instance), so a + # second instance's `theme:` would silently lose to whichever instance is + # processed first instead of doing what its config implies. + themed_configs = sum( + 1 for config in configs if config.get(df.CONF_THEME) is not None + ) + if themed_configs > 1: + raise cv.Invalid( + "'theme' may only be set on one LVGL instance when using multiple LVGL " + "instances -- combine both themes into a single instance's 'theme:' block" + ) base_config = configs[0] for config in configs[1:]: for item in ( @@ -414,6 +429,10 @@ async def to_code(configs): await cg.register_component(lv_component, config) if rotation := config.get(CONF_ROTATION): cg.add(lv_component.set_rotation(rotation)) + if paused := config[df.CONF_PAUSED]: + cg.add(lv_component.set_paused(paused, False)) + if refr_time := config.get(df.CONF_REFRESH_INTERVAL): + cg.add(lv_component.set_refresh_interval(refr_time.total_milliseconds)) Widget.create(config[CONF_ID], lv_component, LvScrActType(), config) lv_scr_act = get_screen_active(lv_component) @@ -431,7 +450,8 @@ async def to_code(configs): await layers_to_code(lv_component, config) await lvgl_update(lv_component, config) await msgboxes_to_code(lv_component, config) - # await disp_update(lv_component.get_disp(), config) + await animations_to_code(config.get(CONF_ANIMATIONS, [])) + # Mark all widgets as completed so awaiters of ``wait_for_widgets`` proceed. set_widgets_completed(True) async with LvContext(): @@ -439,6 +459,7 @@ async def to_code(configs): await generate_align_tos(configs[0]) for config in configs: lv_component = await cg.get_variable(config[CONF_ID]) + await add_animation_triggers(config.get(CONF_ANIMATIONS, [])) await generate_page_triggers(config) await initial_focus_to_code(config) for conf in config.get(CONF_ON_IDLE, ()): @@ -542,34 +563,6 @@ def add_hello_world(config): return config -@functools.cache -def _build_theme_schema( - widget_types: tuple[tuple[str, widgets.WidgetType], ...], -) -> cv.Schema: - # The theme schema is value-independent: it depends only on the set of - # registered widget types. Key the cache on a snapshot of WIDGET_TYPES so - # that an external component registering a new widget after the first - # validation (legal per any_widget_schema's lazy-evaluation contract) - # produces a fresh tuple, a cache miss, and a rebuilt schema -- the cache - # self-heals instead of stale-rejecting valid themes. See obj_dict() in - # schemas.py for why chained .extend() is avoided here. - return cv.Schema( - { - cv.Optional(df.CONF_DARK_MODE, default=False): cv.boolean, - **{ - cv.Optional(name): cv.Schema( - {**obj_dict(w), **FULL_STYLE_SCHEMA.schema} - ) - for name, w in widget_types - }, - } - ) - - -def _theme_schema(value: dict) -> dict: - return _build_theme_schema(tuple(WIDGET_TYPES.items()))(value) - - FINAL_VALIDATE_SCHEMA = final_validation # The options accepted at the top level of an `lvgl:` block, on top of the base @@ -598,6 +591,7 @@ LVGL_TOP_LEVEL_SCHEMA = ( cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font, cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, cv.Optional(df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False): cv.boolean, + cv.Optional(df.CONF_REFRESH_INTERVAL): cv.positive_time_period_milliseconds, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage, cv.Optional(CONF_ROTATION): validate_rotation, @@ -631,17 +625,19 @@ LVGL_TOP_LEVEL_SCHEMA = ( for x in SIMPLE_TRIGGERS }, cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), + cv.Optional(df.CONF_ANIMATIONS): cv.ensure_list(ANIMATION_SCHEMA), cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), cv.Optional(df.CONF_BOTTOM_LAYER): container_schema(obj_spec), cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, - cv.Optional(df.CONF_THEME): _theme_schema, + cv.Optional(df.CONF_THEME): theme_schema, cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA, cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema, cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG, cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG, cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t), cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean, + cv.Optional(df.CONF_PAUSED, default=False): cv.boolean, } ) .extend(DISP_BG_SCHEMA) diff --git a/esphome/components/lvgl/animation.h b/esphome/components/lvgl/animation.h new file mode 100644 index 0000000000..1e0abce358 --- /dev/null +++ b/esphome/components/lvgl/animation.h @@ -0,0 +1,197 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifdef USE_LVGL_ANIMATION +#include "lvgl_esphome.h" +#include "esphome/core/hal.h" + +namespace esphome::lvgl { + +enum class AnimationState { + STOPPED, + STARTED, + RUNNING, +}; + +class LvAnimationTiming { + public: + // Map progress in the range [0, 1] + virtual float map_progress(float value) = 0; +}; + +class LvAnimationTimingRoundTrip : public LvAnimationTiming { + public: + float map_progress(float value) override { + value *= 2.0f; + if (value > 1.0f) + return 2.0f - value; + return value; + } +}; + +class LvAnimationTimingGravity : public LvAnimationTiming { + public: + LvAnimationTimingGravity(float acceleration, float bounce) : acceleration_(acceleration), bounce_(bounce) {} + float map_progress(float value) override { + if (value == 0.0f) { + this->initial_position_ = 0.0f; + this->initial_speed_ = 0.0f; + this->initial_time_ = 0.0f; + } + auto position = this->calc_pos_(value); + if (position > 1.0f) { + auto initial_time = this->calc_end_time_(); + this->initial_speed_ = -this->calc_speed_(initial_time) * this->bounce_; + this->initial_position_ = 1.0f; + this->initial_time_ = initial_time; + position = calc_pos_(value); + if (position > 1.0f) { + position = 1.0f; + } + } + return position; + } + + protected: + float calc_pos_(float value) const { + value -= this->initial_time_; + return (0.5 * value * this->acceleration_ + this->initial_speed_) * value + this->initial_position_; + } + + float calc_speed_(float value) const { + value -= this->initial_time_; + return this->acceleration_ * value + this->initial_speed_; + } + + float calc_end_time_() const { + return (-this->initial_speed_ + std::sqrt(this->initial_speed_ * this->initial_speed_ - + 4.0f * this->acceleration_ / 2.0 * (this->initial_position_ - 1.0f))) / + this->acceleration_ + + this->initial_time_; + } + + float acceleration_; + float bounce_; + float initial_position_{0.0f}; + float initial_time_{0.0f}; + float initial_speed_{0.0f}; +}; + +class LvAnimationTimingEaseInOut : public LvAnimationTiming { + public: + LvAnimationTimingEaseInOut(float slope) : slope_(slope) {} + float map_progress(float value) override { + float sqr = value * value; + sqr = sqr / (2.0f * (sqr - value) + 1.0f); + return this->slope_ * sqr + (1.0 - this->slope_) * value; + } + + protected: + float slope_; +}; + +template class LvAnimation : public Component { + public: + LvAnimation(void (*update_callback)(const lv_coord_t *data), std::vector> from, + std::vector> to) + : update_callback_(update_callback) { + std::copy(from.begin(), from.end(), this->from_); + std::copy(to.begin(), to.end(), this->to_); + } + + void start() { + if (this->state_ > AnimationState::STOPPED) + this->stop(); + if (this->duration_ == 0) + return; + // evaluate any lambdas + for (size_t i = 0; i != DATA_SIZE; i++) { + this->data_from_[i] = this->from_[i].value(); + this->data_to_[i] = this->to_[i].value(); + } + this->start_time_ = millis(); + this->state_ = AnimationState::STARTED; + this->loop(); + this->start_callback_.call(); + } + + void stop() { + // Only fire the stop callback on a genuine running -> stopped transition, so that + // repeated stop() calls (e.g. start() pre-clearing a stopped animation) don't re-fire it. + if (this->state_ == AnimationState::STOPPED) + return; + this->state_ = AnimationState::STOPPED; + this->stop_callback_.call(); + } + + void setup() override { + if constexpr (AUTO_START) + this->start(); + } + + void loop() override { + if (this->state_ == AnimationState::STOPPED) + return; + uint32_t elapsed = millis() - this->start_time_; + float progress = static_cast(elapsed) / static_cast(this->duration_); + switch (this->state_) { + case AnimationState::STARTED: + if (elapsed < this->start_delay_) + return; + this->state_ = AnimationState::RUNNING; + this->start_time_ = millis(); + progress = 0.0f; + break; + case AnimationState::RUNNING: + if (progress >= 1.0f) { + progress = 1.0f; + this->stop(); + if (this->loop_) + this->start(); + } + break; + default: + return; + } + + for (auto *timing : this->timings_) { + progress = timing->map_progress(progress); + } + lv_coord_t data[DATA_SIZE]; + for (size_t i = 0; i != DATA_SIZE; i++) { + data[i] = static_cast( + roundf(this->data_from_[i] + static_cast(this->data_to_[i] - this->data_from_[i]) * progress)); + } + this->update_callback_(data); + } + + float get_setup_priority() const override { return setup_priority::PROCESSOR - 20.0; } + void set_duration(uint32_t duration) { this->duration_ = duration; } + void set_start_delay(uint32_t start_delay) { this->start_delay_ = start_delay; } + void add_timing(LvAnimationTiming *timing) { this->timings_.push_back(timing); } + void set_loop(bool loop) { this->loop_ = loop; } + + template void add_on_start_callback(F &&callback) { + this->start_callback_.add(std::forward(callback)); + } + template void add_on_stop_callback(F &&callback) { this->stop_callback_.add(std::forward(callback)); } + + protected: + void (*const update_callback_)(const lv_coord_t *data); + LazyCallbackManager start_callback_{}; + LazyCallbackManager stop_callback_{}; + TemplatableValue from_[DATA_SIZE]{}; + TemplatableValue to_[DATA_SIZE]{}; + uint32_t duration_{0}; + uint32_t start_delay_{0}; + uint32_t start_time_{0}; + lv_coord_t data_from_[DATA_SIZE]{0}; + lv_coord_t data_to_[DATA_SIZE]{0}; + AnimationState state_{AnimationState::STOPPED}; + std::vector timings_{}; + bool loop_{false}; +}; + +} // namespace esphome::lvgl + +#endif // USE_LVGL_ANIMATION diff --git a/esphome/components/lvgl/animation.py b/esphome/components/lvgl/animation.py new file mode 100644 index 0000000000..2b1500f2c4 --- /dev/null +++ b/esphome/components/lvgl/animation.py @@ -0,0 +1,295 @@ +from esphome import automation, codegen as cg, config_validation as cv +from esphome.automation import Trigger, build_automation +from esphome.config_validation import COMPONENT_SCHEMA +from esphome.const import ( + CONF_ACCELERATION, + CONF_DURATION, + CONF_FROM, + CONF_ID, + CONF_ON_START, + CONF_TIMING, + CONF_TO, + CONF_TRIGGER_ID, + CONF_TYPE, + CONF_WEIGHT, +) +from esphome.cpp_generator import MockObj, TemplateArguments + +from ..const import CONF_LOOP +from .defines import ( + CONF_AUTO_START, + CONF_LVGL_ID, + CONF_ON_STOP, + CONF_WIDGETS, + LValidator, + add_define, + literal, +) +from .lv_validation import ( + color, + get_component_colors, + lv_color, + lv_milliseconds, + lv_positive_float, + lv_zero_to_one_float, +) +from .lvcode import LVGL_COMP_ARG, LambdaContext, LvglComponent, lv_add +from .schemas import STYLE_PROPS +from .types import LvAnimation, LvglAction, lv_color_t, lv_coord_t, lv_obj_t, lvgl_ns +from .widgets import get_widgets + +LvAnimationTimingRoundTrip = lvgl_ns.class_("LvAnimationTimingRoundTrip") +LvAnimationTimingEaseInOut = lvgl_ns.class_("LvAnimationTimingEaseInOut") + +CONF_BOUNCE = "bounce" + + +def timing_class(name, extras=None): + # Convert config option to camel case + cls_name = "LvAnimationTiming" + "".join([w.capitalize() for w in name.split("_")]) + cls = lvgl_ns.class_(cls_name) + schema = cv.Schema({cv.GenerateID(): cv.declare_id(cls)}) + if extras: + schema = schema.extend(extras) + return name, schema + + +# TODO - currently the order of arguments to timing classes is expected to be alphabetical, but this is not enforced. +# It would be better to have a more robust way of passing arguments to the timing classes. +TIMING_SCHEMA = cv.maybe_simple_value( + cv.typed_schema( + dict( + [ + timing_class("round_trip"), + timing_class( + "ease_in_out", + {cv.Optional(CONF_WEIGHT, default=2.0): lv_positive_float}, + ), + timing_class( + "gravity", + { + cv.Optional(CONF_ACCELERATION, default=0.5): lv_positive_float, + cv.Optional(CONF_BOUNCE, default=0.5): lv_zero_to_one_float, + }, + ), + ] + ), + default_type="ease_in_out", + ), + key=CONF_TYPE, +) + +CONF_START_DELAY = "start_delay" + + +class LiteralColorValidator(LValidator): + def __init__(self): + super().__init__( + color, lv_color_t, retmapper=get_component_colors, animatable=True + ) + + def __call__(self, value): + if isinstance(value, cv.Lambda): + raise cv.Invalid( + "An animated color may not be set with a lambda, only a literal color value." + ) + return super().__call__(value) + + +literal_color = LiteralColorValidator() + + +def from_to(validator): + return cv.Schema( + { + cv.Required(CONF_FROM): validator, + cv.Required(CONF_TO): validator, + } + ) + + +# Colors can only be animated between constants, not lambdas. +def map_v(validator): + if validator == lv_color: + return literal_color + return validator + + +ANIMABLE_STYLES = { + k: map_v(v) + for k, v in STYLE_PROPS.items() + if isinstance(v, LValidator) and v.animatable +} + +ANIMATION_SCHEMA = cv.Schema( + { + cv.Optional(CONF_AUTO_START, default=False): cv.boolean, + cv.Optional(CONF_LOOP, default=False): cv.boolean, + cv.Optional(CONF_DURATION, default="5s"): lv_milliseconds, + cv.Optional(CONF_START_DELAY, default="0s"): lv_milliseconds, + cv.Optional(CONF_TIMING, default=[]): cv.ensure_list(TIMING_SCHEMA), + cv.Required(CONF_ID): cv.declare_id(LvAnimation), + cv.Optional(CONF_ON_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()), + } + ), + cv.Optional(CONF_ON_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()), + } + ), + cv.Required(CONF_WIDGETS): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_obj_t), + } + ).extend({cv.Optional(k): from_to(v) for k, v in ANIMABLE_STYLES.items()}) + ), + } +).extend(COMPONENT_SCHEMA) + + +async def _process_arg(validator, arg) -> list: + # from/to values are evaluated at animation start with no arguments, so the + # generated lambda must be parameterless rather than inheriting the enclosing + # update-callback's `values` parameter. + value = await validator.process(arg, args=[], raw_lambda=True) + value = list(value) if isinstance(value, tuple) else [value] + return [literal(f"TemplatableValue({v})") for v in value] + + +async def animations_to_code(config): + for animation in config: + add_define("USE_LVGL_ANIMATION") + widgets = animation[CONF_WIDGETS] + async with LambdaContext( + [(lv_coord_t.operator("const").operator("ptr"), "values")] + ) as ctx: + froms = [] + tos = [] + for widget in widgets: + w = (await get_widgets(widget))[0] + props = [(k, v) for k, v in widget.items() if k in ANIMABLE_STYLES] + for prop, value_range in props: + # prop is the style property, value_range is a dict with from: and to: values + validator = ANIMABLE_STYLES[prop] + from_value = await _process_arg(validator, value_range[CONF_FROM]) + to_value = await _process_arg(validator, value_range[CONF_TO]) + index = len(froms) + if len(from_value) == 1: + value = f"values[{index}]" + else: + value = f"lv_color_make(values[{index}+0], values[{index}+1], values[{index}+2])" + w.set_style(prop, literal(value), 0) + # The value arrays are extended by 1 item for scalar properties, 3 for colors + froms.extend(from_value) + tos.extend(to_value) + + data_size = len(froms) + loop = animation[CONF_LOOP] + start_delay = await lv_milliseconds.process(animation.get(CONF_START_DELAY)) + var = cg.new_Pvariable( + animation[CONF_ID], + TemplateArguments(data_size, animation[CONF_AUTO_START]), + await ctx.get_lambda(), + froms, + tos, + ) + for timing in animation[CONF_TIMING]: + timing_id = timing[CONF_ID] + args = sorted( + [(k, v) for k, v in timing.items() if k not in [CONF_ID, CONF_TYPE]] + ) + args = [v for k, v in args] + timing_var = cg.new_Pvariable(timing_id, *args) + cg.add(var.add_timing(timing_var)) + + if start_delay: + cg.add(var.set_start_delay(start_delay)) + if loop: + cg.add(var.set_loop(loop)) + cg.add( + var.set_duration(await lv_milliseconds.process(animation[CONF_DURATION])) + ) + await cg.register_component(var, animation) + + +async def add_animation_triggers(config): + async def add_triggers(animation: MockObj, event: str, config: dict) -> None: + for conf in config: + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + await build_automation(trigger, [], conf) + async with LambdaContext([]) as context: + lv_add(trigger.trigger()) + lv_add( + getattr( + animation, + f"add_{event}_callback", + )(await context.get_lambda()) + ) + + for animation in config: + var = await cg.get_variable(animation[CONF_ID]) + await add_triggers(var, CONF_ON_START, animation.get(CONF_ON_START, [])) + await add_triggers(var, CONF_ON_STOP, animation.get(CONF_ON_STOP, [])) + + +@automation.register_action( + "lvgl.animation.start", + LvglAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.ensure_list(cv.use_id(LvAnimation)), + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + cv.Optional(CONF_DURATION): lv_milliseconds, + cv.Optional(CONF_START_DELAY): lv_milliseconds, + cv.Optional(CONF_LOOP): cv.boolean, + }, + key=CONF_ID, + ), + synchronous=True, +) +async def start_animation(config, action_id, template_arg, args): + animations = config[CONF_ID] + loop = config.get(CONF_LOOP) + async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: + for animation in animations: + anim_var = await cg.get_variable(animation) + if loop is not None: + context.add(anim_var.set_loop(loop)) + if (duration := config.get(CONF_DURATION)) is not None: + context.add( + anim_var.set_duration(await lv_milliseconds.process(duration)) + ) + if (start_delay := config.get(CONF_START_DELAY)) is not None: + context.add( + anim_var.set_start_delay(await lv_milliseconds.process(start_delay)) + ) + context.add(anim_var.start()) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +@automation.register_action( + "lvgl.animation.stop", + LvglAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.ensure_list(cv.use_id(LvAnimation)), + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + }, + key=CONF_ID, + ), + synchronous=True, +) +async def stop_animation(config, action_id, template_arg, args): + animations = config[CONF_ID] + async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: + for animation in animations: + anim_var = await cg.get_variable(animation) + context.add(anim_var.stop()) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index bf9a3d74ad..b7c90a5c51 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -4,7 +4,6 @@ from typing import Any from esphome import automation from esphome.automation import StatelessLambdaAction import esphome.codegen as cg -from esphome.components.display import validate_rotation import esphome.config_validation as cv from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_ROTATION, CONF_TIMEOUT from esphome.core import Lambda @@ -16,6 +15,7 @@ from .defines import ( CONF_BOTTOM_LAYER, CONF_EDITING, CONF_FREEZE, + CONF_LAYOUT, CONF_LVGL_ID, CONF_MAIN, CONF_OBJ, @@ -29,7 +29,8 @@ from .defines import ( get_options, get_refreshed_widgets, ) -from .lv_validation import lv_bool, lv_milliseconds +from .layout import layout_validator +from .lv_validation import lv_bool, lv_milliseconds, lv_rotation from .lvcode import ( LVGL_COMP_ARG, UPDATE_EVENT, @@ -199,7 +200,7 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): def _validate_rotation(value): # Note that we need rotation get_options()[CONF_ROTATION] = True - return validate_rotation(value) + return lv_rotation(value) @automation.register_action( @@ -218,7 +219,8 @@ def _validate_rotation(value): async def lvgl_set_rotation(config, action_id, template_arg, args): lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) async with LambdaContext(args, where=action_id) as context: - lv_add(lv_comp.set_rotation(config[CONF_ROTATION])) + rotation = await lv_rotation.process(config[CONF_ROTATION]) + lv_add(lv_comp.set_rotation(rotation)) return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) @@ -254,6 +256,13 @@ layer_spec = WidgetType(CONF_OBJ, lv_obj_t, (CONF_MAIN, CONF_SCROLLBAR), is_mock DISP_PROPS = {str(x) for x in DISP_BG_SCHEMA.schema} +def _layer_update_schema() -> cv.Schema: + """Schema for updating a display layer's styling and layout options.""" + return part_schema(layer_spec.parts).extend( + {cv.Optional(CONF_LAYOUT): layout_validator} + ) + + @automation.register_action( "lvgl.update", LvglAction, @@ -262,8 +271,9 @@ DISP_PROPS = {str(x) for x in DISP_BG_SCHEMA.schema} .extend(DISP_BG_SCHEMA) .extend( { - cv.Optional(CONF_TOP_LAYER): part_schema(layer_spec.parts), - cv.Optional(CONF_BOTTOM_LAYER): part_schema(layer_spec.parts), + cv.Optional(CONF_LAYOUT): layout_validator, + cv.Optional(CONF_TOP_LAYER): _layer_update_schema(), + cv.Optional(CONF_BOTTOM_LAYER): _layer_update_schema(), } ), synchronous=True, @@ -272,7 +282,12 @@ async def lvgl_update_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config, CONF_LVGL_ID) w = widgets[0] async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: + # Apply the top-level properties (styles and layout) to the active screen... + await set_obj_properties(get_screen_active(w.var), config) + # ...the deprecated flat `disp_*` background properties... await lvgl_update(w.var, config) + # ...and the `top_layer`/`bottom_layer` keys (styling and layout updates). + await layers_to_code(w.var, config) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) await cg.register_parented(var, w.var) return var diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index d9be881a7f..65e975ad6d 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -33,6 +33,7 @@ KEY_NAMED_STYLES = "named_styles" KEY_REFRESHED_WIDGETS = "refreshed_widgets" KEY_REMAPPED_USES = "remapped_uses" KEY_STYLES_USED = "styles_used" +KEY_THEME_UPDATE_REQUESTS = "theme_update_requests" KEY_THEME_WIDGET_MAP = "theme_widget_map" KEY_UPDATED_WIDGETS = "updated_widgets" KEY_WIDGET_MAP = "widget_map" @@ -118,6 +119,14 @@ def get_theme_widget_map() -> dict[str, Any]: return _get_data(KEY_THEME_WIDGET_MAP, {}) +def get_theme_update_requests() -> dict[str, dict[tuple[str, str], None]]: + # Values are dicts used as ordered sets (insertion order is deterministic, + # unlike a plain `set` of strings/tuples, whose iteration order depends on + # per-process string hash randomization) so codegen output doesn't churn + # between builds of the same config. + return _get_data(KEY_THEME_UPDATE_REQUESTS, {}) + + def get_styles_used() -> set[str]: return _get_data(KEY_STYLES_USED, set()) @@ -214,11 +223,14 @@ class LValidator: has `process()` to convert a value during code generation """ - def __init__(self, validator, rtype: MockObj, retmapper=None, requires=None): + def __init__( + self, validator, rtype: MockObj, retmapper=None, requires=None, animatable=False + ): self.validator = validator self.rtype = rtype self.retmapper = retmapper self.requires = requires + self.animatable = animatable def __call__(self, value): if self.requires: @@ -228,7 +240,10 @@ class LValidator: return self.validator(value) async def process( - self, value: Any, args: list[tuple[SafeExpType, str]] | None = None + self, + value: Any, + args: list[tuple[SafeExpType, str]] | None = None, + raw_lambda: bool = False, ) -> Expression: if value is None: return None @@ -236,11 +251,15 @@ class LValidator: # Local import to avoid circular import from .lvcode import get_lambda_context_args - args = args or get_lambda_context_args() + # `args is None` means "inherit the enclosing lambda context"; an explicit + # empty list means "no parameters" and must be preserved as-is. + if args is None: + args = get_lambda_context_args() - return call_lambda( - await cg.process_lambda(value, args, return_type=self.rtype) - ) + lamb = await cg.process_lambda(value, args, return_type=self.rtype) + if raw_lambda: + return lamb + return call_lambda(lamb) if self.retmapper is not None: return self.retmapper(value) if isinstance(value, ID): @@ -729,6 +748,7 @@ CONF_GRID_ROWS = "grid_rows" CONF_HEADER_BUTTONS = "header_buttons" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" +CONF_IMAGE = "image" CONF_INDICATORS = "indicators" CONF_INITIAL_FOCUS = "initial_focus" CONF_SELECTED_DIGIT = "selected_digit" @@ -742,15 +762,19 @@ CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" CONF_LVGL_ID = "lvgl_id" CONF_LONG_MODE = "long_mode" CONF_MAJOR_TICKS_STYLE = "major_ticks_style" +CONF_MAPPING = "mapping" CONF_MSGBOXES = "msgboxes" CONF_OBJ = "obj" CONF_ONE_CHECKED = "one_checked" CONF_ONE_LINE = "one_line" CONF_ON_DRAW_START = "on_draw_start" CONF_ON_DRAW_END = "on_draw_end" +CONF_ON_LANDSCAPE = "on_landscape" CONF_ON_PAUSE = "on_pause" +CONF_ON_PORTRAIT = "on_portrait" CONF_ON_RESUME = "on_resume" CONF_ON_SELECT = "on_select" +CONF_ON_STOP = "on_stop" CONF_OPA = "opa" CONF_NEXT = "next" CONF_PAD_ROW = "pad_row" @@ -758,12 +782,14 @@ CONF_PAD_COLUMN = "pad_column" CONF_PAGE = "page" CONF_PAGE_WRAP = "page_wrap" CONF_PASSWORD_MODE = "password_mode" +CONF_PAUSED = "paused" CONF_PIVOT_X = "pivot_x" CONF_PIVOT_Y = "pivot_y" CONF_PLACEHOLDER_TEXT = "placeholder_text" CONF_POINTS = "points" CONF_PREVIOUS = "previous" CONF_RADIUS = "radius" +CONF_REFRESH_INTERVAL = "refresh_interval" CONF_REPEAT_COUNT = "repeat_count" CONF_RECOLOR = "recolor" CONF_RESUME_ON_INPUT = "resume_on_input" diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index 32304276d3..fd1f242d86 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -34,6 +34,7 @@ from .defines import ( TYPE_GRID, TYPE_NONE, LvConstant, + add_lv_use, ) from .lv_validation import padding, size @@ -401,6 +402,61 @@ LAYOUT_CLASSES = ( LAYOUT_CHOICES = [x.get_type() for x in LAYOUT_CLASSES] +# Layout properties that may be changed at runtime via an update action. These +# are limited to simple style properties (set via ``lv_obj_set_style_...``). +# Structural properties are deliberately excluded: +# - the layout ``type``, which determines which options are available to child +# widgets, and +# - the grid ``grid_rows``/``grid_columns`` descriptors, which define the cells +# that child widgets are placed into. +# Both are fixed at widget creation. +_GRID_LAYOUT_KEYS = ( + CONF_GRID_COLUMN_ALIGN, + CONF_GRID_ROW_ALIGN, +) +_FLEX_LAYOUT_KEYS = ( + CONF_FLEX_FLOW, + CONF_FLEX_ALIGN_MAIN, + CONF_FLEX_ALIGN_CROSS, + CONF_FLEX_ALIGN_TRACK, +) + +LAYOUT_UPDATE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_FLEX_FLOW): FLEX_FLOWS.one_of, + cv.Optional(CONF_FLEX_ALIGN_MAIN): flex_alignments, + cv.Optional(CONF_FLEX_ALIGN_CROSS): LV_FLEX_CROSS_ALIGNMENTS.one_of, + cv.Optional(CONF_FLEX_ALIGN_TRACK): flex_alignments, + cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments, + cv.Optional(CONF_PAD_ROW): padding, + cv.Optional(CONF_PAD_COLUMN): padding, + } +) + + +def layout_validator(value): + """ + Validate a ``layout:`` value for an update action. Only the layout options + may be changed (not the layout ``type``, which is fixed at widget creation). + :param value: The value of the ``layout:`` key + :return: The validated layout options dict + """ + result = LAYOUT_UPDATE_SCHEMA(value) + if not result: + raise cv.Invalid( + "A layout update must specify at least one layout option", [CONF_LAYOUT] + ) + # Register the relevant layout feature so its LV_USE_* define is emitted even + # when the option is set solely via an update action (whose code generation + # may run after LVGL has finished collecting its used features). + if any(key in result for key in _GRID_LAYOUT_KEYS): + add_lv_use(TYPE_GRID) + if any(key in result for key in _FLEX_LAYOUT_KEYS): + add_lv_use(TYPE_FLEX) + return result + + def append_layout_schema(schema, config: dict): """ Get the child layout schema for a given widget based on its layout type. diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 27cbfff694..42352b9602 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -22,9 +22,12 @@ from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import Expression, SafeExpType +from ..mapping import INDEX_TYPES, get_mapping_metadata from . import types as ty from .defines import ( CONF_END_VALUE, + CONF_IMAGE, + CONF_MAPPING, CONF_START_VALUE, CONF_TIME_FORMAT, LV_FONTS, @@ -60,6 +63,7 @@ opacity = LValidator( opacity_validator, lv_opa_t, retmapper=lambda opa: StaticCastExpression(cg.uint8, opa * 255.0), + animatable=True, ) COLOR_NAMES = { @@ -223,35 +227,33 @@ def color(value): ) -def color_retmapper(value): - if isinstance(value, cv.Lambda): - return cv.returning_lambda(value) +def get_component_colors(value): if isinstance(value, str) and value in COLOR_NAMES: value = COLOR_NAMES[value] if isinstance(value, int): - return literal( - f"lv_color_make({(value >> 16) & 0xFF}, {(value >> 8) & 0xFF}, {value & 0xFF})" - ) + return value >> 16, value >> 8 & 0xFF, value & 0xFF if isinstance(value, ID): cval = [x for x in CORE.config[CONF_COLOR] if x[CONF_ID] == value][0] if CONF_HEX in cval: r, g, b = cval[CONF_HEX] else: r, g, b, _ = from_rgbw(cval) - return literal(f"lv_color_make({r}, {g}, {b})") + return r, g, b raise AssertionError(f"Unhandled lv_color value: {value!r}") -def option_string(value): - value = cv.string(value).strip() - if value.find("\n") != -1: - raise cv.Invalid("Options strings must not contain newlines") - return value +def color_retmapper(value): + if isinstance(value, cv.Lambda): + return cv.returning_lambda(value) + r, g, b = get_component_colors(value) + return literal(f"lv_color_make({r}, {g}, {b})") class LvColor(LValidator): def __init__(self): - super().__init__(color, ty.lv_color_t, retmapper=color_retmapper) + super().__init__( + color, ty.lv_color_t, retmapper=color_retmapper, animatable=True + ) def __getattr__(self, item): if item in COLOR_NAMES: @@ -262,6 +264,13 @@ class LvColor(LValidator): lv_color = LvColor() +def option_string(value): + value = cv.string(value).strip() + if value.find("\n") != -1: + raise cv.Invalid("Options strings must not contain newlines") + return value + + def pixels_or_percent_validator(value): """A length in one axis - either a number (pixels) or a percentage""" if value == SCHEMA_EXTRACT: @@ -277,6 +286,7 @@ pixels_or_percent = LValidator( pixels_or_percent_validator, lv_coord_t, retmapper=lambda x: x if isinstance(x, int) else literal(f"lv_pct({int(x * 100)})"), + animatable=True, ) @@ -315,10 +325,23 @@ def angle(value): # Validator for angles in LVGL expressed in 1/10 degree units. -lv_angle = LValidator(angle, uint32, retmapper=lambda x: int(x * 10)) +lv_angle = LValidator(angle, uint32, retmapper=lambda x: int(x * 10), animatable=True) # Validator for angles in LVGL expressed in whole degrees -lv_angle_degrees = LValidator(angle, uint32, retmapper=int) +lv_angle_degrees = LValidator(angle, uint32, retmapper=int, animatable=True) + + +def rotation_degrees(value): + """Validate a display rotation, returning the angle in whole degrees. + + Accepts the four supported rotations, optionally suffixed with "°". + """ + value = cv.string(value).removesuffix("°") + return cv.one_of(0, 90, 180, 270, int=True)(value) + + +# Validator for a display rotation expressed in whole degrees (templatable) +lv_rotation = LValidator(rotation_degrees, cg.int_) @schema_extractor("one_of") @@ -368,21 +391,57 @@ def stop_value(value): return cv.int_range(0, 255)(value) -def image_validator(value): - value = cv.requires_component("image")(value) +def _image_validator(value): + if isinstance(value, dict) and CONF_MAPPING in value: + from .schemas import MAPPING_IMAGE_SCHEMA + + return MAPPING_IMAGE_SCHEMA(value) value = cv.use_id(Image_)(value) get_lv_images_used().add(value) add_lv_use("label") return value -lv_image = LValidator( - image_validator, - image.Image_.operator("ptr"), - requires="image", -) +class ImageValidator(LValidator): + def __init__(self): + super().__init__( + validator=_image_validator, + rtype=image.Image_.operator("ptr"), + requires=CONF_IMAGE, + ) + + async def process( + self, + value: Any, + args: list[tuple[SafeExpType, str]] | None = None, + raw_lambda: bool = False, + ) -> Expression: + # Local import to avoid circular import at module level + from .lvcode import get_lambda_context_args + + args = args or get_lambda_context_args() + if isinstance(value, dict) and CONF_MAPPING in value: + mapping_id = value[CONF_MAPPING] + mapping_var = await cg.get_variable(mapping_id) + metadata = get_mapping_metadata(mapping_id.id) + index = value[CONF_VALUE] + if isinstance(index, Lambda): + index = call_lambda( + await cg.process_lambda( + index, args, return_type=metadata.from_.data_type + ) + ) + else: + index = await metadata.from_.convert_value(index) + return mapping_var.get(index) + + return await super().process(value, args, raw_lambda) + + +lv_image = ImageValidator() + lv_image_list = LValidator( - cv.ensure_list(image_validator), + cv.ensure_list(_image_validator), cg.std_vector.template(image.Image_.operator("ptr")), requires="image", ) @@ -410,7 +469,10 @@ class TextValidator(LValidator): return super().__call__(value) async def process( - self, value: Any, args: list[tuple[SafeExpType, str]] | None = None + self, + value: Any, + args: list[tuple[SafeExpType, str]] | None = None, + raw_lambda: bool = False, ) -> Expression: # Local import to avoid circular import at module level from .lvcode import get_lambda_context_args @@ -430,6 +492,24 @@ class TextValidator(LValidator): f"(std::isfinite({arg_expr}) ? {sprintf_str} : {nanval})" ) return literal(sprintf_str) + if mapping_id := value.get(CONF_MAPPING): + mapping_var = await cg.get_variable(mapping_id) + metadata = get_mapping_metadata(mapping_id.id) + if metadata.to_ != INDEX_TYPES["string"]: + raise ValueError( + f"Mapping {mapping_id} does not map to strings, cannot use in text" + ) + index = value[CONF_VALUE] + if isinstance(index, Lambda): + index = call_lambda( + await cg.process_lambda( + index, args, return_type=metadata.from_.data_type + ) + ) + else: + index = await metadata.from_.convert_value(index) + return mapping_var.get(index).c_str() + if time_format := value.get(CONF_TIME_FORMAT): source = value[CONF_TIME] if isinstance(source, Lambda): @@ -455,13 +535,18 @@ class TextValidator(LValidator): return value # Either a std::string or a lambda call returning that. We need const char* return MockObj(f"({value}).c_str()") - return await super().process(value, args) + return await super().process(value, args, raw_lambda) lv_text = TextValidator() lv_float = LValidator(cv.float_, cg.float_) -lv_int = LValidator(cv.int_, cg.int_) -lv_positive_int = LValidator(cv.positive_int, cg.int_) +lv_positive_float = LValidator(cv.positive_float, cg.float_) +lv_zero_to_one_float = LValidator(cv.zero_to_one_float, cg.float_) +lv_int = LValidator(cv.int_, cg.int_, animatable=True) +lv_positive_int = LValidator(cv.positive_int, cg.int_, animatable=True) +lv_brightness = LValidator( + cv.percentage, cg.float_, retmapper=lambda x: int(x * 255), animatable=True +) def _percentage_validator(value): @@ -508,12 +593,17 @@ class LvFont(LValidator): # The inline overloads in lvgl_esphome.h handle conversion to lv_font_t* super().__init__(validator, Font.operator("ptr")) - async def process(self, value, args=()): + async def process( + self, + value: Any, + args: list[tuple[SafeExpType, str]] | None = None, + raw_lambda: bool = False, + ): if is_lv_font(value): return literal(f"&lv_font_{value}") if isinstance(value, str): return literal(f"{value}") - return await super().process(value, args) + return await super().process(value, args, raw_lambda) lv_font = LvFont() diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 12bf6d9f37..b66a904437 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -91,11 +91,24 @@ void LvglComponent::set_rotation(display::DisplayRotation rotation) { this->rotation_ = rotation; if (this->is_ready()) { this->set_resolution_(); + this->update_orientation_(); lv_obj_update_layout(this->get_screen_active()); lv_obj_invalidate(this->get_screen_active()); } } +void LvglComponent::set_rotation(int angle) { + // Normalize to [0, 360). The DisplayRotation enum values are the angles in degrees. + angle %= 360; + if (angle < 0) + angle += 360; + if (angle % 90 != 0) { + ESP_LOGW(TAG, "Invalid rotation angle %d; must be a multiple of 90 degrees.", angle); + return; + } + this->set_rotation(static_cast(angle)); +} + void LvglComponent::rotate_coordinates(int32_t &x, int32_t &y) const { switch (this->rotation_) { default: @@ -401,7 +414,10 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { } void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) { - if (!this->is_paused()) { + // no guard here for display busy, since LVGL will not call flush_cb until the refresh timer fires, + // and while the display is busy this is reset to 5 minutes. If that expires and the display is still + // busy there are bigger problems. + if (!this->paused_) { auto now = millis(); this->draw_buffer_(area, reinterpret_cast(color_p)); ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1, @@ -620,20 +636,20 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { void LvglComponent::draw_end_() { if (this->draw_end_callback_ != nullptr) this->draw_end_callback_->trigger(); + // Only reachable once the display is idle again: while busy, the display's refr_timer_ is + // paused (see loop()), so LVGL never renders/flushes and this event never fires. if (this->update_when_display_idle_) { for (auto *disp : this->displays_) disp->update(); } } -bool LvglComponent::is_paused() const { - if (this->paused_) - return true; - if (this->update_when_display_idle_) { - for (auto *disp : this->displays_) { - if (!disp->is_idle()) - return true; - } +bool LvglComponent::displays_busy_() const { + if (!this->update_when_display_idle_) + return false; + for (auto *disp : this->displays_) { + if (!disp->is_idle()) + return true; } return false; } @@ -716,6 +732,18 @@ void LvglComponent::set_resolution_() const { } lv_display_set_resolution(this->disp_, width, height); } + +void LvglComponent::update_orientation_() { + // A square display is treated as landscape. + auto orientation = this->get_width() >= this->get_height() ? Orientation::LANDSCAPE : Orientation::PORTRAIT; + if (orientation == this->orientation_) + return; + this->orientation_ = orientation; + auto *trigger = orientation == Orientation::LANDSCAPE ? this->landscape_callback_ : this->portrait_callback_; + if (trigger != nullptr) + trigger->trigger(); +} + void LvglComponent::setup() { auto *display = this->displays_[0]; auto rounding = this->draw_rounding; @@ -754,7 +782,7 @@ void LvglComponent::setup() { lv_display_add_event_cb(this->disp_, rounder_cb, LV_EVENT_INVALIDATE_AREA, this); lv_display_set_buffers(this->disp_, this->draw_buf_, nullptr, buf_bytes, this->full_refresh_ ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL); - if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { + if (this->rotation_type_ == ROTATION_SOFTWARE) { this->rotate_buf_ = static_cast(lv_alloc_draw_buf(buf_bytes, false)); // NOLINT if (this->rotate_buf_ == nullptr) { this->status_set_error(LOG_STR("Memory allocation failure")); @@ -777,6 +805,8 @@ void LvglComponent::setup() { if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) { lv_display_add_event_cb(this->disp_, render_end_cb, LV_EVENT_REFR_READY, this); } + this->refr_timer_ = lv_display_get_refr_timer(this->disp_); + lv_timer_set_period(this->refr_timer_, this->refr_timer_period_); #if LV_USE_LOG lv_log_register_print_cb([](lv_log_level_t level, const char *buf) { auto next = strchr(buf, ')'); @@ -791,6 +821,7 @@ void LvglComponent::setup() { #endif this->show_page(0, LV_SCREEN_LOAD_ANIM_NONE, 0); lv_display_trigger_activity(this->disp_); + this->update_orientation_(); } void LvglComponent::update() { @@ -802,21 +833,32 @@ void LvglComponent::update() { } void LvglComponent::loop() { - if (this->is_paused()) { - if (this->paused_ && this->show_snow_) + if (this->paused_) { + if (this->show_snow_) this->write_random_(); - } else { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - auto now = millis(); - lv_timer_handler(); - auto elapsed = millis() - now; - if (elapsed > 15) { - ESP_LOGV(TAG, "lv_timer_handler took %dms", (int) (millis() - now)); - } -#else - lv_timer_handler(); -#endif + return; } + // Pause/resume the display's own refresh timer to track its busy state. While paused, LVGL + // still keeps track of invalidated areas but won't render or flush them, so nothing needs to + // be discarded or replayed: once resumed, the accumulated areas are simply drawn as normal. + // Input events and other timers keep being processed below regardless of this state. + if (this->update_when_display_idle_) { + bool busy = this->displays_busy_(); + if (busy && !this->refr_timer_paused_) { + this->refr_timer_paused_ = true; + // calling lv_timer_pause() here would be ineffective; LVGL pauses and resumes the timer based on its own internal + // state, which is not aware of the display's busy state. Instead, we extend the timer period to avoid it firing + // while the display is busy. + lv_timer_set_period(this->refr_timer_, 5 * 60 * 1000); + } else if (!busy && this->refr_timer_paused_) { + this->refr_timer_paused_ = false; + lv_timer_set_period(this->refr_timer_, this->refr_timer_period_); + // Don't wait for the timer's next natural period: refresh right away now that the + // display is idle again. + lv_timer_ready(this->refr_timer_); + } + } + lv_timer_handler(); } #ifdef USE_LVGL_ANIMIMG @@ -929,7 +971,7 @@ void lv_mem_init() {} void lv_mem_deinit() {} -#if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266) +#if defined(USE_HOST) || defined(USE_RP2) || defined(USE_ESP8266) void *lv_malloc_core(size_t size) { auto *ptr = malloc(size); // NOLINT if (ptr == nullptr) { diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 8840b0ad30..9221ab9542 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -185,6 +185,12 @@ enum RotationType : uint8_t { ROTATION_HARDWARE, }; +enum class Orientation : uint8_t { + UNKNOWN, + LANDSCAPE, + PORTRAIT, +}; + class LvglComponent final : public PollingComponent { constexpr static const char *const TAG = "lvgl"; @@ -214,9 +220,14 @@ class LvglComponent final : public PollingComponent { // @param paused If true, pause the display. If false, resume the display. // @param show_snow If true, show the snow effect when paused. void set_paused(bool paused, bool show_snow); + void set_refresh_interval(uint32_t period) { + this->refr_timer_period_ = period; + if (this->refr_timer_ != nullptr) + lv_timer_set_period(this->refr_timer_, period); + } - // Returns true if the display is explicitly paused, or a blocking display update is in progress. - bool is_paused() const; + // Returns true if the display has been explicitly paused via set_paused(). + bool is_paused() const { return this->paused_; } // If the display is paused and we have resume_on_input_ set to true, resume the display. void maybe_wakeup() { if (this->paused_ && this->resume_on_input_) { @@ -286,7 +297,11 @@ class LvglComponent final : public PollingComponent { void set_resume_trigger(Trigger<> *trigger) { this->resume_callback_ = trigger; } void set_draw_start_trigger(Trigger<> *trigger) { this->draw_start_callback_ = trigger; } void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; } + void set_landscape_trigger(Trigger<> *trigger) { this->landscape_callback_ = trigger; } + void set_portrait_trigger(Trigger<> *trigger) { this->portrait_callback_ = trigger; } void set_rotation(display::DisplayRotation rotation); + /// Set the rotation from an angle in degrees. Must be a multiple of 90. + void set_rotation(int angle); display::DisplayRotation get_rotation() const { return this->rotation_; } void rotate_coordinates(int32_t &x, int32_t &y) const; @@ -295,10 +310,16 @@ class LvglComponent final : public PollingComponent { protected: void set_resolution_() const; + // Determine the current orientation from the effective resolution and fire the + // landscape/portrait trigger if it has changed since the last check. + void update_orientation_(); void draw_end_(); // Not checking for non-null callback since the // LVGL callback that calls it is not set in that case void draw_start_() const { this->draw_start_callback_->trigger(); } + // Returns true if update_when_display_idle is enabled and at least one underlying display + // component is currently busy (e.g. mid-refresh). + bool displays_busy_() const; void write_random_(); void draw_buffer_(const lv_area_t *area, lv_color_data *ptr); @@ -316,6 +337,14 @@ class LvglComponent final : public PollingComponent { uint8_t *draw_buf_{}; lv_display_t *disp_{}; + // The display's own periodic refresh timer, effectively paused while the display is busy (see + // displays_busy_()) so LVGL neither renders nor flushes to it, without losing track of + // invalidated areas. Other timers (indev reading, animations, ...) keep running as normal. + lv_timer_t *refr_timer_{}; + // Tracks whether refr_timer_ is currently paused, so loop() can detect the busy -> idle edge + // and kick off an immediate refresh instead of waiting for the timer's next natural period. + bool refr_timer_paused_{}; + uint32_t refr_timer_period_{16}; uint16_t width_{}; uint16_t height_{}; bool paused_{}; @@ -331,6 +360,9 @@ class LvglComponent final : public PollingComponent { Trigger<> *resume_callback_{}; Trigger<> *draw_start_callback_{}; Trigger<> *draw_end_callback_{}; + Trigger<> *landscape_callback_{}; + Trigger<> *portrait_callback_{}; + Orientation orientation_{Orientation::UNKNOWN}; void *rotate_buf_{}; display::DisplayRotation rotation_{display::DISPLAY_ROTATION_0_DEGREES}; RotationType rotation_type_; diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index d7df628907..e400dae50f 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,12 +1,15 @@ from collections.abc import Callable +import functools from typing import Any from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation +from esphome.components.mapping import mapping_class from esphome.components.time import RealTimeClock from esphome.config_validation import prepend_path from esphome.const import ( CONF_ARGS, + CONF_DEFAULT, CONF_FORMAT, CONF_GROUP, CONF_ID, @@ -17,6 +20,7 @@ from esphome.const import ( CONF_TEXT, CONF_TIME, CONF_TRIGGER_ID, + CONF_VALUE, CONF_X, CONF_Y, ) @@ -31,6 +35,7 @@ from esphome.schema_extractors import ( from . import defines as df, lv_validation as lvalid from .defines import ( CONF_EXT_CLICK_AREA, + CONF_MAPPING, CONF_SCROLL_DIR, CONF_SCROLL_SNAP_X, CONF_SCROLL_SNAP_Y, @@ -52,6 +57,7 @@ from .layout import ( GRID_CELL_SCHEMA, append_layout_schema, grid_alignments, + layout_validator, ) from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity from .lvcode import UPDATE_EVENT, LvglComponent, lv_event_t_ptr @@ -65,7 +71,7 @@ from .types import ( lv_pseudo_button_t, lv_style_t, ) -from .widgets import WidgetType +from .widgets import WidgetType, collect_parts # this will be populated later, in __init__.py to avoid circular imports. WIDGET_TYPES: dict = {} @@ -89,6 +95,20 @@ PRINTF_TEXT_SCHEMA = cv.All( validate_printf, ) +MAPPING_TEXT_SCHEMA = cv.Schema( + { + cv.Required(CONF_MAPPING): cv.use_id(mapping_class), + cv.Required(CONF_VALUE): cv.templatable(cv.string), + } +) + +MAPPING_IMAGE_SCHEMA = cv.Schema( + { + cv.Required(CONF_MAPPING): cv.use_id(mapping_class), + cv.Required(CONF_VALUE): cv.templatable(cv.string), + } +) + def _validate_text(value): """ @@ -100,6 +120,8 @@ def _validate_text(value): if isinstance(value, dict): if CONF_TIME_FORMAT in value: return TIME_TEXT_SCHEMA(value) + if CONF_MAPPING in value: + return MAPPING_TEXT_SCHEMA(value) return PRINTF_TEXT_SCHEMA(value) return cv.templatable(cv.string)(value) @@ -504,6 +526,7 @@ def base_update_schema(widget_type: WidgetType | LvType, parts): ) ), cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + cv.Optional(df.CONF_LAYOUT): layout_validator, } ) @@ -570,6 +593,96 @@ def obj_schema(widget_type: WidgetType) -> cv.Schema: return schema +@functools.cache +def _build_theme_schema( + widget_types: tuple[tuple[str, WidgetType], ...], + include_dark_mode: bool = True, +) -> cv.Schema: + # The theme schema is value-independent: it depends only on the set of + # registered widget types. Key the cache on a snapshot of WIDGET_TYPES so + # that an external component registering a new widget after the first + # validation (legal per any_widget_schema's lazy-evaluation contract) + # produces a fresh tuple, a cache miss, and a rebuilt schema -- the cache + # self-heals instead of stale-rejecting valid themes. See obj_dict() above + # for why chained .extend() is avoided here. + return cv.Schema( + { + **( + {cv.Optional(df.CONF_DARK_MODE, default=False): cv.boolean} + if include_dark_mode + else {} + ), + **{ + cv.Optional(name): cv.Schema( + {**obj_dict(w), **FULL_STYLE_SCHEMA.schema} + ) + for name, w in widget_types + }, + } + ) + + +def _reject_theme_styles_key(validated: dict) -> dict: + """ + `styles:` (a list of already-declared named styles) is not allowed inside + `theme:` -- the hidden style objects theme: creates only carry direct + style properties, so a `styles:` reference there would be silently + dropped by `style_set`, which only walks `ALL_STYLES`. + """ + for w_name, style in validated.items(): + if w_name not in WIDGET_TYPES: + continue + for part, states in collect_parts(style).items(): + for state, props in states.items(): + if df.CONF_STYLES not in props: + continue + path = [w_name] + if part != df.CONF_MAIN: + path.append(part) + if state != CONF_DEFAULT: + path.append(state) + path.append(df.CONF_STYLES) + raise cv.Invalid( + "'styles:' is not allowed in LVGL theme styles. " + "Set style properties directly instead.", + path, + ) + return validated + + +def theme_schema(value: dict) -> dict: + return _reject_theme_styles_key( + _build_theme_schema(tuple(WIDGET_TYPES.items()))(value) + ) + + +def theme_update_schema(value: dict) -> dict: + """ + Schema for `lvgl.theme.update`: same shape as `theme:` minus `dark_mode`. + As a validation side effect, records which (widget type, part, state) + combos are targeted so `theme_to_code` can make sure a hidden style + exists for each -- even ones never mentioned under `theme:` -- and gets + it attached to widgets at the same point real theme styles are. + """ + validated = _reject_theme_styles_key( + _build_theme_schema(tuple(WIDGET_TYPES.items()), include_dark_mode=False)(value) + ) + for w_name, style in validated.items(): + for part, states in collect_parts(style).items(): + for state, props in states.items(): + # collect_parts() unconditionally seeds a main/default entry + # even when nothing was set for it (e.g. `{pressed: {...}}` + # alone) -- skip combos with no properties so a request for + # one state doesn't also create an unused, empty main/default + # style that gets attached to every widget of this type. + if not props: + continue + df.get_theme_update_requests().setdefault(w_name, {})[(part, state)] = ( + None + ) + return validated + + ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( { diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 5911505555..ad42028327 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -10,11 +10,18 @@ from .defines import ( LValidator, add_lv_use, get_styles_used, + get_theme_update_requests, get_theme_widget_map, literal, ) from .lvcode import LambdaContext, lv -from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, WIDGET_TYPES, remap_property +from .schemas import ( + ALL_STYLES, + FULL_STYLE_SCHEMA, + WIDGET_TYPES, + remap_property, + theme_update_schema, +) from .types import ObjUpdateAction, lv_style_t from .widgets import collect_parts, wait_for_widgets @@ -86,23 +93,91 @@ async def style_update_to_code(config, action_id, template_arg, args): style = await cg.get_variable(config[CONF_ID]) async with LambdaContext(parameters=args, where=action_id) as context: await style_set(style, config) + # Refresh and redraw every widget using this style -- otherwise the + # updated properties would sit unused until something else happens to + # invalidate the affected widgets. + lv.obj_report_style_change(style) return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) async def theme_to_code(config): - if theme := config.get(CONF_THEME): - add_lv_use(CONF_THEME) - for w_name, style in ((k, v) for k, v in theme.items() if k in WIDGET_TYPES): - # Work around Python 3.10 bug with nested async comprehensions - # With Python 3.11 this could be simplified - # TODO: Now that we require Python 3.11+, this can be updated to use nested comprehensions - styles = {} - for part, states in collect_parts(style).items(): - styles[part] = { - state: await create_style( + theme = config.get(CONF_THEME) or {} + requests = get_theme_update_requests() + # Iterate in WIDGET_TYPES' (deterministic, registration-order) sequence rather + # than a set -- a set of strings/tuples iterates in an order that depends on + # per-process hash randomization, which would otherwise churn the order hidden + # style variables are declared in main.cpp between builds of the same config. + widget_names = [ + w_name for w_name in WIDGET_TYPES if w_name in theme or w_name in requests + ] + if not widget_names: + return + add_lv_use(CONF_THEME) + theme_map = get_theme_widget_map() + for w_name in widget_names: + declared_parts = collect_parts(theme[w_name]) if w_name in theme else {} + parts = {part: dict(states) for part, states in declared_parts.items()} + for part, state in requests.get(w_name, {}): + parts.setdefault(part, {}).setdefault(state, {}) + widget_styles = theme_map.setdefault(w_name, {}) + for part, states in parts.items(): + part_styles = widget_styles.setdefault(part, {}) + declared_states = declared_parts.get(part, {}) + for state, props in states.items(): + if state not in part_styles: + part_styles[state] = await create_style( "_lv_theme_style_" + w_name + "_" + part + "_" + state, props ) - for state, props in states.items() - } - get_theme_widget_map()[w_name] = styles + elif state in declared_states: + # A `theme.update` request for this combo (possibly from + # another LVGL instance) already created the style as an + # empty placeholder before this instance's real `theme:` + # declaration was reached -- apply the real values now + # instead of silently leaving it empty. + await style_set(part_styles[state], props) + + +@automation.register_action( + "lvgl.theme.update", + ObjUpdateAction, + theme_update_schema, + synchronous=True, +) +async def theme_update_to_code(config, action_id, template_arg, args): + await wait_for_widgets() + theme_map = get_theme_widget_map() + # Invariant this relies on: theme_update_schema() records every (widget + # type, part, state) combo this action targets as a request during config + # validation (which completes for the whole config tree before any + # to_code runs), and theme_to_code() -- which runs for every LVGL + # instance before any action's own to_code -- materialises a style for + # each recorded request. If that handshake is ever broken by a future + # change, fail with a diagnosable message rather than a bare KeyError. + to_update = [] + for w_name, style in config.items(): + for part, states in collect_parts(style).items(): + for state, props in states.items(): + # collect_parts() unconditionally seeds an (empty) main/default + # entry even when this action didn't target it -- skip it, both + # because there's nothing to update and because + # theme_update_schema no longer pre-creates a placeholder style + # for combos with no properties. + if not props: + continue + style_var = theme_map.get(w_name, {}).get(part, {}).get(state) + if style_var is None: + raise cv.Invalid( + f"No theme style exists for '{w_name}' {part}/{state}. " + "This is an internal error -- please report it." + ) + to_update.append((style_var, props)) + async with LambdaContext(parameters=args, where=action_id) as context: + for style_var, props in to_update: + await style_set(style_var, props) + # Refresh and redraw every widget using this style -- otherwise the + # updated properties would sit unused until something else happens + # to invalidate the affected widgets. + lv.obj_report_style_change(style_var) + + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 509d5cc782..61efe385e6 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -67,6 +67,7 @@ lv_obj_t = LvType("lv_obj_t") lv_page_t = LvType("LvPageType", parents=(LvCompound,)) lv_image_t = LvType("lv_image_t") lv_gradient_t = LvType("lv_grad_dsc_t") +LvAnimation = lvgl_ns.class_("LvAnimation", cg.Component) lv_event_t = LvType("lv_event_t") RotationType = lvgl_ns.enum("RotationType") lv_point_t = cg.global_ns.struct("lv_point_t") diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 4d62c3de05..968db46adc 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -36,11 +36,10 @@ from ..defines import ( CONF_SCALE, CONF_STYLES, CONF_WIDGETS, + LOGGER, OBJ_FLAGS, PARTS, STATES, - TYPE_FLEX, - TYPE_GRID, LValidator, add_lv_use, call_lambda, @@ -541,44 +540,76 @@ def _size_to_str(value): return str(value) +def _grid_descriptor_array(name: str, specs) -> MockObj: + """Generate a file-scope ``static const`` grid row/column descriptor array + and return a reference to it.""" + values = ",".join(_size_to_str(x) for x in specs) + initializer = "{" + values + ", LV_GRID_TEMPLATE_LAST}" + arr_id = ID(name, is_declaration=True, type=lv_coord_t) + return cg.static_const_array(arr_id, cg.RawExpression(initializer)) + + +def _set_layout_options(w: Widget, layout: dict, base_name: str | None) -> None: + """Apply the layout options present in ``layout`` to ``w``. + + Only options actually present are applied, so this works both for widget + creation (where every option is supplied) and for update actions (where the + layout ``type`` and grid structure are fixed and only the style options are + changed). ``base_name`` names the generated grid descriptor arrays and is + only required at creation, when ``grid_rows``/``grid_columns`` are present. + """ + if (pad_row := layout.get(CONF_PAD_ROW)) is not None: + w.set_style(CONF_PAD_ROW, pad_row) + if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: + w.set_style(CONF_PAD_COLUMN, pad_column) + if (rows := layout.get(CONF_GRID_ROWS)) is not None: + w.set_style( + "grid_row_dsc_array", _grid_descriptor_array(f"{base_name}_row_dsc", rows) + ) + if (columns := layout.get(CONF_GRID_COLUMNS)) is not None: + w.set_style( + "grid_column_dsc_array", + _grid_descriptor_array(f"{base_name}_column_dsc", columns), + ) + if (align := layout.get(CONF_GRID_COLUMN_ALIGN)) is not None: + w.set_style(CONF_GRID_COLUMN_ALIGN, literal(align)) + if (align := layout.get(CONF_GRID_ROW_ALIGN)) is not None: + w.set_style(CONF_GRID_ROW_ALIGN, literal(align)) + if (flow := layout.get(CONF_FLEX_FLOW)) is not None: + lv_obj.set_flex_flow(w.obj, literal(flow)) + if (main := layout.get(CONF_FLEX_ALIGN_MAIN)) is not None: + w.set_style("flex_main_place", literal(main)) + if (cross := layout.get(CONF_FLEX_ALIGN_CROSS)) is not None: + # Stretch is implemented at creation time by sizing the children; at + # runtime we can only fall back to centering. + if cross == "LV_FLEX_ALIGN_STRETCH": + LOGGER.warning( + "Flex cross alignment 'stretch' is not supported at runtime; using 'center' instead" + ) + cross = "LV_FLEX_ALIGN_CENTER" + w.set_style("flex_cross_place", literal(cross)) + if (track := layout.get(CONF_FLEX_ALIGN_TRACK)) is not None: + w.set_style("flex_track_place", literal(track)) + + async def set_obj_properties(w: Widget, config): """Generate a list of C++ statements to apply properties to an lv_obj_t""" from ..schemas import ALL_STYLES, OBJ_PROPERTIES, remap_property if layout := config.get(CONF_LAYOUT): - layout_type: str = layout[CONF_TYPE] - add_lv_use(layout_type) - lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) - if (pad_row := layout.get(CONF_PAD_ROW)) is not None: - w.set_style(CONF_PAD_ROW, pad_row) - if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: - w.set_style(CONF_PAD_COLUMN, pad_column) - if layout_type == TYPE_GRID: - wid = config[CONF_ID] - rows = [_size_to_str(x) for x in layout[CONF_GRID_ROWS]] - rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}" - row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) - row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) - w.set_style("grid_row_dsc_array", row_array) - columns = [_size_to_str(x) for x in layout[CONF_GRID_COLUMNS]] - columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}" - column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) - column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) - w.set_style("grid_column_dsc_array", column_array) - w.set_style( - CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)) - ) - w.set_style(CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN))) - if layout_type == TYPE_FLEX: - lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) - main = literal(layout[CONF_FLEX_ALIGN_MAIN]) - cross = layout[CONF_FLEX_ALIGN_CROSS] - if cross == "LV_FLEX_ALIGN_STRETCH": - cross = "LV_FLEX_ALIGN_CENTER" - cross = literal(cross) - track = literal(layout[CONF_FLEX_ALIGN_TRACK]) - lv_obj.set_flex_align(w.obj, main, cross, track) + # The layout `type` (and the grid row/column structure) is only present + # when a widget is created; update actions only change the layout style + # options, leaving the type and grid structure unchanged. + layout_type = layout.get(CONF_TYPE) + if layout_type is not None: + add_lv_use(layout_type) + lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) + # The widget's own id gives the grid descriptor arrays stable names. + base_name = str(config[CONF_ID]) + else: + base_name = None + _set_layout_options(w, layout, base_name) parts = collect_parts(config) for part, states in parts.items(): part = "LV_PART_" + part.upper() diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index 8a046fea33..da81ab7737 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -1,3 +1,5 @@ +from esphome.components.image import INSTANCE_TYPE as IMAGE_TYPE +from esphome.components.mapping import get_mapping_metadata import esphome.config_validation as cv from esphome.const import ( CONF_ANGLE, @@ -9,7 +11,9 @@ from esphome.const import ( from ..defines import ( CONF_ANTIALIAS, + CONF_IMAGE, CONF_MAIN, + CONF_MAPPING, CONF_PIVOT_X, CONF_PIVOT_Y, CONF_SCALE, @@ -21,8 +25,6 @@ from ..types import lv_image_t from . import Widget, WidgetType from .label import CONF_LABEL -CONF_IMAGE = "image" - BASE_IMG_SCHEMA = cv.Schema( { cv.Optional(CONF_PIVOT_X): size, @@ -69,5 +71,16 @@ class ImgType(WidgetType): for prop, validator in BASE_IMG_SCHEMA.schema.items(): await w.set_property(prop, config, processor=validator) + def final_validate(self, widget, update_config, widget_config, path): + src = update_config.get(CONF_SRC) + if isinstance(src, dict) and CONF_MAPPING in src: + mapping_id = src[CONF_MAPPING] + metadata = get_mapping_metadata(mapping_id.id) + if str(metadata.to_.data_type) != str(IMAGE_TYPE): + raise cv.Invalid( + f"Mapping '{mapping_id}' does not map to an image type, but '{metadata.to_.data_type}'", + path=path + [CONF_SRC, CONF_MAPPING], + ) + img_spec = ImgType() diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 9e3d75575a..173d117457 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -19,6 +19,10 @@ void MCP23017::setup() { this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); + // Reset IPOL to 0x00: ESPHome handles 'inverted' in software. + this->write_reg(mcp23x17_base::MCP23X17_IPOLA, 0x00); + this->write_reg(mcp23x17_base::MCP23X17_IPOLB, 0x00); + uint8_t iocon_flags = 0; if (this->open_drain_ints_) { iocon_flags |= IOCON_ODR; diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 4573553664..e83a6847d6 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -342,7 +342,7 @@ bool Mcp4461Component::increase_wiper_(Mcp4461WiperIdx wiper) { ESP_LOGV(TAG, "Increasing wiper %u", wiper_idx); uint8_t addr = this->get_wiper_address_(wiper_idx); uint8_t reg = addr | static_cast(Mcp4461Commands::INCREMENT); - auto err = this->write(&this->address_, reg); + auto err = this->write(®, 1); if (err != i2c::ERROR_OK) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); @@ -373,7 +373,7 @@ bool Mcp4461Component::decrease_wiper_(Mcp4461WiperIdx wiper) { ESP_LOGV(TAG, "Decreasing wiper %u", wiper_idx); uint8_t addr = this->get_wiper_address_(wiper_idx); uint8_t reg = addr | static_cast(Mcp4461Commands::DECREMENT); - auto err = this->write(&this->address_, reg); + auto err = this->write(®, 1); if (err != i2c::ERROR_OK) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp index 10c81aac39..17e3665f50 100644 --- a/esphome/components/md5/md5.cpp +++ b/esphome/components/md5/md5.cpp @@ -5,7 +5,7 @@ namespace esphome::md5 { -#if defined(USE_ARDUINO) && !defined(USE_RP2040) && !defined(USE_ESP32) +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_ESP32) void MD5Digest::init() { memset(this->digest_, 0, 16); MD5Init(&this->ctx_); @@ -14,7 +14,7 @@ void MD5Digest::init() { void MD5Digest::add(const uint8_t *data, size_t len) { MD5Update(&this->ctx_, data, len); } void MD5Digest::calculate() { MD5Final(this->digest_, &this->ctx_); } -#endif // USE_ARDUINO && !USE_RP2040 +#endif // USE_ARDUINO && !USE_RP2 #ifdef USE_ESP32 void MD5Digest::init() { @@ -27,7 +27,7 @@ void MD5Digest::add(const uint8_t *data, size_t len) { esp_rom_md5_update(&this- void MD5Digest::calculate() { esp_rom_md5_final(this->digest_, &this->ctx_); } #endif // USE_ESP32 -#ifdef USE_RP2040 +#ifdef USE_RP2 void MD5Digest::init() { memset(this->digest_, 0, 16); br_md5_init(&this->ctx_); @@ -36,7 +36,7 @@ void MD5Digest::init() { void MD5Digest::add(const uint8_t *data, size_t len) { br_md5_update(&this->ctx_, data, len); } void MD5Digest::calculate() { br_md5_out(&this->ctx_, this->digest_); } -#endif // USE_RP2040 +#endif // USE_RP2 #ifdef USE_HOST MD5Digest::~MD5Digest() { diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index 5e841edd83..ff0f2852c8 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -19,7 +19,7 @@ #define MD5_CTX_TYPE md5_context_t #endif -#ifdef USE_RP2040 +#ifdef USE_RP2 #include #define MD5_CTX_TYPE br_md5_context #endif diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 2de67542b2..2d4f6085e5 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -70,11 +70,11 @@ def _require_network_interface(config: ConfigType) -> ConfigType: window. Reject at config time rather than silently producing a component that never initializes. """ - if config.get(CONF_DISABLED) or not (CORE.is_esp8266 or CORE.is_rp2040): + if config.get(CONF_DISABLED) or not (CORE.is_esp8266 or CORE.is_rp2): return config full_config = fv.full_config.get() has_wifi = "wifi" in full_config - has_ethernet = CORE.is_rp2040 and "ethernet" in full_config + has_ethernet = CORE.is_rp2 and "ethernet" in full_config if not (has_wifi or has_ethernet): options = "'wifi'" if CORE.is_esp8266 else "'wifi' or 'ethernet'" raise cv.Invalid( @@ -192,24 +192,24 @@ async def to_code(config): if CORE.using_arduino: if CORE.is_esp8266: cg.add_library("ESP8266mDNS", None) - elif CORE.is_rp2040: + elif CORE.is_rp2: cg.add_library("LEAmDNS", None) # Subscribe to the network IP state listener(s) so MDNS.update() is only # scheduled during the probe+announce phase. Same on_ip_state() override # serves both WiFi and Ethernet (signatures match). - if CORE.is_esp8266 or CORE.is_rp2040: + if CORE.is_esp8266 or CORE.is_rp2: if "wifi" in CORE.config: from esphome.components import wifi wifi.request_wifi_ip_state_listener() - if CORE.is_rp2040 and "ethernet" in CORE.config: + if CORE.is_rp2 and "ethernet" in CORE.config: from esphome.components import ethernet ethernet.request_ethernet_ip_state_listener() if CORE.is_esp32: - add_idf_component(name="espressif/mdns", ref="1.11.0") + add_idf_component(name="espressif/mdns", ref="1.11.3") cg.add_define("USE_MDNS") @@ -274,7 +274,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( }, "mdns_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "mdns_host.cpp": {PlatformFramework.HOST_NATIVE}, - "mdns_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "mdns_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, "mdns_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index e11cb1abaa..fa39e86ed0 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -47,7 +47,7 @@ void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_regi auto &services = services_storage; #endif -#ifdef USE_API +#ifdef USE_MDNS_DEVICE_INFO_TXT #ifdef USE_MDNS_STORE_SERVICES get_mac_address_into_buffer(this->mac_address_); char *mac_ptr = this->mac_address_; @@ -70,17 +70,20 @@ void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_regi platform_register(this, services); } -void MDNSComponent::compile_records_(StaticVector &services, char *mac_address_buf, - char *config_hash_buf) { +void MDNSComponent::compile_records_(StaticVector &services, + const char *mac_address_buf, const char *config_hash_buf) { // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES // in mdns/__init__.py. If you add a new service here, update both locations. +#ifdef USE_MDNS_DEVICE_INFO_TXT + MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); + MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac"); + MDNS_STATIC_CONST_CHAR(TXT_CONFIG_HASH, "config_hash"); +#endif + #ifdef USE_API MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib"); MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name"); - MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); - MDNS_STATIC_CONST_CHAR(TXT_CONFIG_HASH, "config_hash"); - MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac"); MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform"); MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board"); MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network"); @@ -100,14 +103,20 @@ void MDNSComponent::compile_records_(StaticVectorget_noise_ctx().has_psk(); txt_count++; // api_encryption or api_encryption_supported +#ifndef USE_API_NOISE_PSK_FROM_YAML + if (!api_has_psk) { + txt_count++; // api_provisioning + } +#endif #endif #ifdef ESPHOME_PROJECT_NAME txt_count += 2; // project_name and project_version @@ -136,9 +145,9 @@ void MDNSComponent::compile_records_(StaticVectorget_noise_ctx().has_psk(); - const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED; + const char *encryption_key = api_has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED; txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)}); +#ifndef USE_API_NOISE_PSK_FROM_YAML + if (!api_has_psk) { + // Unprovisioned device without a YAML key: advertise that the encryption + // key can be provisioned over a zero-PSK Noise connection. Gated on the + // YAML define so this survives the plaintext removal in 2027.2.0. + MDNS_STATIC_CONST_CHAR(TXT_API_PROVISIONING, "api_provisioning"); + MDNS_STATIC_CONST_CHAR(VALUE_ZERO_PSK, "zero-psk"); + txt_records.push_back({MDNS_STR(TXT_API_PROVISIONING), MDNS_STR(VALUE_ZERO_PSK)}); + } +#endif #endif #ifdef ESPHOME_PROJECT_NAME @@ -212,12 +230,18 @@ void MDNSComponent::compile_records_(StaticVector uint16_t { return USE_WEBSERVER_PORT; }; +#ifndef USE_API + // Without the native API there is no _esphomelib service, so publish the + // device info here for the device builder to discover. + web_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}, + {MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)}, + {MDNS_STR(TXT_CONFIG_HASH), MDNS_STR(config_hash_buf)}}; +#endif #endif #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_WEBSERVER) && \ !defined(USE_MDNS_EXTRA_SERVICES) MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http"); - MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); // Publish "http" service if not using native API or any other services // This is just to have *some* mDNS service so that .local resolution works @@ -225,7 +249,9 @@ void MDNSComponent::compile_records_(StaticVector uint16_t { return USE_WEBSERVER_PORT; }; - fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}}; + fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}, + {MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)}, + {MDNS_STR(TXT_CONFIG_HASH), MDNS_STR(config_hash_buf)}}; #endif } diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index e981a65941..4f97e8cb99 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -7,7 +7,7 @@ #include "esphome/core/helpers.h" // On ESP8266 and RP2040 the scheduler-backed MDNS.update() polling window is armed by // IP state listener events on whichever network interface is configured. -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && \ +#if (defined(USE_ESP8266) || defined(USE_RP2)) && \ ((defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)) || \ (defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS))) #include "esphome/components/network/ip_address.h" @@ -22,6 +22,15 @@ #endif #endif +// Device info TXT records (version, mac, config_hash) are published on the _esphomelib service +// when the native API is enabled, otherwise on the _http service (web_server's or the fallback one). +// When neither applies (only prometheus, sendspin or user-defined services are configured), no +// device info records are published and the buffers below are not needed. +#if defined(USE_API) || defined(USE_WEBSERVER) || \ + (!defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_MDNS_EXTRA_SERVICES)) +#define USE_MDNS_DEVICE_INFO_TXT +#endif + namespace esphome::mdns { // Helper struct that identifies strings that may be stored in flash storage (similar to LogString) @@ -136,7 +145,7 @@ class MDNSComponent final : public Component StaticVector dynamic_txt_values_; #endif -#if defined(USE_API) && defined(USE_MDNS_STORE_SERVICES) +#if defined(USE_MDNS_DEVICE_INFO_TXT) && defined(USE_MDNS_STORE_SERVICES) /// Fixed buffer for MAC address (only needed when services are stored) char mac_address_[MAC_ADDRESS_BUFFER_SIZE]; /// Fixed buffer for config hash hex string (only needed when services are stored) @@ -145,12 +154,12 @@ class MDNSComponent final : public Component #ifdef USE_MDNS_STORE_SERVICES StaticVector services_{}; #endif -#if defined(USE_RP2040) && defined(USE_MDNS_EVENT_DRIVEN_POLLING) +#if defined(USE_RP2) && defined(USE_MDNS_EVENT_DRIVEN_POLLING) // RP2040 defers MDNS.begin() until the first IP-up event; this tracks that. bool initialized_{false}; #endif - void compile_records_(StaticVector &services, char *mac_address_buf, - char *config_hash_buf); + void compile_records_(StaticVector &services, const char *mac_address_buf, + const char *config_hash_buf); }; } // namespace esphome::mdns diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index 1e66a10df0..c5d849df26 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -12,7 +12,7 @@ namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES -#ifdef USE_API +#ifdef USE_MDNS_DEVICE_INFO_TXT get_mac_address_into_buffer(this->mac_address_); char *mac_ptr = this->mac_address_; format_hex_to(this->config_hash_str_, App.get_config_hash()); diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index a543a3809a..5354bd241c 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -27,8 +27,8 @@ static void register_libretiny(MDNSComponent *, StaticVector &services) { +static void register_rp2(MDNSComponent *, StaticVector &services) { MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { @@ -82,7 +82,7 @@ void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network:: return; } if (!this->initialized_) { - this->setup_buffers_and_register_(register_rp2040); + this->setup_buffers_and_register_(register_rp2); this->initialized_ = true; } else { MDNS.notifyAPChange(); diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index cba6bcfa50..c427f28028 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -2,12 +2,15 @@ import hashlib import json import logging from pathlib import Path +import re from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition +from esphome.bundle import add_bundle_file import esphome.codegen as cg from esphome.components import esp32, microphone, ota, psram +from esphome.components.http_request import validate_url import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -28,6 +31,7 @@ from esphome.const import ( TYPE_LOCAL, ) from esphome.core import CORE, HexInt +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -207,40 +211,62 @@ def _validate_manifest_version(manifest_data): raise cv.Invalid("Invalid manifest file, missing 'version' key") -def _process_http_source(config): - url = config[CONF_URL] - path = _compute_local_file_path(config) +HTTP_SCHEMA = cv.Schema( + { + # validate_url only accepts http(s); the shorthand validator relies + # on this branch rejecting git shorthands ("github://...") so they + # fall through to the git branch. + cv.Required(CONF_URL): validate_url, + } +) - json_path = path / "manifest.json" - json_contents = external_files.download_content(url, json_path) +def _register_local_model_file(config: ConfigType) -> ConfigType: + """Register the model file that the manifest points to, so bundles include it. - manifest_data = json.loads(json_contents) - if not isinstance(manifest_data, dict): - raise cv.Invalid("Manifest file must contain a JSON object") - - model = manifest_data[CONF_MODEL] - model_url = urljoin(url, model) - - model_path = path / model - - external_files.download_content(str(model_url), model_path) + The manifest names its model file relative to itself, so that path never appears + in the YAML and bundle discovery cannot find it on its own. + Problems with the manifest are logged and ignored here rather than raised. Loading + the manifest later reports them with better messages, and raising would be + swallowed by the shorthand validator, which then reports a confusing error about a + missing file in a git repository. Logging keeps the skipped registration + diagnosable if the manifest is only briefly unreadable, since the bundle would + then be built without the model file. + """ + manifest_path: Path = config[CONF_PATH] + try: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + model = manifest[CONF_MODEL] + except (OSError, ValueError, KeyError, TypeError) as err: + _LOGGER.debug("Not registering a model file from %s: %s", manifest_path, err) + return config + if not isinstance(model, str): + _LOGGER.debug( + "Not registering a model file from %s: 'model' is %s, expected a string", + manifest_path, + type(model).__name__, + ) + return config + add_bundle_file(manifest_path.parent / model) return config -HTTP_SCHEMA = cv.All( - { - cv.Required(CONF_URL): cv.url, - }, - _process_http_source, +LOCAL_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_PATH): cv.All(_validate_json_filename, cv.file_), + } + ), + _register_local_model_file, ) -LOCAL_SCHEMA = cv.Schema( - { - cv.Required(CONF_PATH): cv.All(_validate_json_filename, cv.file_), - } -) + +# Bare model names in the official model repository ("okay_nabu"). Must not +# overlap with local paths, http(s) urls, or git shorthands +# ("github://user/repo/file.json@ref"), which the shorthand validator tries +# next; anything containing "/", ":" or "@" is not a model name. +_MODEL_NAME_RE = re.compile(r"[A-Za-z0-9_.-]+") def _validate_source_model_name(value): @@ -250,6 +276,9 @@ def _validate_source_model_name(value): if value.endswith(".json"): raise cv.Invalid("Model name must not end with .json") + if not _MODEL_NAME_RE.fullmatch(value): + raise cv.Invalid("Model name may only contain letters, numbers, . _ -") + return MODEL_SOURCE_SCHEMA( { CONF_TYPE: TYPE_HTTP, @@ -339,6 +368,58 @@ def _maybe_empty_vad_schema(value): return VAD_MODEL_SCHEMA(value) +def _download_http_models(config: ConfigType) -> ConfigType: + """Download every http-sourced manifest and model file in two concurrent + batches (all manifests, then all model files). + + The model file's URL only becomes known once its manifest has been + fetched and parsed, so the two stages cannot be merged into one batch. + """ + model_parameters = [*config[CONF_MODELS]] + if vad := config.get(CONF_VAD): + model_parameters.append(vad) + # Keyed by cache path so a URL referenced twice is fetched and parsed once + http_models: dict[Path, str] = { + _compute_local_file_path(model_config): model_config[CONF_URL] + for parameters in model_parameters + if (model_config := parameters.get(CONF_MODEL)) is not None + and model_config.get(CONF_TYPE) == TYPE_HTTP + } + if not http_models: + return config + + external_files.download_content_many( + ((url, path / "manifest.json") for path, url in http_models.items()), + description="wake word manifest(s)", + ) + + model_files: list[tuple[str, Path]] = [] + errors: list[cv.Invalid] = [] + for path, url in http_models.items(): + try: + manifest_data = json.loads((path / "manifest.json").read_bytes()) + except (OSError, ValueError) as e: + errors.append(cv.Invalid(f"Invalid manifest file at {url}: {e}")) + continue + if not isinstance(manifest_data, dict): + errors.append( + cv.Invalid(f"Manifest file at {url} must contain a JSON object") + ) + continue + model = manifest_data.get(CONF_MODEL) + if not isinstance(model, str): + errors.append( + cv.Invalid(f"Manifest file at {url} is missing the 'model' key") + ) + continue + model_files.append((urljoin(url, model), path / model)) + if errors: + raise cv.MultipleInvalid(errors) + + external_files.download_content_many(model_files, description="wake word model(s)") + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -372,6 +453,7 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.only_on_esp32, + _download_http_models, ) diff --git a/esphome/components/micro_wake_word/automation.h b/esphome/components/micro_wake_word/automation.h index e3b35583fb..59dfc624fa 100644 --- a/esphome/components/micro_wake_word/automation.h +++ b/esphome/components/micro_wake_word/automation.h @@ -7,22 +7,22 @@ namespace esphome::micro_wake_word { -template class StartAction : public Action, public Parented { +template class StartAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->start(); } }; -template class StopAction : public Action, public Parented { +template class StopAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->stop(); } }; -template class IsRunningCondition : public Condition, public Parented { +template class IsRunningCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_running(); } }; -template class EnableModelAction : public Action { +template class EnableModelAction final : public Action { public: explicit EnableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} void play(const Ts &...x) override { this->wake_word_model_->enable(); } @@ -31,7 +31,7 @@ template class EnableModelAction : public Action { WakeWordModel *wake_word_model_; }; -template class DisableModelAction : public Action { +template class DisableModelAction final : public Action { public: explicit DisableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} void play(const Ts &...x) override { this->wake_word_model_->disable(); } @@ -40,7 +40,7 @@ template class DisableModelAction : public Action { WakeWordModel *wake_word_model_; }; -template class ModelIsEnabledCondition : public Condition { +template class ModelIsEnabledCondition final : public Condition { public: explicit ModelIsEnabledCondition(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} bool check(const Ts &...x) override { return this->wake_word_model_->is_enabled(); } diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index e4c590a423..aebb5b2595 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -31,10 +31,10 @@ enum State { STOPPED, }; -class MicroWakeWord : public Component +class MicroWakeWord final : public Component #ifdef USE_OTA_STATE_LISTENER , - public ota::OTAGlobalStateListener + public ota::OTAGlobalStateListener #endif { public: diff --git a/esphome/components/microphone/automation.h b/esphome/components/microphone/automation.h index 1dfd91f903..c28616a290 100644 --- a/esphome/components/microphone/automation.h +++ b/esphome/components/microphone/automation.h @@ -7,34 +7,34 @@ namespace esphome::microphone { -template class CaptureAction : public Action, public Parented { +template class CaptureAction final : public Action, public Parented { void play(const Ts &...x) override { this->parent_->start(); } }; -template class StopCaptureAction : public Action, public Parented { +template class StopCaptureAction final : public Action, public Parented { void play(const Ts &...x) override { this->parent_->stop(); } }; -template class MuteAction : public Action, public Parented { +template class MuteAction final : public Action, public Parented { void play(const Ts &...x) override { this->parent_->set_mute_state(true); } }; -template class UnmuteAction : public Action, public Parented { +template class UnmuteAction final : public Action, public Parented { void play(const Ts &...x) override { this->parent_->set_mute_state(false); } }; -class DataTrigger : public Trigger &> { +class DataTrigger final : public Trigger &> { public: explicit DataTrigger(Microphone *mic) { mic->add_data_callback([this](const std::vector &data) { this->trigger(data); }); } }; -template class IsCapturingCondition : public Condition, public Parented { +template class IsCapturingCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_running(); } }; -template class IsMutedCondition : public Condition, public Parented { +template class IsMutedCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->get_mute_state(); } }; diff --git a/esphome/components/microphone/microphone_source.h b/esphome/components/microphone/microphone_source.h index c3c675e854..7be3b8cdb5 100644 --- a/esphome/components/microphone/microphone_source.h +++ b/esphome/components/microphone/microphone_source.h @@ -13,7 +13,7 @@ namespace esphome::microphone { static const int32_t MAX_GAIN_FACTOR = 64; -class MicrophoneSource { +class MicrophoneSource final { /* * @brief Helper class that handles converting raw microphone data to a requested format. * Components requesting microphone audio should register a callback through this class instead of registering a diff --git a/esphome/components/mics_4514/mics_4514.h b/esphome/components/mics_4514/mics_4514.h index 4f8b970f06..d8c422808a 100644 --- a/esphome/components/mics_4514/mics_4514.h +++ b/esphome/components/mics_4514/mics_4514.h @@ -7,7 +7,7 @@ namespace esphome::mics_4514 { -class MICS4514Component : public PollingComponent, public i2c::I2CDevice { +class MICS4514Component final : public PollingComponent, public i2c::I2CDevice { SUB_SENSOR(carbon_monoxide) SUB_SENSOR(nitrogen_dioxide) SUB_SENSOR(methane) diff --git a/esphome/components/midea/ac_adapter.cpp b/esphome/components/midea/ac_adapter.cpp index 2f4ef5c948..77bb9bbe86 100644 --- a/esphome/components/midea/ac_adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY) #include "esphome/core/log.h" #include "ac_adapter.h" diff --git a/esphome/components/midea/ac_adapter.h b/esphome/components/midea/ac_adapter.h index a7924ae51e..4545743564 100644 --- a/esphome/components/midea/ac_adapter.h +++ b/esphome/components/midea/ac_adapter.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY) // MideaUART #include diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h index acd9191916..b595a018b3 100644 --- a/esphome/components/midea/ac_automations.h +++ b/esphome/components/midea/ac_automations.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY) #include "esphome/core/automation.h" #include "air_conditioner.h" diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 7603dd5254..e55afedd8a 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY) #include "esphome/core/helpers.h" #include "esphome/core/log.h" diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 6ed5a82ff5..089928902e 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY) // MideaUART #include @@ -21,7 +21,7 @@ using climate::ClimateModeMask; using climate::ClimateSwingModeMask; using climate::ClimatePresetMask; -class AirConditioner : public ApplianceBase, public climate::Climate { +class AirConditioner final : public ApplianceBase, public climate::Climate { public: void dump_config() override; void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; } diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index d36f5a322c..1b45561fab 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY) // MideaUART #include diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 4a75464b90..b0c102af6d 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -25,6 +25,8 @@ from esphome.const import ( ICON_POWER, ICON_THERMOMETER, ICON_WATER_PERCENT, + PLATFORM_ESP32, + PLATFORM_ESP8266, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -152,6 +154,12 @@ CONFIG_SCHEMA = cv.All( .extend(uart.UART_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA), cv.only_with_arduino, + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + ] + ), ) # Actions diff --git a/esphome/components/midea/ir_transmitter.h b/esphome/components/midea/ir_transmitter.h index f11682230d..ecf3fa1c1a 100644 --- a/esphome/components/midea/ir_transmitter.h +++ b/esphome/components/midea/ir_transmitter.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY) #ifdef USE_REMOTE_TRANSMITTER #include "esphome/components/remote_base/midea_protocol.h" diff --git a/esphome/components/midea_ir/midea_ir.h b/esphome/components/midea_ir/midea_ir.h index dd883172d4..e89eaf0110 100644 --- a/esphome/components/midea_ir/midea_ir.h +++ b/esphome/components/midea_ir/midea_ir.h @@ -11,7 +11,7 @@ const uint8_t MIDEA_TEMPC_MAX = 30; // Celsius const uint8_t MIDEA_TEMPF_MIN = 62; // Fahrenheit const uint8_t MIDEA_TEMPF_MAX = 86; // Fahrenheit -class MideaIR : public climate_ir::ClimateIR { +class MideaIR final : public climate_ir::ClimateIR { public: MideaIR() : climate_ir::ClimateIR( diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 1d6c8277e8..3f73f96327 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -26,16 +26,22 @@ from esphome.const import ( CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, CONF_PAGES, + CONF_RESET_PIN, CONF_ROTATION, CONF_SWAP_XY, CONF_TRANSFORM, CONF_WIDTH, ) -from esphome.core import TimePeriod +from esphome.core import CORE, TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor LOGGER = cv.logging.getLogger(__name__) +CONF_TRANSFORMS = "transforms" + +# All axis transforms a model may support, in the order they appear in the schema. +ALL_TRANSFORMS = (CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY) + ColorOrder = display_ns.enum("ColorMode") NOP = 0x00 @@ -302,7 +308,8 @@ class DriverChip: """ A class representing a MIPI DBI driver chip model. The parameters supplied as defaults will be used to provide default values for the display configuration. - Setting swap_xy to cv.UNDEFINED will indicate that the model does not support swapping X and Y axes. + Pass a ``transforms`` set to restrict which axis transforms (mirror_x, mirror_y, swap_xy) the model + supports; by default all three are available. """ models: dict[str, Self] = {} @@ -387,11 +394,15 @@ class DriverChip: """ Return the available transforms for this model. """ + if (transforms := self.get_default(CONF_TRANSFORMS, None)) is not None: + return transforms if self.get_default("no_transform", False): return set() if self.get_default(CONF_SWAP_XY) != cv.UNDEFINED: return {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} - return {CONF_MIRROR_X, CONF_MIRROR_Y} + raise ValueError( + "Setting 'swap_xy' to 'cv.UNDEFINED' is no longer supported; set 'transforms' instead" + ) def has_hardware_transform(self, config) -> bool: """ @@ -533,17 +544,31 @@ class DriverChip: transform[CONF_TRANSFORM] = self.rotation_as_transform(config) return transform - def swap_xy_schema(self): - uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED + def transform_schema(self): + """ + Build the schema for the ``transform`` config option of this model. - def validator(value): - if value: - raise cv.Invalid("Axis swapping not supported by this model") - return cv.boolean(value) + Each transform the model supports is a required boolean. A transform the model does not + support may be omitted or set to ``false``; setting it to ``true`` reports a clear error + naming the unsupported transform instead of a generic "extra keys not allowed". + """ + supported = self.transforms - if uses_swap: - return {cv.Required(CONF_SWAP_XY): cv.boolean} - return {cv.Optional(CONF_SWAP_XY, default=False): validator} + def unsupported(name): + def validator(value): + if cv.boolean(value): + raise cv.Invalid(f"'{name}' is not supported by this model") + return False + + return validator + + schema = {} + for name in ALL_TRANSFORMS: + if name in supported: + schema[cv.Required(name)] = cv.boolean + else: + schema[cv.Optional(name, default=False)] = unsupported(name) + return cv.Any(cv.Schema(schema), cv.one_of(CONF_DISABLED, lower=True)) def get_madctl(self, transform: dict, config: dict) -> int: """ @@ -577,12 +602,15 @@ class DriverChip: """ return self.get_default(f"no_{command.lower()}", False) - def get_sequence(self, config, add_madctl=True) -> tuple[int, ...]: + def get_sequence(self, config, add_madctl=True, add_reset=False) -> tuple[int, ...]: """ Create the init sequence for the display. Use the default sequence from the model, if any, and append any custom sequence provided in the config. Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence MADCTL will be set if add_madctl is True + If add_reset is True, a reset is prepended: a software reset when no reset pin + is configured (and the model doesn't skip it), followed by a settling delay that + both a software and a hardware reset require. Returns the init sequence """ sequence = list(self.initsequence or ()) @@ -591,6 +619,15 @@ class DriverChip: # Ensure each command is a tuple sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] + if add_reset: + reset: list = [] + # A software reset is only needed when there is no hardware reset pin. + if CONF_RESET_PIN not in config and not self.skip_command("SWRESET"): + reset.append((SWRESET,)) + # Both a software and a hardware reset need a settling delay before further commands. + reset.append(delay(10)) + sequence = reset + sequence + # Set pixel format if not already in the custom sequence pixel_mode = config[CONF_PIXEL_MODE] if not isinstance(pixel_mode, int): @@ -611,13 +648,47 @@ class DriverChip: sequence.append((BRIGHTNESS, brightness)) # Add a SLPOUT command if required. if not self.skip_command("SLPOUT"): + # A zero delay will delay until 120ms after reset + sequence.append(delay(0)) sequence.append((SLPOUT,)) + sequence.append(delay(10)) sequence.append((DISPON,)) + # Add a delay here because additional commands may be added after this at runtime. + sequence.append(delay(10)) # Flatten the sequence into a list of bytes, with the length of each command # or the delay flag inserted where needed return flatten_sequence(sequence) + def check_requirements(self) -> None: + """ + Raise a friendly error if any component this model requires is not configured. + + This runs during schema validation (before ID references are resolved) so that a + model whose default pins live on a pin expander reports the missing expander clearly + instead of a cryptic "Couldn't find ID" from the unresolved pin reference. + + Also logs a warning if the model is deprecated. + """ + if deprecation_reason := self.get_default("deprecation_reason"): + LOGGER.warning( + "Display model %s is deprecated: %s", self.name, deprecation_reason + ) + if requirements := self.get_default("requires", set()): + # ``raw_config`` is populated before any component schema runs during a real + # validation, so presence of a required component is simply a top-level key. + # When it is absent (e.g. a unit test that invokes the schema directly) there + # is no config to check against, so skip. + global_config = CORE.raw_config + if global_config is None: + return + missing = {x for x in requirements if x not in global_config} + if missing: + reqstr = ", ".join(f"'{x}'" for x in sorted(missing)) + raise cv.Invalid( + f"{self.name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured" + ) + def requires_buffer(config) -> bool: """ diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index 896140b4b1..e5bb3d413d 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -41,24 +41,21 @@ from esphome.const import ( CONF_AUTO_CLEAR_ENABLED, CONF_COLOR_ORDER, CONF_DIMENSIONS, - CONF_DISABLED, CONF_ENABLE_PIN, CONF_ID, CONF_INIT_SEQUENCE, CONF_INVERT_COLORS, CONF_LAMBDA, - CONF_MIRROR_X, - CONF_MIRROR_Y, CONF_MODEL, CONF_RESET_PIN, CONF_ROTATION, - CONF_SWAP_XY, CONF_TRANSFORM, CONF_WIDTH, ) from esphome.final_validate import full_config from . import mipi_dsi_ns, models +from .models import DsiDriverChip # Currently only ESP32-P4 is supported, so esp_ldo and psram are required DEPENDENCIES = ["esp32", "esp_ldo", "psram"] @@ -73,7 +70,7 @@ ColorBitness = display.display_ns.enum("ColorBitness") CONF_LANE_BIT_RATE = "lane_bit_rate" CONF_LANES = "lanes" -DriverChip("CUSTOM") +DsiDriverChip("CUSTOM") # Import all models dynamically from the models package @@ -90,19 +87,7 @@ COLOR_DEPTHS = { def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] - model.defaults[CONF_SWAP_XY] = cv.UNDEFINED - transform = cv.Any( - cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - cv.Optional(CONF_SWAP_XY): cv.invalid( - "Axis swapping not supported by DSI displays" - ), - } - ), - cv.one_of(CONF_DISABLED, lower=True), - ) + transform = model.transform_schema() # CUSTOM model will need to provide a custom init sequence iseqconf = ( cv.Required(CONF_INIT_SEQUENCE) @@ -172,6 +157,7 @@ def _config_schema(config): )(config) config = model_schema(config)(config) model = MODELS[config[CONF_MODEL].upper()] + model.check_requirements() width, height, _offset_width, _offset_height, _pad_width, _pad_height = ( model.get_dimensions(config) ) diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index c99f69989a..7bf2feb73c 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -35,7 +35,7 @@ const uint8_t MADCTL_MV = 0x20; // row/column swap const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically -class MipiDsi : public display::Display { +class MipiDsi final : public display::Display { public: MipiDsi(size_t width, size_t height, display::ColorBitness color_depth, uint8_t pixel_mode) : width_(width), height_(height), color_depth_(color_depth), pixel_mode_(pixel_mode) {} diff --git a/esphome/components/mipi_dsi/models/__init__.py b/esphome/components/mipi_dsi/models/__init__.py index e69de29bb2..3f7f8370a3 100644 --- a/esphome/components/mipi_dsi/models/__init__.py +++ b/esphome/components/mipi_dsi/models/__init__.py @@ -0,0 +1,14 @@ +from esphome.components.mipi import DriverChip +from esphome.const import CONF_SWAP_XY + + +class DsiDriverChip(DriverChip): + """A driver chip for MIPI DSI displays.""" + + @property + def transforms(self) -> set[str]: + """ + Return the set of transformations supported by this driver chip. + DSI displays do not support axis swapping, so this method removes CONF_SWAP_XY + """ + return super().transforms - {CONF_SWAP_XY} diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py index db13c7f6cc..914361a4ac 100644 --- a/esphome/components/mipi_dsi/models/guition.py +++ b/esphome/components/mipi_dsi/models/guition.py @@ -1,8 +1,7 @@ -from esphome.components.mipi import DriverChip -import esphome.config_validation as cv +from . import DsiDriverChip # fmt: off -DriverChip( +DsiDriverChip( "JC1060P470", width=1024, height=600, @@ -14,7 +13,6 @@ DriverChip( vsync_front_porch=12, pclk_frequency="54MHz", lane_bit_rate="750Mbps", - swap_xy=cv.UNDEFINED, color_order="RGB", initsequence=[ (0x30, 0x00), (0xF7, 0x49, 0x61, 0x02, 0x00), (0x30, 0x01), (0x04, 0x0C), (0x05, 0x00), (0x06, 0x00), @@ -46,7 +44,7 @@ DriverChip( # * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42) # * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166) # ---------------------------------------------------------------------------------------------------------------------- -DriverChip( +DsiDriverChip( "JC4880P443", width=480, height=800, @@ -58,7 +56,6 @@ DriverChip( vsync_front_porch=166, pclk_frequency="34MHz", lane_bit_rate="500Mbps", - swap_xy=cv.UNDEFINED, color_order="RGB", reset_pin=5, initsequence=[ @@ -111,7 +108,7 @@ DriverChip( # * Horizontal Timing (hsync_pulse_width=20, hsync_back_porch=20, hsync_front_porch=40) # * Vertical Timing (vsync_pulse_width=4, vsync_back_porch=8, vsync_front_porch=20) # ---------------------------------------------------------------------------------------------------------------------- -DriverChip( +DsiDriverChip( "JC8012P4A1", width=800, height=1280, @@ -123,7 +120,6 @@ DriverChip( vsync_front_porch=20, pclk_frequency="60MHz", lane_bit_rate="1Gbps", - swap_xy=cv.UNDEFINED, color_order="RGB", reset_pin=27, initsequence=[ @@ -322,4 +318,232 @@ DriverChip( (0xE0, 0x00), ] ) + +# JC8012P4A1 V2 Driver Configuration (jd9365) +# Some units of this model have a different LCD panel but still use the same JD9365 driver chip. +# Using parameters from esp_lcd_jd9365.h and the working full init sequence +# ---------------------------------------------------------------------------------------------------------------------- +# * Resolution: 800x1280 +# * PCLK Frequency: 70 MHz +# * DSI Lane Bit Rate: 1.5 Gbps (using 2-Lane DSI configuration) +# * Horizontal Timing (hsync_pulse_width=20, hsync_back_porch=20, hsync_front_porch=40) +# * Vertical Timing (vsync_pulse_width=4, vsync_back_porch=10, vsync_front_porch=20) +# ---------------------------------------------------------------------------------------------------------------------- +DsiDriverChip( + "JC8012P4A1-V2", + width=800, + height=1280, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=10, + vsync_pulse_width=4, + vsync_front_porch=20, + pclk_frequency="70MHz", + lane_bit_rate="1500Mbps", + color_order="RGB", + reset_pin=27, + initsequence=[ + (0xE0, 0x00), + (0xE1, 0x93), + (0xE2, 0x65), + (0xE3, 0xF8), + (0x80, 0x01), + (0xE0, 0x01), + (0x00, 0x00), + (0x01, 0x44), + (0x03, 0x10), + (0x04, 0x38), + (0x0C, 0x74), + (0x17, 0x00), + (0x18, 0xAF), + (0x19, 0x00), + (0x1A, 0x00), + (0x1B, 0xAF), + (0x1C, 0x00), + (0x35, 0x26), + (0x37, 0x09), + (0x38, 0x04), + (0x39, 0x00), + (0x3A, 0x01), + (0x3C, 0x78), + (0x3D, 0xFF), + (0x3E, 0xFF), + (0x3F, 0x7F), + (0x40, 0x06), + (0x41, 0xA0), + (0x42, 0x81), + (0x43, 0x1E), + (0x44, 0x0D), + (0x45, 0x28), + (0x55, 0x02), + (0x57, 0x69), + (0x59, 0x0A), + (0x5A, 0x2A), + (0x5B, 0x17), + (0x5D, 0x7F), + (0x5E, 0x6B), + (0x5F, 0x5C), + (0x60, 0x50), + (0x61, 0x4C), + (0x62, 0x3E), + (0x63, 0x41), + (0x64, 0x2B), + (0x65, 0x43), + (0x66, 0x42), + (0x67, 0x43), + (0x68, 0x62), + (0x69, 0x52), + (0x6A, 0x5A), + (0x6B, 0x4C), + (0x6C, 0x48), + (0x6D, 0x3A), + (0x6E, 0x28), + (0x6F, 0x10), + (0x70, 0x7F), + (0x71, 0x6B), + (0x72, 0x5C), + (0x73, 0x50), + (0x74, 0x4C), + (0x75, 0x3E), + (0x76, 0x41), + (0x77, 0x2B), + (0x78, 0x43), + (0x79, 0x42), + (0x7A, 0x43), + (0x7B, 0x62), + (0x7C, 0x52), + (0x7D, 0x5A), + (0x7E, 0x4C), + (0x7F, 0x48), + (0x80, 0x3A), + (0x81, 0x28), + (0x82, 0x10), + (0xE0, 0x02), + (0x00, 0x42), + (0x01, 0x42), + (0x02, 0x40), + (0x03, 0x40), + (0x04, 0x5E), + (0x05, 0x5E), + (0x06, 0x5F), + (0x07, 0x5F), + (0x08, 0x5F), + (0x09, 0x57), + (0x0A, 0x57), + (0x0B, 0x77), + (0x0C, 0x77), + (0x0D, 0x47), + (0x0E, 0x47), + (0x0F, 0x45), + (0x10, 0x45), + (0x11, 0x4B), + (0x12, 0x4B), + (0x13, 0x49), + (0x14, 0x49), + (0x15, 0x5F), + (0x16, 0x41), + (0x17, 0x41), + (0x18, 0x40), + (0x19, 0x40), + (0x1A, 0x5E), + (0x1B, 0x5E), + (0x1C, 0x5F), + (0x1D, 0x5F), + (0x1E, 0x5F), + (0x1F, 0x57), + (0x20, 0x57), + (0x21, 0x77), + (0x22, 0x77), + (0x23, 0x46), + (0x24, 0x46), + (0x25, 0x44), + (0x26, 0x44), + (0x27, 0x4A), + (0x28, 0x4A), + (0x29, 0x48), + (0x2A, 0x48), + (0x2B, 0x5F), + (0x2C, 0x01), + (0x2D, 0x01), + (0x2E, 0x00), + (0x2F, 0x00), + (0x30, 0x1F), + (0x31, 0x1F), + (0x32, 0x1E), + (0x33, 0x1E), + (0x34, 0x1F), + (0x35, 0x17), + (0x36, 0x17), + (0x37, 0x37), + (0x38, 0x37), + (0x39, 0x08), + (0x3A, 0x08), + (0x3B, 0x0A), + (0x3C, 0x0A), + (0x3D, 0x04), + (0x3E, 0x04), + (0x3F, 0x06), + (0x40, 0x06), + (0x41, 0x1F), + (0x42, 0x02), + (0x43, 0x02), + (0x44, 0x00), + (0x45, 0x00), + (0x46, 0x1F), + (0x47, 0x1F), + (0x48, 0x1E), + (0x49, 0x1E), + (0x4A, 0x1F), + (0x4B, 0x17), + (0x4C, 0x17), + (0x4D, 0x37), + (0x4E, 0x37), + (0x4F, 0x09), + (0x50, 0x09), + (0x51, 0x0B), + (0x52, 0x0B), + (0x53, 0x05), + (0x54, 0x05), + (0x55, 0x07), + (0x56, 0x07), + (0x57, 0x1F), + (0x58, 0x40), + (0x5B, 0x30), + (0x5C, 0x00), + (0x5D, 0x34), + (0x5E, 0x05), + (0x5F, 0x02), + (0x63, 0x00), + (0x64, 0x6A), + (0x67, 0x73), + (0x68, 0x07), + (0x69, 0x08), + (0x6A, 0x6A), + (0x6B, 0x08), + (0x6C, 0x00), + (0x6D, 0x00), + (0x6E, 0x00), + (0x6F, 0x88), + (0x75, 0xFF), + (0x77, 0xDD), + (0x78, 0x2C), + (0x79, 0x15), + (0x7A, 0x17), + (0x7D, 0x14), + (0x7E, 0x82), + (0xE0, 0x04), + (0x00, 0x0E), + (0x02, 0xB3), + (0x09, 0x60), + (0x0E, 0x48), + (0x37, 0x58), + (0x2B, 0x0F), + (0xE0, 0x05), + (0x15, 0x1D), + (0xE0, 0x00), + (0xE6, 0x02), + (0xE7, 0x0C) + ] +) # fmt: on diff --git a/esphome/components/mipi_dsi/models/m5stack.py b/esphome/components/mipi_dsi/models/m5stack.py index 53fac9b534..5b07229ec7 100644 --- a/esphome/components/mipi_dsi/models/m5stack.py +++ b/esphome/components/mipi_dsi/models/m5stack.py @@ -1,8 +1,7 @@ -from esphome.components.mipi import DriverChip -import esphome.config_validation as cv +from . import DsiDriverChip # fmt: off -DriverChip( +DsiDriverChip( "M5STACK-TAB5", height=1280, width=720, @@ -14,7 +13,6 @@ DriverChip( vsync_front_porch=20, pclk_frequency="60MHz", lane_bit_rate="730Mbps", - swap_xy=cv.UNDEFINED, color_order="RGB", initsequence=[ (0xFF, 0x98, 0x81, 0x01), # Select Page 1 @@ -56,8 +54,8 @@ DriverChip( ], ) -DriverChip( - "M5STACK-TAB5-V2", +TAB5_ST7123 = DsiDriverChip( + "M5STACK-TAB5-ST7123", height=1280, width=720, hsync_back_porch=40, @@ -68,7 +66,6 @@ DriverChip( vsync_front_porch=220, pclk_frequency="80MHz", lane_bit_rate="960Mbps", - swap_xy=cv.UNDEFINED, color_order="RGB", initsequence=[ (0x01,), @@ -97,3 +94,58 @@ DriverChip( (0xC9, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF), ], ) + +TAB5_ST7123.extend( + "M5STACK-TAB5-V2", + deprecation_reason="Use 'M5STACK-TAB5-ST7123' or 'M5STACK-TAB5-ST7121' instead." +) + +# Some Tab5 "v2" units ship with an ST7121 controller instead of the ST7123. +# The two are distinguishable at runtime by the touch controller firmware version (the M5 +# factory firmware branches on it), but ESPHome selects the panel at compile time, so ST7121 +# units must select this model explicitly. Values taken from M5's factory source +# (m5stack/M5Tab5-UserDemo: m5stack_tab5.c is_st7121 path + esp_lcd_st7121.c default table). +DsiDriverChip( + "M5STACK-TAB5-ST7121", + height=1280, + width=720, + hsync_back_porch=40, + hsync_pulse_width=2, + hsync_front_porch=40, + vsync_back_porch=24, + vsync_pulse_width=20, + vsync_front_porch=200, + pclk_frequency="70MHz", + lane_bit_rate="965Mbps", + color_order="RGB", + initsequence=[ + (0x01,), + (0x60, 0x71, 0x21, 0xA2), + (0x60, 0x71, 0x21, 0xA3), + (0x60, 0x71, 0x21, 0xA4), + (0x78, 0x21), + (0x79, 0xEF), + (0xA4, 0x31), + (0xB7, 0x00, 0x00, 0x5F, 0x5F, 0x44, 0x1A), + (0xB0, 0x22, 0x6B, 0x11, 0x89, 0x25, 0x43, 0x43), + (0xBF, 0xA7, 0xA7), + (0xA5, 0xF0, 0x03), + (0xD7, 0x10, 0x2C, 0x14, 0x2A, 0x80, 0x80), + (0x90, 0x71, 0x23, 0x5A, 0x20, 0x24, 0x11, 0x21), + (0xA3, 0x80, 0x01, 0x8C, 0xFF, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x10, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x10, 0xEF, 0x58, 0x00, 0x00, 0x00, 0xFF), + (0xA6, 0x0A, 0x00, 0x24, 0x71, 0x36, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x00, 0x24, 0x71, 0x37, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x00, 0x24, 0x71, 0x00, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x00, 0x2C, 0x71, 0x00, 0x01, 0x00, 0x00, 0x68, 0x68, 0xFF, 0xFF, 0x00, 0x08, 0x80, 0x08, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00), + (0xA7, 0x1A, 0x1A, 0xC0, 0x64, 0x40, 0x04, 0x15, 0x40, 0x00, 0x40, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x26, 0x37, 0x40, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x8C, 0x9D, 0x40, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0xAE, 0xBF, 0x00, 0x00, 0x20, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x08, 0x80, 0x79), + (0xAC, 0x1D, 0x18, 0x19, 0x1D, 0x18, 0x19, 0x04, 0x1C, 0x1D, 0x08, 0x0A, 0x10, 0x12, 0x0C, 0x0E, 0x14, 0x16, 0x00, 0x1D, 0x1D, 0x1D, 0x1D, 0x1D, 0x18, 0x19, 0x1D, 0x18, 0x19, 0x06, 0x1C, 0x1D, 0x09, 0x0B, 0x11, 0x13, 0x0D, 0x0F, 0x15, 0x17, 0x02, 0x1D, 0x1D, 0x1D, 0x1D), + (0xAD, 0x0C, 0x40, 0x46, 0x00, 0x07, 0x4B, 0x4B, 0xFF, 0xFF, 0xF0, 0x40, 0x0E, 0x01, 0x07, 0x42, 0x42, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF), + (0xAE, 0xF0, 0xFF, 0x03, 0xF0, 0xFF, 0x03, 0x00), + (0xB2, 0x15, 0x19, 0x05, 0x23, 0x49, 0x2D, 0x03, 0x2E, 0x5C, 0xD2, 0xFF, 0x10, 0x60, 0xFD, 0x20, 0xC0, 0x00), + (0xE8, 0x20, 0x60, 0x04, 0x8E, 0x8E, 0x3E, 0x04, 0xDC, 0xDC, 0x3E, 0x06, 0xFA, 0x26, 0x3E), + (0x75, 0x03, 0x04), + (0xE7, 0x4B, 0x00, 0x00, 0xBE, 0x4B, 0x8C, 0x20, 0x1A, 0xF0, 0x7D, 0x14, 0x7D, 0x14, 0x7D, 0x14, 0x7D, 0x14, 0xFF, 0x00, 0x32, 0x30, 0x73, 0x00, 0x00, 0xC8, 0x6A, 0xFF, 0x5A, 0x64, 0x38, 0x88, 0x15, 0xB1, 0x01, 0x01, 0x64, 0x01, 0x01, 0x7C, 0xFF, 0x1A, 0x51), + (0xE1, 0x0C, 0x0C), + (0xEA, 0x15, 0x00, 0x01), + (0xC8, 0x00, 0x00, 0x04, 0x08, 0x10, 0x00, 0x1F, 0x01, 0x39, 0x3E, 0x00, 0x78, 0x06, 0xE2, 0x02, 0x11, 0x33, 0x01, 0x7A, 0x0D, 0x21, 0xC4, 0x0B, 0x19, 0x08, 0x32, 0xA0, 0x08, 0x1A, 0x0A, 0xF3, 0x7F, 0x0E, 0xC5, 0xE8, 0x03, 0xFF), + (0xC9, 0x00, 0x00, 0x04, 0x08, 0x10, 0x00, 0x1F, 0x01, 0x39, 0x3E, 0x00, 0x78, 0x06, 0xE2, 0x02, 0x11, 0x33, 0x01, 0x7A, 0x0D, 0x21, 0xC4, 0x0B, 0x19, 0x08, 0x32, 0xA0, 0x08, 0x1A, 0x0A, 0xF3, 0x7F, 0x0E, 0xC5, 0xE8, 0x03, 0xFF), + (0x60, 0x71, 0x21, 0x00), + ], +) diff --git a/esphome/components/mipi_dsi/models/seeed.py b/esphome/components/mipi_dsi/models/seeed.py index 290b0e07ee..84593b40e6 100644 --- a/esphome/components/mipi_dsi/models/seeed.py +++ b/esphome/components/mipi_dsi/models/seeed.py @@ -1,9 +1,8 @@ -from esphome.components.mipi import DriverChip -import esphome.config_validation as cv +from . import DsiDriverChip # Standalone display # Product page: https://www.seeedstudio.com/reTerminal-D1001-p-6729.html -DriverChip( +DsiDriverChip( "SEEED-RETERMINAL-D1001", height=1280, width=800, @@ -15,10 +14,10 @@ DriverChip( vsync_front_porch=30, pclk_frequency="80MHz", lane_bit_rate="1.5Gbps", - swap_xy=cv.UNDEFINED, color_order="RGB", enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}], reset_pin={"xl9535": None, "number": 2}, + requires={"psram", "xl9535"}, initsequence=( (0xE0, 0x00), (0xE1, 0x93), diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index c97a0bbd02..a1702fb6a1 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -1,12 +1,11 @@ -from esphome.components.mipi import DriverChip -import esphome.config_validation as cv +from . import DsiDriverChip # fmt: off # Source for parameters and initsequence: # https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365_10_1 # Product page: https://www.waveshare.com/wiki/ESP32-P4-Nano-StartPage -JD9365_10_1_DSI_TOUCH_A = DriverChip( +JD9365_10_1_DSI_TOUCH_A = DsiDriverChip( "WAVESHARE-P4-NANO-10.1", height=1280, width=800, @@ -18,7 +17,6 @@ JD9365_10_1_DSI_TOUCH_A = DriverChip( vsync_front_porch=30, pclk_frequency="80MHz", lane_bit_rate="1.5Gbps", - swap_xy=cv.UNDEFINED, color_order="RGB", initsequence=[ (0xE0, 0x00), # select userpage @@ -65,7 +63,7 @@ JD9365_10_1_DSI_TOUCH_A.extend( # Source for parameters and initsequence: # https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_st7703 # Product page: https://www.waveshare.com/wiki/ESP32-P4-86-Panel-ETH-2RO -DriverChip( +DsiDriverChip( "WAVESHARE-P4-86-PANEL", height=720, width=720, @@ -77,7 +75,6 @@ DriverChip( vsync_front_porch=20, pclk_frequency="38MHz", lane_bit_rate="480Mbps", - swap_xy=cv.UNDEFINED, color_order="RGB", reset_pin=27, initsequence=[ @@ -109,7 +106,7 @@ DriverChip( # Source for parameters and initsequence: # https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_ek79007 # Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-7B -DriverChip( +DsiDriverChip( "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B", height=600, width=1024, @@ -139,7 +136,7 @@ DriverChip( # Source for parameters and initsequence: # https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365 # Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-3.4C -JD9365_3_4_DSI_TOUCH_C = DriverChip( +JD9365_3_4_DSI_TOUCH_C = DsiDriverChip( "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C", height=800, width=800, @@ -151,7 +148,6 @@ JD9365_3_4_DSI_TOUCH_C = DriverChip( vsync_front_porch=24, pclk_frequency="80MHz", lane_bit_rate="1.5Gbps", - swap_xy=cv.UNDEFINED, color_order="RGB", initsequence=[ (0xE0, 0x00), # select userpage @@ -197,7 +193,7 @@ JD9365_3_4_DSI_TOUCH_C.extend( # Source for parameters and initsequence: # https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365 # Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-4C -JD9365_4_DSI_TOUCH_C = DriverChip( +JD9365_4_DSI_TOUCH_C = DsiDriverChip( "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C", height=720, width=720, @@ -209,7 +205,6 @@ JD9365_4_DSI_TOUCH_C = DriverChip( vsync_front_porch=24, pclk_frequency="80MHz", lane_bit_rate="1.5Gbps", - swap_xy=cv.UNDEFINED, color_order="RGB", initsequence=[ (0xE0, 0x00), # select userpage @@ -255,7 +250,7 @@ JD9365_4_DSI_TOUCH_C.extend( # Source for parameters and initsequence: # https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365 # Product page: https://www.waveshare.com/wiki/8-DSI-TOUCH-A -DriverChip( +DsiDriverChip( "WAVESHARE-8-DSI-TOUCH-A", height=1280, width=800, @@ -267,7 +262,6 @@ DriverChip( vsync_front_porch=30, pclk_frequency="80MHz", lane_bit_rate="1.5Gbps", - swap_xy=cv.UNDEFINED, color_order="RGB", initsequence=[ (0xE0, 0x00), # select userpage @@ -304,7 +298,7 @@ DriverChip( # Source for parameters and initsequence: # https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_ili9881c # Product page: https://www.waveshare.com/wiki/7-DSI-TOUCH-A -DriverChip( +DsiDriverChip( "WAVESHARE-7-DSI-TOUCH-A", height=1280, width=720, diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 1eacc31fc5..ebe930d37a 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -18,6 +18,8 @@ from esphome.components.mipi import ( CONF_HSYNC_BACK_PORCH, CONF_HSYNC_FRONT_PORCH, CONF_HSYNC_PULSE_WIDTH, + CONF_PCLK_FREQUENCY, + CONF_PCLK_INVERTED, CONF_PCLK_PIN, CONF_PIXEL_MODE, CONF_USE_AXIS_FLIPS, @@ -34,9 +36,11 @@ from esphome.components.mipi import ( power_of_two, requires_buffer, ) -from esphome.components.rpi_dpi_rgb.display import ( - CONF_PCLK_FREQUENCY, - CONF_PCLK_INVERTED, +from esphome.components.spi import ( + CONF_SPI_MODE, + SPI_DATA_RATE_SCHEMA, + SPI_MODE_OPTIONS, + SPIComponent, ) import esphome.config_validation as cv from esphome.const import ( @@ -48,7 +52,6 @@ from esphome.const import ( CONF_DATA_RATE, CONF_DC_PIN, CONF_DIMENSIONS, - CONF_DISABLED, CONF_ENABLE_PIN, CONF_GREEN, CONF_HSYNC_PIN, @@ -57,8 +60,6 @@ from esphome.const import ( CONF_INIT_SEQUENCE, CONF_INVERT_COLORS, CONF_LAMBDA, - CONF_MIRROR_X, - CONF_MIRROR_Y, CONF_MODEL, CONF_NUMBER, CONF_RED, @@ -72,10 +73,10 @@ from esphome.const import ( ) from esphome.final_validate import full_config -from ..spi import CONF_SPI_MODE, SPI_DATA_RATE_SCHEMA, SPI_MODE_OPTIONS, SPIComponent from . import models +from .models import RgbDriverChip -DEPENDENCIES = ["esp32", "psram"] +DEPENDENCIES = ["esp32"] mipi_rgb_ns = cg.esphome_ns.namespace("mipi_rgb") mipi_rgb = mipi_rgb_ns.class_("MipiRgb", display.Display, cg.Component) @@ -86,7 +87,7 @@ ColorOrder = display.display_ns.enum("ColorMode") DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema -DriverChip("CUSTOM") +RgbDriverChip("CUSTOM") # Import all models dynamically from the models package @@ -120,16 +121,7 @@ def data_pin_set(length): def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] - transform = cv.Any( - cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - **model.swap_xy_schema(), - } - ), - cv.one_of(CONF_DISABLED, lower=True), - ) + transform = model.transform_schema() # RPI model does not use an init sequence, indicates with empty list if model.initsequence is None: # Custom model requires an init sequence @@ -235,6 +227,7 @@ def _config_schema(config): only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]), )(config) model = MODELS[config[CONF_MODEL].upper()] + model.check_requirements() width, height, _offset_width, _offset_height, _pad_width, _pad_height = ( model.get_dimensions(config) ) diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h index dfa8a36e1a..1480004833 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.h +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -98,9 +98,9 @@ class MipiRgb : public display::Display { }; #ifdef USE_SPI -class MipiRgbSpi : public MipiRgb, - public spi::SPIDevice { +class MipiRgbSpi final : public MipiRgb, + public spi::SPIDevice { public: MipiRgbSpi(int width, int height) : MipiRgb(width, height) {} diff --git a/esphome/components/mipi_rgb/models/__init__.py b/esphome/components/mipi_rgb/models/__init__.py new file mode 100644 index 0000000000..9e3fe2a476 --- /dev/null +++ b/esphome/components/mipi_rgb/models/__init__.py @@ -0,0 +1,14 @@ +from esphome.components.mipi import DriverChip +from esphome.const import CONF_SWAP_XY + + +class RgbDriverChip(DriverChip): + """A driver chip for MIPI RGB displays.""" + + @property + def transforms(self) -> set[str]: + """ + Return the set of transformations supported by this driver chip. + RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY + """ + return super().transforms - {CONF_SWAP_XY} diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py index 915b8beda0..c0aaf0a3d2 100644 --- a/esphome/components/mipi_rgb/models/guition.py +++ b/esphome/components/mipi_rgb/models/guition.py @@ -5,6 +5,7 @@ st7701s.extend( width=480, height=480, data_rate="2MHz", + requires={"psram"}, cs_pin=39, de_pin=18, hsync_pin=16, diff --git a/esphome/components/mipi_rgb/models/lilygo.py b/esphome/components/mipi_rgb/models/lilygo.py index c0e91cd8ae..4e0615b439 100644 --- a/esphome/components/mipi_rgb/models/lilygo.py +++ b/esphome/components/mipi_rgb/models/lilygo.py @@ -1,5 +1,3 @@ -from esphome.config_validation import UNDEFINED - from .st7701s import ST7701S # fmt: off @@ -8,10 +6,10 @@ ST7701S( width=480, height=480, invert_colors=False, - swap_xy=UNDEFINED, spi_mode="MODE3", cs_pin={"xl9535": None, "number": 17}, reset_pin={"xl9535": None, "number": 5}, + requires={"psram", "xl9535"}, hsync_pin=39, vsync_pin=40, pclk_pin=41, @@ -57,9 +55,9 @@ t_rgb = ST7701S( height=480, pixel_mode="18bit", invert_colors=False, - swap_xy=UNDEFINED, spi_mode="MODE3", cs_pin={"xl9535": None, "number": 3}, + requires={"psram", "xl9535"}, de_pin=45, hsync_pin=47, vsync_pin=41, diff --git a/esphome/components/mipi_rgb/models/rpi.py b/esphome/components/mipi_rgb/models/rpi.py index 076d96b658..1e2a6600ee 100644 --- a/esphome/components/mipi_rgb/models/rpi.py +++ b/esphome/components/mipi_rgb/models/rpi.py @@ -1,9 +1,7 @@ -from esphome.components.mipi import DriverChip -from esphome.config_validation import UNDEFINED +from . import RgbDriverChip # A driver chip for Raspberry Pi MIPI RGB displays. These require no init sequence -DriverChip( +RgbDriverChip( "RPI", - swap_xy=UNDEFINED, initsequence=(), ) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py index 990a1ca4f3..a20e9d1c01 100644 --- a/esphome/components/mipi_rgb/models/st7701s.py +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -1,17 +1,12 @@ -from esphome.components.mipi import ( - MADCTL, - MADCTL_ML, - MADCTL_XFLIP, - MODE_BGR, - DriverChip, -) -from esphome.config_validation import UNDEFINED +from esphome.components.mipi import MADCTL, MADCTL_ML, MADCTL_XFLIP, MODE_BGR from esphome.const import CONF_COLOR_ORDER, CONF_HEIGHT, CONF_MIRROR_X, CONF_MIRROR_Y +from . import RgbDriverChip + SDIR_CMD = 0xC7 -class ST7701S(DriverChip): +class ST7701S(RgbDriverChip): # The ST7701s does not use the standard MADCTL bits for x/y mirroring def add_madctl(self, sequence: list, config: dict): transform = self.get_transform(config) @@ -45,7 +40,6 @@ st7701s = ST7701S( "ST7701S", width=480, height=864, - swap_xy=UNDEFINED, hsync_front_porch=20, hsync_back_porch=10, hsync_pulse_width=10, @@ -85,6 +79,7 @@ st7701s.extend( height=480, invert_colors=True, pixel_mode="18bit", + requires={"psram"}, cs_pin=1, de_pin={ "number": 45, @@ -117,6 +112,7 @@ st7701s.extend( vsync_pulse_width=8, vsync_back_porch=20, cs_pin={"pca9554": None, "number": 4}, + requires={"psram", "pca9554"}, de_pin=18, hsync_pin=16, vsync_pin=17, @@ -134,6 +130,7 @@ st7701s.extend( width=480, height=480, pixel_mode="18bit", + requires={"psram"}, cs_pin=18, reset_pin=8, de_pin=17, @@ -177,6 +174,7 @@ st7701s.extend( width=480, height=480, pixel_mode="18bit", + requires={"psram"}, cs_pin=21, de_pin=39, vsync_pin=48, diff --git a/esphome/components/mipi_rgb/models/sunton.py b/esphome/components/mipi_rgb/models/sunton.py index a33625dfe4..a87d5f3c38 100644 --- a/esphome/components/mipi_rgb/models/sunton.py +++ b/esphome/components/mipi_rgb/models/sunton.py @@ -1,14 +1,13 @@ -from esphome.components.mipi import DriverChip -from esphome.config_validation import UNDEFINED +from . import RgbDriverChip # fmt: off -sunton = DriverChip( +sunton = RgbDriverChip( "ESP32-8048S070", - swap_xy=UNDEFINED, initsequence=(), width=800, height=480, pclk_frequency="12.5MHz", + requires={"psram"}, de_pin=41, hsync_pin=39, vsync_pin=40, @@ -28,7 +27,6 @@ sunton = DriverChip( sunton.extend( "ESP32-8048S050", - swap_xy=UNDEFINED, initsequence=(), width=800, height=480, diff --git a/esphome/components/mipi_rgb/models/waveshare.py b/esphome/components/mipi_rgb/models/waveshare.py index cd1fc341ef..ef1a5cd2d6 100644 --- a/esphome/components/mipi_rgb/models/waveshare.py +++ b/esphome/components/mipi_rgb/models/waveshare.py @@ -1,18 +1,18 @@ -from esphome.components.mipi import DriverChip, delay -from esphome.config_validation import UNDEFINED +from esphome.components.mipi import delay +from . import RgbDriverChip from .st7701s import st7701s # fmt: off -wave_4_3 = DriverChip( +wave_4_3 = RgbDriverChip( "ESP32-S3-TOUCH-LCD-4.3", - swap_xy=UNDEFINED, initsequence=(), width=800, height=480, pclk_frequency="16MHz", reset_pin={"ch422g": None, "number": 3}, enable_pin={"ch422g": None, "number": 2}, + requires={"psram", "ch422g"}, de_pin=5, hsync_pin={"number": 46, "ignore_strapping_warning": True}, vsync_pin={"number": 3, "ignore_strapping_warning": True}, @@ -69,6 +69,7 @@ st7701s.extend( pclk_pin=41, pclk_frequency="12MHz", pclk_inverted=False, + requires={"psram"}, data_pins={ "red": [46, 3, 8, 18, 17], "green": [14, 13, 12, 11, 10, 9], @@ -80,6 +81,7 @@ st7701s.extend( "WAVESHARE-3.16-320X820", width=320, height=820, + requires={"psram"}, de_pin=40, hsync_pin=38, vsync_pin=39, diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 871736abd1..246db237b1 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -41,14 +41,11 @@ from esphome.const import ( CONF_DATA_RATE, CONF_DC_PIN, CONF_DIMENSIONS, - CONF_DISABLED, CONF_ENABLE_PIN, CONF_ID, CONF_INIT_SEQUENCE, CONF_INVERT_COLORS, CONF_LAMBDA, - CONF_MIRROR_X, - CONF_MIRROR_Y, CONF_MODEL, CONF_RESET_PIN, CONF_ROTATION, @@ -138,16 +135,7 @@ def denominator(config): def model_schema(config): model = MODELS[config[CONF_MODEL]] bus_mode = config[CONF_BUS_MODE] - transform = cv.Any( - cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - **model.swap_xy_schema(), - } - ), - cv.one_of(CONF_DISABLED, lower=True), - ) + transform = model.transform_schema() # CUSTOM model will need to provide a custom init sequence iseqconf = ( cv.Required(CONF_INIT_SEQUENCE) @@ -265,6 +253,7 @@ def customise_schema(config): extra=ALLOW_EXTRA, )(config) model = MODELS[config[CONF_MODEL]] + model.check_requirements() bus_modes = (TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL) config = cv.Schema( { @@ -408,7 +397,7 @@ def get_instance(config): async def to_code(config): model = MODELS[config[CONF_MODEL]] var_id = config[CONF_ID] - init_sequence = model.get_sequence(config, False) + init_sequence = model.get_sequence(config, add_madctl=False, add_reset=True) var_id.type, templateargs = get_instance(config) var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) cg.add(var.set_init_sequence(init_sequence)) diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 48184fa5c1..b269f46dc9 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -13,6 +13,8 @@ constexpr static const char *const TAG = "display.mipi_spi"; // Maximum bytes to log for commands (truncated if larger) static constexpr size_t MIPI_SPI_MAX_CMD_LOG_BYTES = 64; + +// Command codes for MIPI SPI displays. Not all currently used, kept here for reference. static constexpr uint8_t SW_RESET_CMD = 0x01; static constexpr uint8_t SLEEP_OUT = 0x11; static constexpr uint8_t NORON = 0x13; @@ -151,14 +153,11 @@ class MipiSpi : public display::Display, this->reset_pin_->digital_write(false); delay(5); this->reset_pin_->digital_write(true); - } else { - // no reset pin, send software reset command - this->write_command_(SW_RESET_CMD); + // required delay after reset is already in the init sequence, don't duplicate } // need to know when the display is ready for SLPOUT command - will be 120ms after reset auto when = millis() + 120; - delay(10); size_t index = 0; auto &vec = this->init_sequence_; while (index != vec.size()) { @@ -170,6 +169,9 @@ class MipiSpi : public display::Display, uint8_t cmd = vec[index++]; uint8_t x = vec[index++]; if (x == DELAY_FLAG) { + if (cmd == 0) { + cmd = clamp_at_least((int) (when - millis()), 0); + } esph_log_d(TAG, "Delay %dms", cmd); delay(cmd); } else { @@ -179,24 +181,9 @@ class MipiSpi : public display::Display, this->mark_failed(); return; } - switch (cmd) { - case SLEEP_OUT: { - // are we ready, boots? - int duration = when - millis(); - if (duration > 0) { - esph_log_d(TAG, "Sleep %dms", duration); - delay(duration); - } - } break; - - default: - break; - } const auto *ptr = vec.data() + index; this->write_command_(cmd, ptr, num_args); index += num_args; - if (cmd == SLEEP_OUT) - delay(10); } } this->reset_params_(); @@ -398,10 +385,10 @@ class MipiSpi : public display::Display, * @param ptr The pointer to the pixel data * @param w Width of each line in bytes * @param h Height of the buffer in rows - * @param pad Padding in bytes after each line + * @param stride Total length of each line in bytes, including any padding */ - void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) { - if (pad == 0) { + void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { + if (stride == w) { if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { this->write_array(ptr, w * h); } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { @@ -418,7 +405,7 @@ class MipiSpi : public display::Display, } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8); } - ptr += w + pad; + ptr += stride; } } } @@ -436,7 +423,7 @@ class MipiSpi : public display::Display, ptr += y_offset * (x_offset + w + x_pad) + x_offset; if constexpr (BUFFERPIXEL == DISPLAYPIXEL) { this->write_display_data_(reinterpret_cast(ptr), w * sizeof(BUFFERTYPE), h, - x_pad * sizeof(BUFFERTYPE)); + (x_offset + w + x_pad) * sizeof(BUFFERTYPE)); } else { // type conversion required, do it in chunks uint8_t dbuffer[DISPLAYPIXEL * 48]; @@ -472,14 +459,14 @@ class MipiSpi : public display::Display, } // buffer full? Flush. if (dptr == dbuffer + sizeof(dbuffer)) { - this->write_display_data_(dbuffer, sizeof(dbuffer), 1, 0); + this->write_display_data_(dbuffer, sizeof(dbuffer), 1, sizeof(dbuffer)); dptr = dbuffer; } } } // flush any remaining data if (dptr != dbuffer) { - this->write_display_data_(dbuffer, dptr - dbuffer, 1, 0); + this->write_display_data_(dbuffer, dptr - dbuffer, 1, dptr - dbuffer); } } this->disable(); diff --git a/esphome/components/mipi_spi/models/adafruit.py b/esphome/components/mipi_spi/models/adafruit.py index 26790b1493..cc295487eb 100644 --- a/esphome/components/mipi_spi/models/adafruit.py +++ b/esphome/components/mipi_spi/models/adafruit.py @@ -13,6 +13,7 @@ ST7789V.extend( mirror_x=True, mirror_y=True, data_rate="80MHz", + requires={"psram"}, ) ST7789V.extend( @@ -25,4 +26,5 @@ ST7789V.extend( dc_pin=39, reset_pin=40, invert_colors=True, + requires={"psram"}, ) diff --git a/esphome/components/mipi_spi/models/amoled.py b/esphome/components/mipi_spi/models/amoled.py index 32cad70ac0..8a869f2284 100644 --- a/esphome/components/mipi_spi/models/amoled.py +++ b/esphome/components/mipi_spi/models/amoled.py @@ -16,6 +16,7 @@ from esphome.components.mipi import ( delay, ) from esphome.components.spi import TYPE_QUAD +from esphome.const import CONF_MIRROR_X, CONF_MIRROR_Y DriverChip( "T-DISPLAY-S3-AMOLED", @@ -28,6 +29,7 @@ DriverChip( brightness=0xD0, color_order=MODE_RGB, no_slpout=True, # SLPOUT is in the init sequence, early + requires={"psram"}, initsequence=(SLPOUT,), ) @@ -42,6 +44,7 @@ DriverChip( data_rate="40MHz", brightness=0xD0, color_order=MODE_RGB, + requires={"psram"}, initsequence=( (PAGESEL, 4), (0x6A, 0x00), @@ -89,6 +92,7 @@ T4_S3_AMOLED = RM690B0.extend( reset_pin=13, enable_pin=9, bus_mode=TYPE_QUAD, + requires={"psram"}, ) CO5300 = DriverChip( @@ -97,6 +101,9 @@ CO5300 = DriverChip( color_order=MODE_RGB, bus_mode=TYPE_QUAD, no_slpout=True, + transforms={CONF_MIRROR_X, CONF_MIRROR_Y}, + width=480, + height=480, initsequence=( (SLPOUT,), # Requires early SLPOUT (PAGESEL, 0x00), diff --git a/esphome/components/mipi_spi/models/ili.py b/esphome/components/mipi_spi/models/ili.py index 5598a51073..187fcfd8c0 100644 --- a/esphome/components/mipi_spi/models/ili.py +++ b/esphome/components/mipi_spi/models/ili.py @@ -10,7 +10,6 @@ from esphome.components.mipi import ( GMCTR, GMCTRN1, GMCTRP1, - IDMOFF, IFCTR, IFMODE, INVCTR, @@ -23,7 +22,6 @@ from esphome.components.mipi import ( PWCTR5, PWSET, PWSETN, - SETEXTC, VMCTR, VMCTR1, VMCTR2, @@ -32,60 +30,6 @@ from esphome.components.mipi import ( ) from esphome.components.spi import TYPE_OCTAL -DriverChip( - "M5CORE", - width=320, - height=240, - cs_pin=14, - dc_pin=27, - reset_pin=33, - initsequence=( - (SETEXTC, 0xFF, 0x93, 0x42), - (PWCTR1, 0x12, 0x12), - (PWCTR2, 0x03), - (VMCTR1, 0xF2), - (IFMODE, 0xE0), - (0xF6, 0x01, 0x00, 0x00), - ( - GMCTRP1, - 0x00, - 0x0C, - 0x11, - 0x04, - 0x11, - 0x08, - 0x37, - 0x89, - 0x4C, - 0x06, - 0x0C, - 0x0A, - 0x2E, - 0x34, - 0x0F, - ), - ( - GMCTRN1, - 0x00, - 0x0B, - 0x11, - 0x05, - 0x13, - 0x09, - 0x33, - 0x67, - 0x48, - 0x07, - 0x0E, - 0x0B, - 0x2E, - 0x33, - 0x0F, - ), - (DFUNCTR, 0x08, 0x82, 0x1D, 0x04), - (IDMOFF,), - ), -) ILI9341 = DriverChip( "ILI9341", mirror_x=True, @@ -174,22 +118,6 @@ ILI9342 = DriverChip( ), ) -# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation -ILI9341.extend( - "M5CORE2", - # Reset native dimensions due to axis swap. - native_width=320, - native_height=240, - width=320, - height=240, - mirror_x=False, - cs_pin=5, - dc_pin=15, - invert_colors=True, - pixel_mode="18bit", - data_rate="40MHz", -) - DriverChip( "ILI9481", mirror_x=True, @@ -386,6 +314,7 @@ DriverChip( data_rate="40MHz", dc_pin=4, cs_pin=5, + requires={"psram"}, # reset_pin={CONF_INVERTED: True, CONF_NUMBER: 48}, initsequence=( (0xEF, 0x03, 0x80, 0x02), @@ -451,6 +380,7 @@ DriverChip( cs_pin=5, dc_pin=4, reset_pin=48, + requires={"psram"}, initsequence=( (0xEF, 0x03, 0x80, 0x02), (0xCF, 0x00, 0xC1, 0x30), @@ -783,6 +713,7 @@ ST7796.extend( reset_pin=4, dc_pin={"number": 0, "ignore_strapping_warning": True}, invert_colors=True, + requires={"psram"}, ) ST7789V.extend( diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index 854814f572..ca9adb4a72 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -1,14 +1,19 @@ from esphome.components.mipi import MODE_RGB, DriverChip from esphome.components.spi import TYPE_QUAD -import esphome.config_validation as cv -from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER +from esphome.const import ( + CONF_IGNORE_STRAPPING_WARNING, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_NUMBER, +) AXS15231 = DriverChip( "AXS15231", draw_rounding=8, - swap_xy=cv.UNDEFINED, + transforms={CONF_MIRROR_X, CONF_MIRROR_Y}, color_order=MODE_RGB, bus_mode=TYPE_QUAD, + no_swreset=True, initsequence=( (0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5), (0xC1, 0x33), @@ -22,6 +27,7 @@ AXS15231.extend( height=480, cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True}, data_rate="40MHz", + requires={"psram"}, ) DriverChip( @@ -36,6 +42,7 @@ DriverChip( color_order=MODE_RGB, bus_mode=TYPE_QUAD, data_rate="40MHz", + requires={"psram"}, initsequence=( (0xF0, 0x08), (0xF2, 0x08), @@ -267,6 +274,7 @@ DriverChip( color_order=MODE_RGB, bus_mode=TYPE_QUAD, data_rate="40MHz", + requires={"psram"}, initsequence=( (0xF0, 0x28), (0xF2, 0x28), @@ -495,6 +503,7 @@ DriverChip( color_order=MODE_RGB, bus_mode=TYPE_QUAD, data_rate="20MHz", + requires={"psram"}, initsequence=( (0xFF, 0xA5), (0x41, 0x03), diff --git a/esphome/components/mipi_spi/models/lanbon.py b/esphome/components/mipi_spi/models/lanbon.py index 8cec3c8317..1188300136 100644 --- a/esphome/components/mipi_spi/models/lanbon.py +++ b/esphome/components/mipi_spi/models/lanbon.py @@ -10,4 +10,5 @@ ST7789V.extend( cs_pin=22, dc_pin=21, reset_pin=18, + requires={"psram"}, ) diff --git a/esphome/components/mipi_spi/models/lilygo.py b/esphome/components/mipi_spi/models/lilygo.py index 46ec809029..84f44a3dae 100644 --- a/esphome/components/mipi_spi/models/lilygo.py +++ b/esphome/components/mipi_spi/models/lilygo.py @@ -15,6 +15,7 @@ ST7789V.extend( dc_pin=13, reset_pin=9, data_rate="80MHz", + requires={"psram"}, ) ST7789V.extend( @@ -42,6 +43,7 @@ ST7789V.extend( enable_pin=[9, 15], data_rate="10MHz", bus_mode=TYPE_OCTAL, + requires={"psram"}, ) ST7796.extend( @@ -55,4 +57,5 @@ ST7796.extend( dc_pin=9, backlight_pin=48, invert_colors=True, + requires={"psram"}, ) diff --git a/esphome/components/mipi_spi/models/m5stack.py b/esphome/components/mipi_spi/models/m5stack.py new file mode 100644 index 0000000000..a54bd19d88 --- /dev/null +++ b/esphome/components/mipi_spi/models/m5stack.py @@ -0,0 +1,73 @@ +from esphome.components.mipi import ( + DFUNCTR, + GMCTRN1, + GMCTRP1, + IDMOFF, + IFMODE, + PWCTR1, + PWCTR2, + SETEXTC, + VMCTR1, + DriverChip, +) + +from .ili import ILI9341, ST7789V + +# fmt: off +DriverChip( + "M5CORE", + width=320, + height=240, + cs_pin=14, + dc_pin=27, + reset_pin=33, + initsequence=( + (SETEXTC, 0xFF, 0x93, 0x42), + (PWCTR1, 0x12, 0x12), + (PWCTR2, 0x03), + (VMCTR1, 0xF2), + (IFMODE, 0xE0), + (0xF6, 0x01, 0x00, 0x00), + (GMCTRP1, 0x00, 0x0C, 0x11, 0x04, 0x11, 0x08, 0x37, 0x89, 0x4C, 0x06, 0x0C, 0x0A, 0x2E, 0x34, 0x0F,), + (GMCTRN1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x2E, 0x33, 0x0F,), + (DFUNCTR, 0x08, 0x82, 0x1D, 0x04), + (IDMOFF,), + ), +) + +# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation +ILI9341.extend( + "M5CORE2", + # Reset native dimensions due to axis swap. + native_width=320, + native_height=240, + width=320, + height=240, + mirror_x=False, + cs_pin=5, + dc_pin=15, + invert_colors=True, + pixel_mode="18bit", + data_rate="40MHz", + requires={"psram"}, +) + +GC9107 = ST7789V.extend( + "GC9107", + width=128, + height=128, + offset_width=2, + offset_height=1, + pad_width=2, + pad_height=1, +) + +GC9107.extend( + "M5STACK-ATOMS3R-GC9107", + data_rate="40MHz", + invert_colors=True, + reset_pin=48, + dc_pin=42, + cs_pin=14, + requires={"psram"}, +) diff --git a/esphome/components/mipi_spi/models/st77916.py b/esphome/components/mipi_spi/models/st77916.py new file mode 100644 index 0000000000..38852f135f --- /dev/null +++ b/esphome/components/mipi_spi/models/st77916.py @@ -0,0 +1,269 @@ +# Init sequence sourced from Espressif's esp-bsp repository: +# https://github.com/espressif/esp-bsp/blob/master/bsp/esp_vocat/priv_include/disp_init_data.h +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +from esphome.components.mipi import MODE_RGB, DriverChip +from esphome.components.spi import TYPE_QUAD +from esphome.const import CONF_INVERTED, CONF_NUMBER + +# Init sequence for the ST77916 QSPI display on the ESP-VoCat v1.2 board. +# Source: disp_init_data.h from espressif/esp-bsp (bsp/esp_vocat/priv_include). +# The standard INVON/INVOFF and SLPOUT+DISPON commands (and required delays) are omitted because +# the mipi_spi framework appends them automatically based on the invert_colors and no_slpout settings. +# (So this sequence only contains panel-specific setup commands.) +_ESP_VOCAT_INIT = ( + # Page 1a — startup unlock + (0xF0, 0x28), + (0xF2, 0x28), + (0x73, 0xF0), + (0x7C, 0xD1), + (0x83, 0xE0), + (0x84, 0x61), + (0xF2, 0x82), + # Switch to page 0 → page 1 + (0xF0, 0x00), + (0xF0, 0x01), + (0xF1, 0x01), + # Power settings (Bx) + (0xB0, 0x56), + (0xB1, 0x4D), + (0xB2, 0x24), + (0xB4, 0x87), + (0xB5, 0x44), + (0xB6, 0x8B), + (0xB7, 0x40), + (0xB8, 0x86), + (0xBA, 0x00), + (0xBB, 0x08), + (0xBC, 0x08), + (0xBD, 0x00), + # VCOM / gate settings (Cx) + (0xC0, 0x80), + (0xC1, 0x10), + (0xC2, 0x37), + (0xC3, 0x80), + (0xC4, 0x10), + (0xC5, 0x37), + (0xC6, 0xA9), + (0xC7, 0x41), + (0xC8, 0x01), + (0xC9, 0xA9), + (0xCA, 0x41), + (0xCB, 0x01), + # Source settings (Dx) + (0xD0, 0x91), + (0xD1, 0x68), + (0xD2, 0x68), + # Misc + (0xF5, 0x00, 0xA5), + (0xDD, 0x4F), + (0xDE, 0x4F), + (0xF1, 0x10), + (0xF0, 0x00), + # Switch to page 2 — gamma + (0xF0, 0x02), + ( + 0xE0, + 0xF0, + 0x0A, + 0x10, + 0x09, + 0x09, + 0x36, + 0x35, + 0x33, + 0x4A, + 0x29, + 0x15, + 0x15, + 0x2E, + 0x34, + ), + ( + 0xE1, + 0xF0, + 0x0A, + 0x0F, + 0x08, + 0x08, + 0x05, + 0x34, + 0x33, + 0x4A, + 0x39, + 0x15, + 0x15, + 0x2D, + 0x33, + ), + # Switch to page 10 — GIP / timing + (0xF0, 0x10), + (0xF3, 0x10), + # Page 10: Exxx + (0xE0, 0x07), + (0xE1, 0x00), + (0xE2, 0x00), + (0xE3, 0x00), + (0xE4, 0xE0), + (0xE5, 0x06), + (0xE6, 0x21), + (0xE7, 0x01), + (0xE8, 0x05), + (0xE9, 0x02), + (0xEA, 0xDA), + (0xEB, 0x00), + (0xEC, 0x00), + (0xED, 0x0F), + (0xEE, 0x00), + (0xEF, 0x00), + # Page 10: Fxxx + (0xF8, 0x00), + (0xF9, 0x00), + (0xFA, 0x00), + (0xFB, 0x00), + (0xFC, 0x00), + (0xFD, 0x00), + (0xFE, 0x00), + (0xFF, 0x00), + # GIP section A (0x60–0x6B) + (0x60, 0x40), + (0x61, 0x04), + (0x62, 0x00), + (0x63, 0x42), + (0x64, 0xD9), + (0x65, 0x00), + (0x66, 0x00), + (0x67, 0x00), + (0x68, 0x00), + (0x69, 0x00), + (0x6A, 0x00), + (0x6B, 0x00), + # GIP section B (0x70–0x7B) + (0x70, 0x40), + (0x71, 0x03), + (0x72, 0x00), + (0x73, 0x42), + (0x74, 0xD8), + (0x75, 0x00), + (0x76, 0x00), + (0x77, 0x00), + (0x78, 0x00), + (0x79, 0x00), + (0x7A, 0x00), + (0x7B, 0x00), + # GIP timing (0x80–0x9F) + (0x80, 0x48), + (0x81, 0x00), + (0x82, 0x06), + (0x83, 0x02), + (0x84, 0xD6), + (0x85, 0x04), + (0x86, 0x00), + (0x87, 0x00), + (0x88, 0x48), + (0x89, 0x00), + (0x8A, 0x08), + (0x8B, 0x02), + (0x8C, 0xD8), + (0x8D, 0x04), + (0x8E, 0x00), + (0x8F, 0x00), + (0x90, 0x48), + (0x91, 0x00), + (0x92, 0x0A), + (0x93, 0x02), + (0x94, 0xDA), + (0x95, 0x04), + (0x96, 0x00), + (0x97, 0x00), + (0x98, 0x48), + (0x99, 0x00), + (0x9A, 0x0C), + (0x9B, 0x02), + (0x9C, 0xDC), + (0x9D, 0x04), + (0x9E, 0x00), + (0x9F, 0x00), + # GIP timing (0xA0–0xBF) + (0xA0, 0x48), + (0xA1, 0x00), + (0xA2, 0x05), + (0xA3, 0x02), + (0xA4, 0xD5), + (0xA5, 0x04), + (0xA6, 0x00), + (0xA7, 0x00), + (0xA8, 0x48), + (0xA9, 0x00), + (0xAA, 0x07), + (0xAB, 0x02), + (0xAC, 0xD7), + (0xAD, 0x04), + (0xAE, 0x00), + (0xAF, 0x00), + (0xB0, 0x48), + (0xB1, 0x00), + (0xB2, 0x09), + (0xB3, 0x02), + (0xB4, 0xD9), + (0xB5, 0x04), + (0xB6, 0x00), + (0xB7, 0x00), + (0xB8, 0x48), + (0xB9, 0x00), + (0xBA, 0x0B), + (0xBB, 0x02), + (0xBC, 0xDB), + (0xBD, 0x04), + (0xBE, 0x00), + (0xBF, 0x00), + # Source timing (0xC0–0xC9) + (0xC0, 0x10), + (0xC1, 0x47), + (0xC2, 0x56), + (0xC3, 0x65), + (0xC4, 0x74), + (0xC5, 0x88), + (0xC6, 0x99), + (0xC7, 0x01), + (0xC8, 0xBB), + (0xC9, 0xAA), + # Source timing (0xD0–0xD9) + (0xD0, 0x10), + (0xD1, 0x47), + (0xD2, 0x56), + (0xD3, 0x65), + (0xD4, 0x74), + (0xD5, 0x88), + (0xD6, 0x99), + (0xD7, 0x01), + (0xD8, 0xBB), + (0xD9, 0xAA), + # Finalise page 10, return to page 0 + (0xF3, 0x01), + (0xF0, 0x00), + # INVON (0x21) and SLPOUT (0x11) are appended by the framework. +) + +DriverChip( + "ESP-VOCAT", + width=360, + height=360, + # SPI pins for the ESP-VoCat v1.2 board. + # PCLK (GPIO18) and data pins (GPIO46/13/11/12) are configured on the spi: bus. + cs_pin=14, + # RST is active-HIGH on this panel; invert the ESPHome pin so the framework's + # active-low reset pulse (HIGH→LOW→HIGH) maps to the correct physical sequence + # (LOW→HIGH→LOW) on the wire. + reset_pin={CONF_NUMBER: 47, CONF_INVERTED: True}, + # Note: GPIO9 behaviour varies by board revision (may be POWER_CTRL, not LCD_EN). + # Do not set a default enable_pin — manage LCD power in on_boot if needed. + # GPIO44 is the backlight; manage it separately via an output: or light:. + bus_mode=TYPE_QUAD, + data_rate="80MHz", + invert_colors=True, + color_order=MODE_RGB, + requires={"psram"}, + initsequence=_ESP_VOCAT_INIT, +) diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index 3c719b0f5e..bdd0c3c90b 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -12,7 +12,7 @@ from esphome.components.mipi import ( PWSET, DriverChip, ) -import esphome.config_validation as cv +from esphome.const import CONF_MIRROR_X, CONF_MIRROR_Y from .amoled import CO5300 from .ili import ILI9488_A, ST7789V @@ -155,7 +155,7 @@ ST7789P = DriverChip( ILI9488_A.extend( "PICO-RESTOUCH-LCD-3.5", - swap_xy=cv.UNDEFINED, + transforms={CONF_MIRROR_X, CONF_MIRROR_Y}, spi_16=True, pixel_mode="16bit", mirror_x=True, @@ -175,6 +175,7 @@ CO5300.extend( offset_width=6, cs_pin=12, reset_pin=39, + requires={"psram"}, ) # Waveshare ESP32-S3 Touch AMOLED 2.16" (CO5300 controller) @@ -189,6 +190,7 @@ CO5300.extend( cs_pin=12, reset_pin=39, data_rate="40MHz", + requires={"psram"}, ) AXS15231.extend( @@ -198,8 +200,57 @@ AXS15231.extend( data_rate="80MHz", cs_pin=9, reset_pin=21, + requires={"psram"}, ) +# Init sequence and pins taken from the vendor demo: +# "ESP32-S3-Touch-LCD-3.5B-Demo/Arduino/examples/09_lvgl_arduino_v8" +# The panel has no reset line (demo passes RST = -1); the sleep-out, display-on +# and first RAM write are issued by the framework, so they are omitted here. +# fmt: off +AXS15231.extend( + "WAVESHARE-ESP32-S3-TOUCH-LCD-3.5B", + width=320, + height=480, + # Vendor demo runs the AXS15231B at 32MHz; the ESP32 SPI clock cannot hit + # that exactly, data sheet says max 50MHz, so use 40MHz (proven on the same controller, JC3248W535) + data_rate="40MHz", + cs_pin=12, + requires={"psram"}, + initsequence=( + (0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5), + (0xA0, 0xC0, 0x10, 0x00, 0x02, 0x00, 0x00, 0x04, 0x3F, 0x20, 0x05, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xA2, 0x30, 0x3C, 0x24, 0x14, 0xD0, 0x20, 0xFF, 0xE0, 0x40, 0x19, 0x80, 0x80, 0x80, 0x20, 0xF9, 0x10, 0x02, 0xFF, 0xFF, 0xF0, 0x90, 0x01, 0x32, 0xA0, 0x91, 0xE0, 0x20, 0x7F, 0xFF, 0x00, 0x5A), + (0xD0, 0xE0, 0x40, 0x51, 0x24, 0x08, 0x05, 0x10, 0x01, 0x20, 0x15, 0xC2, 0x42, 0x22, 0x22, 0xAA, 0x03, 0x10, 0x12, 0x60, 0x14, 0x1E, 0x51, 0x15, 0x00, 0x8A, 0x20, 0x00, 0x03, 0x3A, 0x12), + (0xA3, 0xA0, 0x06, 0xAA, 0x00, 0x08, 0x02, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x55, 0x55), + (0xC1, 0x31, 0x04, 0x02, 0x02, 0x71, 0x05, 0x24, 0x55, 0x02, 0x00, 0x41, 0x00, 0x53, 0xFF, 0xFF, 0xFF, 0x4F, 0x52, 0x00, 0x4F, 0x52, 0x00, 0x45, 0x3B, 0x0B, 0x02, 0x0D, 0x00, 0xFF, 0x40), + (0xC3, 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01), + (0xC4, 0x00, 0x24, 0x33, 0x80, 0x00, 0xEA, 0x64, 0x32, 0xC8, 0x64, 0xC8, 0x32, 0x90, 0x90, 0x11, 0x06, 0xDC, 0xFA, 0x00, 0x00, 0x80, 0xFE, 0x10, 0x10, 0x00, 0x0A, 0x0A, 0x44, 0x50), + (0xC5, 0x18, 0x00, 0x00, 0x03, 0xFE, 0x3A, 0x4A, 0x20, 0x30, 0x10, 0x88, 0xDE, 0x0D, 0x08, 0x0F, 0x0F, 0x01, 0x3A, 0x4A, 0x20, 0x10, 0x10, 0x00), + (0xC6, 0x05, 0x0A, 0x05, 0x0A, 0x00, 0xE0, 0x2E, 0x0B, 0x12, 0x22, 0x12, 0x22, 0x01, 0x03, 0x00, 0x3F, 0x6A, 0x18, 0xC8, 0x22), + (0xC7, 0x50, 0x32, 0x28, 0x00, 0xA2, 0x80, 0x8F, 0x00, 0x80, 0xFF, 0x07, 0x11, 0x9C, 0x67, 0xFF, 0x24, 0x0C, 0x0D, 0x0E, 0x0F), + (0xC9, 0x33, 0x44, 0x44, 0x01), + (0xCF, 0x2C, 0x1E, 0x88, 0x58, 0x13, 0x18, 0x56, 0x18, 0x1E, 0x68, 0x88, 0x00, 0x65, 0x09, 0x22, 0xC4, 0x0C, 0x77, 0x22, 0x44, 0xAA, 0x55, 0x08, 0x08, 0x12, 0xA0, 0x08), + (0xD5, 0x40, 0x8E, 0x8D, 0x01, 0x35, 0x04, 0x92, 0x74, 0x04, 0x92, 0x74, 0x04, 0x08, 0x6A, 0x04, 0x46, 0x03, 0x03, 0x03, 0x03, 0x82, 0x01, 0x03, 0x00, 0xE0, 0x51, 0xA1, 0x00, 0x00, 0x00), + (0xD6, 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x93, 0x00, 0x01, 0x83, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0x84, 0x00, 0x20, 0x01, 0x00), + (0xD7, 0x03, 0x01, 0x0B, 0x09, 0x0F, 0x0D, 0x1E, 0x1F, 0x18, 0x1D, 0x1F, 0x19, 0x40, 0x8E, 0x04, 0x00, 0x20, 0xA0, 0x1F), + (0xD8, 0x02, 0x00, 0x0A, 0x08, 0x0E, 0x0C, 0x1E, 0x1F, 0x18, 0x1D, 0x1F, 0x19), + (0xD9, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F), + (0xDD, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F), + (0xDF, 0x44, 0x73, 0x4B, 0x69, 0x00, 0x0A, 0x02, 0x90), + (0xE0, 0x3B, 0x28, 0x10, 0x16, 0x0C, 0x06, 0x11, 0x28, 0x5C, 0x21, 0x0D, 0x35, 0x13, 0x2C, 0x33, 0x28, 0x0D), + (0xE1, 0x37, 0x28, 0x10, 0x16, 0x0B, 0x06, 0x11, 0x28, 0x5C, 0x21, 0x0D, 0x35, 0x14, 0x2C, 0x33, 0x28, 0x0F), + (0xE2, 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D), + (0xE3, 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x35, 0x44, 0x32, 0x0C, 0x14, 0x14, 0x36, 0x32, 0x2F, 0x0F), + (0xE4, 0x3B, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0D), + (0xE5, 0x37, 0x07, 0x12, 0x18, 0x0E, 0x0D, 0x17, 0x39, 0x44, 0x2E, 0x0C, 0x14, 0x14, 0x36, 0x3A, 0x2F, 0x0F), + (0xA4, 0x85, 0x85, 0x95, 0x82, 0xAF, 0xAA, 0xAA, 0x80, 0x10, 0x30, 0x40, 0x40, 0x20, 0xFF, 0x60, 0x30), + (0xA4, 0x85, 0x85, 0x95, 0x85), + (0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + ), +) +# fmt: on + # Waveshare 1.83-v2 # # Do not use on 1.83-v1: Vendor warning on different chip! @@ -281,4 +332,16 @@ ST7789V.extend( offset_height=40, invert_colors=True, data_rate="40MHz", + requires={"psram"}, +) + +CO5300.extend( + "WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.64", + width=280, + height=456, + offset_width=20, + cs_pin=9, + reset_pin=21, + enable_pin=1, + requires={"psram"}, ) diff --git a/esphome/components/mitsubishi/mitsubishi.h b/esphome/components/mitsubishi/mitsubishi.h index 769390ce3a..7925b7ce44 100644 --- a/esphome/components/mitsubishi/mitsubishi.h +++ b/esphome/components/mitsubishi/mitsubishi.h @@ -38,7 +38,7 @@ enum VerticalDirection { VERTICAL_DIRECTION_DOWN = 0x28, }; -class MitsubishiClimate : public climate_ir::ClimateIR { +class MitsubishiClimate final : public climate_ir::ClimateIR { public: MitsubishiClimate() : climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX, 1.0f, true, true, diff --git a/esphome/components/mixer/speaker/automation.h b/esphome/components/mixer/speaker/automation.h index cdfda0c700..ea51b6b889 100644 --- a/esphome/components/mixer/speaker/automation.h +++ b/esphome/components/mixer/speaker/automation.h @@ -6,7 +6,7 @@ #ifdef USE_ESP32 namespace esphome::mixer_speaker { -template class DuckingApplyAction : public Action, public Parented { +template class DuckingApplyAction final : public Action, public Parented { TEMPLATABLE_VALUE(uint8_t, decibel_reduction); TEMPLATABLE_VALUE(uint32_t, duration); void play(const Ts &...x) override { diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index f1ae919b50..00e89d1782 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -44,7 +44,7 @@ namespace esphome::mixer_speaker { class MixerSpeaker; -class SourceSpeaker : public speaker::Speaker, public Component { +class SourceSpeaker final : public speaker::Speaker, public Component { public: void dump_config() override; void setup() override; @@ -118,7 +118,7 @@ class SourceSpeaker : public speaker::Speaker, public Component { uint32_t stopping_start_ms_{0}; }; -class MixerSpeaker : public Component { +class MixerSpeaker final : public Component { public: void dump_config() override; void setup() override; diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index 7048302124..2288e8ff84 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -1,3 +1,5 @@ +#ifndef USE_BK72XX + #include "sensor_mlx90393.h" #include "esphome/core/log.h" @@ -270,3 +272,5 @@ void MLX90393Cls::verify_settings_timeout_(MLX90393Setting stage) { } } // namespace esphome::mlx90393 + +#endif // USE_BK72XX diff --git a/esphome/components/mlx90393/sensor_mlx90393.h b/esphome/components/mlx90393/sensor_mlx90393.h index 28053216e2..03e78f51cc 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.h +++ b/esphome/components/mlx90393/sensor_mlx90393.h @@ -1,5 +1,7 @@ #pragma once +#ifndef USE_BK72XX + #include #include #include "esphome/components/i2c/i2c.h" @@ -20,7 +22,7 @@ enum MLX90393Setting { MLX90393_LAST, }; -class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90393Hal { +class MLX90393Cls final : public PollingComponent, public i2c::I2CDevice, public MLX90393Hal { public: void setup() override; void dump_config() override; @@ -76,3 +78,5 @@ class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90 }; } // namespace esphome::mlx90393 + +#endif // USE_BK72XX diff --git a/esphome/components/mlx90614/mlx90614.h b/esphome/components/mlx90614/mlx90614.h index 12081f20ac..882ee45186 100644 --- a/esphome/components/mlx90614/mlx90614.h +++ b/esphome/components/mlx90614/mlx90614.h @@ -6,7 +6,7 @@ namespace esphome::mlx90614 { -class MLX90614Component : public PollingComponent, public i2c::I2CDevice { +class MLX90614Component final : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/mmc5603/mmc5603.h b/esphome/components/mmc5603/mmc5603.h index 0d8eb152a7..d291e6d272 100644 --- a/esphome/components/mmc5603/mmc5603.h +++ b/esphome/components/mmc5603/mmc5603.h @@ -12,7 +12,7 @@ enum MMC5603Datarate { MMC5603_DATARATE_255_0_HZ, }; -class MMC5603Component : public PollingComponent, public i2c::I2CDevice { +class MMC5603Component final : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/mmc5983/mmc5983.h b/esphome/components/mmc5983/mmc5983.h index 020d3b2e4c..3ab9e86dcd 100644 --- a/esphome/components/mmc5983/mmc5983.h +++ b/esphome/components/mmc5983/mmc5983.h @@ -6,7 +6,7 @@ namespace esphome::mmc5983 { -class MMC5983Component : public PollingComponent, public i2c::I2CDevice { +class MMC5983Component final : public PollingComponent, public i2c::I2CDevice { public: void update() override; void setup() override; diff --git a/esphome/components/modbus/helpers.py b/esphome/components/modbus/helpers.py index 6f97f1e605..e3029b2648 100644 --- a/esphome/components/modbus/helpers.py +++ b/esphome/components/modbus/helpers.py @@ -3,33 +3,34 @@ import esphome.codegen as cg modbus_ns = cg.esphome_ns.namespace("modbus") modbus_helpers_ns = modbus_ns.namespace("helpers") -ModbusFunctionCode_ns = modbus_ns.namespace("ModbusFunctionCode") -ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") +FunctionCode_ns = modbus_ns.namespace("FunctionCode") +FunctionCode = FunctionCode_ns.enum("FunctionCode") MODBUS_FUNCTION_CODE = { - "read_coils": ModbusFunctionCode.READ_COILS, - "read_discrete_inputs": ModbusFunctionCode.READ_DISCRETE_INPUTS, - "read_holding_registers": ModbusFunctionCode.READ_HOLDING_REGISTERS, - "read_input_registers": ModbusFunctionCode.READ_INPUT_REGISTERS, - "write_single_coil": ModbusFunctionCode.WRITE_SINGLE_COIL, - "write_single_register": ModbusFunctionCode.WRITE_SINGLE_REGISTER, - "write_multiple_coils": ModbusFunctionCode.WRITE_MULTIPLE_COILS, - "write_multiple_registers": ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, + "read_coils": FunctionCode.READ_COILS, + "read_discrete_inputs": FunctionCode.READ_DISCRETE_INPUTS, + "read_holding_registers": FunctionCode.READ_HOLDING_REGISTERS, + "read_input_registers": FunctionCode.READ_INPUT_REGISTERS, + "write_single_coil": FunctionCode.WRITE_SINGLE_COIL, + "write_single_register": FunctionCode.WRITE_SINGLE_REGISTER, + "write_multiple_coils": FunctionCode.WRITE_MULTIPLE_COILS, + "write_multiple_registers": FunctionCode.WRITE_MULTIPLE_REGISTERS, } -ModbusRegisterType_ns = modbus_ns.namespace("ModbusRegisterType") -ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") +EntityType_ns = modbus_ns.namespace("EntityType") +EntityType = EntityType_ns.enum("EntityType") MODBUS_WRITE_REGISTER_TYPE = { - "custom": ModbusRegisterType.CUSTOM, - "coil": ModbusRegisterType.COIL, - "holding": ModbusRegisterType.HOLDING, + "custom": EntityType.CUSTOM, + "coil": EntityType.COIL, + "holding": EntityType.HOLDING, } MODBUS_REGISTER_TYPE = { **MODBUS_WRITE_REGISTER_TYPE, - "discrete_input": ModbusRegisterType.DISCRETE_INPUT, - "read": ModbusRegisterType.READ, + "discrete_input": EntityType.DISCRETE_INPUT, + "read": EntityType.INPUT_REGISTER, + "input": EntityType.INPUT_REGISTER, } SensorValueType_ns = modbus_helpers_ns.namespace("SensorValueType") diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 488bcf1459..57371b9e79 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -45,27 +45,46 @@ void Modbus::loop() { } void ModbusClientHub::loop() { - // Call base class to receive bytes and parse frames - this->Modbus::loop(); + // Drain anything owed since the last loop (e.g. an external clear) before the watchdog runs, so it + // never times out an entry whose pending count has not been drained. No-op when nothing is owed. + this->sweep_(); - // If we're past the send_wait_time timeout and response buffer doesn't have the start of the expected response - if (this->waiting_for_response_.has_value()) { - ModbusDeviceCommand &wfr = this->waiting_for_response_.value(); - uint8_t expected_address = wfr.frame.data.get()[0]; - if (this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_ && - (this->rx_buffer_.empty() || this->rx_buffer_[0] != expected_address)) { - ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", expected_address, - this->last_receive_check_ - this->last_send_); - if (wfr.device) - wfr.device->on_modbus_no_response(); - this->waiting_for_response_.reset(); - } + this->Modbus::loop(); // receive bytes and parse frames + + // Send-wait watchdog: only the cheap time check runs at loop rate; expire_waiting_() looks the + // entry up and holds off if the response has started arriving. + if (this->waiting_for_response_ && + this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_) { + this->expire_waiting_(); } - // If there's no response pending and there's commands in the buffer + this->sweep_(); // deliver owed callbacks with the hub quiescent this->send_next_frame_(); } +void ModbusClientHub::expire_waiting_() { + ModbusDeviceCommand *cmd = this->find_waiting_(); + if (cmd == nullptr) { + this->waiting_for_response_ = false; + return; + } + if (!this->rx_buffer_.empty() && this->rx_buffer_[0] == cmd->frame.address()) { + // The start of the response is in the buffer: let the frame finish arriving. + return; + } + // Only a genuine WAITING entry warrants the log (a cleared or interrupted shell timing out is expected). + if (cmd->state == FrameState::WAITING) { + ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", cmd->frame.address(), + this->last_receive_check_ - this->last_send_); + } + // Deliver on_no_response directly, the way the parse path delivers response()/error(): the entry + // lands in TIMED_OUT and the following sweep reschedules a retry or erases it. Free the + // wire first so a resend from inside the callback sees it available. + this->waiting_for_response_ = false; + this->sweep_needed_ = true; + cmd->timed_out(); +} + bool Modbus::timeout_() { // If the response frame is finished (including interframe delay) - we timeout. // The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts @@ -111,12 +130,21 @@ bool Modbus::tx_blocked() { bool ModbusClientHub::tx_blocked() { // We block transmission in any of these case: - // 1. We're waiting for a response + // 1. We're waiting for a response (a waiting entry: WAITING/INTERRUPTED/WAITING_RETIRED/INTERRUPTED_RETIRED) // 2. Any of the base class tx_blocked conditions - return (this->waiting_for_response_.has_value()) || this->Modbus::tx_blocked(); + return this->waiting_for_response_ || this->Modbus::tx_blocked(); } -bool ModbusClientHub::tx_buffer_empty() { return this->tx_buffer_.empty(); } +bool ModbusClientHub::tx_buffer_empty() { + // "Empty" for ready_for_immediate_send(): no one-shot is queued ahead of the caller. Entries in + // other states are mid-transaction or owed bookkeeping, not queued sends - and a READY continuous + // poll does not count either, since it ranks below every one-shot, so a new send goes out first. + for (const auto &cmd : this->tx_buffer_) { + if (cmd.state == FrameState::READY && !cmd.continuous) + return false; + } + return true; +} void Modbus::receive_bytes_() { this->last_receive_check_ = millis(); @@ -215,11 +243,10 @@ bool Modbus::parse_modbus_server_frame_() { // Process before clearing: process_modbus_server_frame (receiving a response or peer message) never sends a reply // synchronously. We can safely point directly into rx_buffer_ and avoid a copy. - uint8_t data_offset = helpers::server_frame_data_offset(this->rx_buffer_.data(), this->rx_buffer_.size()); - const uint8_t *data = this->rx_buffer_.data() + data_offset; - uint16_t data_len = frame_length - 2 - data_offset; + // The PDU is the frame without the leading address and the trailing CRC. + std::span pdu(this->rx_buffer_.data() + 1, frame_length - 3); - this->process_modbus_server_frame(address, function_code, data, data_len); + this->process_modbus_server_frame(address, pdu); this->clear_rx_buffer_(LOG_STR("parse succeeded"), false, frame_length); return true; @@ -259,64 +286,61 @@ bool ModbusServerHub::parse_modbus_client_frame_() { return true; } -void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, - uint16_t len) { - if (!this->waiting_for_response_.has_value()) { +// The parser (parse_modbus_server_frame_) guarantees the bounds relied on here: pdu is never empty, +// and an exception-flagged pdu is at least 2 bytes. Keep that in mind when changing server_pdu_length(). +void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span pdu) { + const uint8_t function_code = pdu[0]; + ModbusDeviceCommand *cmd = this->waiting_for_response_ ? this->find_waiting_() : nullptr; + if (cmd == nullptr) { ESP_LOGW(TAG, "Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "ms after last send", address, function_code, this->last_modbus_byte_ - this->last_send_); return; - } else { // We are waiting for a response - // Check if the response matches the expected address and function code + } - ModbusDeviceCommand &wfr = this->waiting_for_response_.value(); - uint8_t expected_address = wfr.frame.data.get()[0]; - uint8_t expected_function_code = wfr.frame.data.get()[1]; - if (expected_address != address || expected_function_code != (function_code & FUNCTION_CODE_MASK)) { - ESP_LOGW(TAG, - "Received incorrect frame address %" PRIu8 " <> %" PRIu8 " or function code 0x%X <> 0x%X, %" PRIu32 - "ms after last send", - address, expected_address, (function_code & FUNCTION_CODE_MASK), expected_function_code, - this->last_modbus_byte_ - this->last_send_); - // Invalidate the waiting device so it won't process this response. - if (wfr.device) - wfr.device->on_modbus_no_response(); - wfr.interrupted = true; - wfr.device = nullptr; - return; - } + // Check if the response matches the expected address and function code + const uint8_t expected_address = cmd->frame.address(); + const uint8_t expected_function_code = cmd->frame.pdu()[0]; + if (expected_address != address || expected_function_code != (function_code & FUNCTION_CODE_MASK)) { + ESP_LOGW(TAG, + "Received incorrect frame address %" PRIu8 " <> %" PRIu8 " or function code 0x%X <> 0x%X, %" PRIu32 + "ms after last send", + address, expected_address, (function_code & FUNCTION_CODE_MASK), expected_function_code, + this->last_modbus_byte_ - this->last_send_); + // Unexpected frame: flip a WAITING entry to an INTERRUPTED shell that ignores the rest of this + // transaction and blocks tx until the send-wait timeout, where it gets its on_no_response. + cmd->interrupt(); + return; + } - if (wfr.interrupted) { - ESP_LOGW(TAG, - "Ignoring response from %" PRIu8 " - transmission interrupted by previous unexpected response, %" PRIu32 - "ms after last send", - address, this->last_modbus_byte_ - this->last_send_); - return; - } else { // We have a valid device waiting for this response + if (cmd->state == FrameState::INTERRUPTED || cmd->state == FrameState::INTERRUPTED_RETIRED) { + // An interrupted shell keeps blocking until the send-wait timeout; a late response for it is + // ignored and does NOT free the wire. The distrust survives a clear (INTERRUPTED_RETIRED), so a + // cleared-interrupted frame still ends in on_no_response rather than delivering a late response. + ESP_LOGW(TAG, + "Ignoring response from %" PRIu8 " - transmission interrupted by previous unexpected response, %" PRIu32 + "ms after last send", + address, this->last_modbus_byte_ - this->last_send_); + return; + } - ModbusClientDevice *device = wfr.device; - this->waiting_for_response_.reset(); - // Is it an error response? - if (helpers::is_function_code_exception(function_code)) { - uint8_t exception = len > 0 ? data[0] : 0; - ESP_LOGW(TAG, - "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "ms after last send", - function_code, exception, address, this->last_modbus_byte_ - this->last_send_); - if (device) - device->on_modbus_error(function_code & FUNCTION_CODE_MASK, exception); - - } else if (device) { // Not an error response - // on_modbus_data is existing public API taking const std::vector& - device->on_modbus_data(std::vector(data, data + len)); - } else { // Not an error response, but no device to respond to - ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "ms after last send", - address, this->last_modbus_byte_ - this->last_send_); - } - } + // Deliver at parse time so the response span can point into the rx buffer (zero copy). error()/ + // response() set the state and consume the request BEFORE the callback, so a clear from inside it + // ("stop polling now") wins. A device-less shell runs no callback and the sweep erases it. + this->waiting_for_response_ = false; + this->sweep_needed_ = true; + if (helpers::is_function_code_exception(function_code)) { + uint8_t exception = pdu[1]; // exception frames are fixed-length, so the code is always present + ESP_LOGW(TAG, "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "ms after last send", + function_code, exception, address, this->last_modbus_byte_ - this->last_send_); + cmd->error(static_cast(exception)); + } else if (!cmd->response(pdu)) { + ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "ms after last send", address, + this->last_modbus_byte_ - this->last_send_); } } -void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *, uint16_t) { +void ModbusServerHub::process_modbus_server_frame(uint8_t address, std::span) { if (this->find_device_(address) != nullptr) { ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address); } @@ -346,7 +370,7 @@ bool ModbusServerHub::check_register_range_(uint8_t address, uint8_t function_co if ((uint32_t) start_address + number_of_registers > 0x10000u) { ESP_LOGW(TAG, "Register address out of range - start: %" PRIu16 " num: %" PRIu16, start_address, number_of_registers); - this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + this->send_exception_(address, function_code, ExceptionCode::ILLEGAL_DATA_ADDRESS); return false; } return true; @@ -360,30 +384,30 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func return; } - ServerResponseStatus status; + ResponseStatus status; uint8_t response_buffer[modbus::MAX_RAW_SIZE]; const uint8_t *response_data = response_buffer; uint16_t response_len = 0; - switch (static_cast(function_code)) { - case ModbusFunctionCode::READ_HOLDING_REGISTERS: - case ModbusFunctionCode::READ_INPUT_REGISTERS: { + switch (static_cast(function_code)) { + case FunctionCode::READ_HOLDING_REGISTERS: + case FunctionCode::READ_INPUT_REGISTERS: { // PDU data: start address(2) + quantity(2). uint16_t start_address = helpers::get_data(data, 0); uint16_t number_of_registers = helpers::get_data(data, 2); if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_READ) { ESP_LOGW(TAG, "Invalid number of registers %" PRIu16, number_of_registers); - this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + this->send_exception_(address, function_code, ExceptionCode::ILLEGAL_DATA_VALUE); return; } if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) { return; } RegisterValues registers; - if (static_cast(function_code) == ModbusFunctionCode::READ_HOLDING_REGISTERS) { - status = device->on_modbus_read_holding_registers(start_address, number_of_registers, registers); + if (static_cast(function_code) == FunctionCode::READ_HOLDING_REGISTERS) { + status = device->on_read_holding_registers(start_address, number_of_registers, registers); } else { - status = device->on_modbus_read_input_registers(start_address, number_of_registers, registers); + status = device->on_read_input_registers(start_address, number_of_registers, registers); } // A handler that returns an exception leaves registers partially filled, so check the exception @@ -395,7 +419,7 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func if (registers.size() != number_of_registers) { ESP_LOGE(TAG, "Incorrect response %" PRIu16 " requested, %zu returned", number_of_registers, registers.size()); - this->send_exception_(address, function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE); + this->send_exception_(address, function_code, ExceptionCode::SERVICE_DEVICE_FAILURE); return; } @@ -407,8 +431,8 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func } break; } - case ModbusFunctionCode::WRITE_SINGLE_REGISTER: - case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS: { + case FunctionCode::WRITE_SINGLE_REGISTER: + case FunctionCode::WRITE_MULTIPLE_REGISTERS: { // PDU data: start address(2) [+ quantity(2) + byte count(1)] + register values. // A single-register write always targets one register; for a multiple-register write the // quantity is in the frame and its byte count must equal quantity * 2. The register values are @@ -416,7 +440,7 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func uint16_t start_address = helpers::get_data(data, 0); uint16_t number_of_registers = 1; uint16_t values_offset = 2; // single write: values follow the 2-byte start address - if (static_cast(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { + if (static_cast(function_code) == FunctionCode::WRITE_MULTIPLE_REGISTERS) { number_of_registers = helpers::get_data(data, 2); uint8_t number_of_bytes = helpers::get_data(data, 4); values_offset = 5; // multiple write: values follow start address(2) + quantity(2) + byte count(1) @@ -424,7 +448,7 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func number_of_registers * 2 != number_of_bytes) { ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 " or bytes %" PRIu8, number_of_registers, number_of_bytes); - this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + this->send_exception_(address, function_code, ExceptionCode::ILLEGAL_DATA_VALUE); return; } if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) { @@ -436,14 +460,14 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func for (uint16_t i = 0; i < number_of_registers; i++) { registers.push_back(helpers::get_data(data, values_offset + i * 2)); } - status = device->on_modbus_write_registers(start_address, registers); + status = device->on_write_registers(start_address, registers); response_data = data; // echo the request header per Modbus 6.6, 6.12 response_len = 4; break; } default: ESP_LOGW(TAG, "Unsupported function code %" PRIu8, function_code); - this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); + this->send_exception_(address, function_code, ExceptionCode::ILLEGAL_FUNCTION); return; } if (status.has_value()) { @@ -453,30 +477,29 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func } } +// Callers gate on tx_blocked() first, but the pre-send delay below can span several ms, so re-check +// after it and refuse (return false) if a byte arrived in that window rather than transmit over it. bool Modbus::send_frame_(const ModbusFrame &frame) { - if (this->tx_blocked()) { - ESP_LOGE(TAG, "Attempted to send while transmission blocked"); - return false; - } - if (frame.size > MAX_FRAME_SIZE) { - ESP_LOGE(TAG, "Attempted to send frame larger than max frame size of %" PRIu16 " bytes", MAX_FRAME_SIZE); - return false; - } - const int32_t tx_delay_remaining = this->tx_delay_remaining(); if (tx_delay_remaining > 0) { delay(tx_delay_remaining); } + // The delay above can span several ms; a byte arriving in that window blocks transmission after the + // caller's gate already passed. Don't collide with the incoming frame - leave the entry to retry. + if (this->tx_blocked()) { + return false; + } + if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->digital_write(true); - this->write_array(frame.data.get(), frame.size); + this->write_array(frame.data.data(), frame.size()); this->flush(); this->flow_control_pin_->digital_write(false); this->last_send_tx_offset_ = 0; } else { - this->write_array(frame.data.get(), frame.size); - this->last_send_tx_offset_ = frame.size * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1; + this->write_array(frame.data.data(), frame.size()); + this->last_send_tx_offset_ = frame.size() * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1; } uint32_t now = millis(); @@ -484,35 +507,28 @@ bool Modbus::send_frame_(const ModbusFrame &frame) { char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; #endif ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send, %" PRIu32 "ms after last receive", - format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), now - this->last_send_, + format_hex_pretty_to(hex_buf, frame.data.data(), frame.size()), now - this->last_send_, now - this->last_modbus_byte_); this->last_send_ = now; return true; } void ModbusClientHub::send_next_frame_() { - if (this->tx_buffer_.empty()) { + if (this->tx_blocked()) + return; + + ModbusDeviceCommand *cmd = this->select_next_ready_(); + if (cmd == nullptr) + return; + + if (!this->send_frame_(cmd->frame)) { + ESP_LOGV(TAG, "Send deferred for %" PRIu8 ": a frame arrived during the send delay, will retry", + cmd->frame.address()); return; } - if (this->tx_blocked()) { - return; - } - - ModbusDeviceCommand &command = this->tx_buffer_.front(); - - if (this->send_frame_(command.frame)) { - this->waiting_for_response_ = std::move(command); - } else { - if (command.device) - command.device->on_modbus_not_sent(); - } - - this->tx_buffer_.pop_front(); - - if (!this->tx_buffer_.empty()) { - ESP_LOGV(TAG, "Write queue contains %zu items.", this->tx_buffer_.size()); - } + cmd->sent(); + this->waiting_for_response_ = true; } void ModbusClientHub::dump_config() { @@ -555,7 +571,7 @@ void ModbusServerHub::send_response_(uint8_t address, uint8_t function_code, con this->send_raw_(raw_frame, payload_len + 2); } -void ModbusServerHub::send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code) { +void ModbusServerHub::send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code) { uint8_t raw_frame[3]; raw_frame[0] = address; raw_frame[1] = function_code | FUNCTION_CODE_EXCEPTION_MASK; @@ -563,68 +579,261 @@ void ModbusServerHub::send_exception_(uint8_t address, uint8_t function_code, Mo this->send_raw_(raw_frame, 3); } -// Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload. -void ModbusClientHub::queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t pdu_len, ModbusClientDevice *device) { - if (pdu_len == 0) { - if (device) - device->on_modbus_not_sent(); +ModbusDeviceCommand *ModbusClientHub::find_waiting_() { + for (auto &cmd : this->tx_buffer_) { + if (cmd.waiting_state()) + return &cmd; + } + return nullptr; +} + +ModbusDeviceCommand *ModbusClientHub::select_next_ready_() { + // Class first (WRITE, then one-shot READ, then CONTINUOUS), oldest within a class. seq is a + // free-running counter, so compare each entry's AGE against it (correct across the full range). + const uint16_t now = this->next_seq_; + const auto age = [now](const ModbusDeviceCommand &cmd) -> uint16_t { return now - cmd.seq; }; + const auto older = [&age](const ModbusDeviceCommand &a, const ModbusDeviceCommand &b) { return age(a) > age(b); }; + ModbusDeviceCommand *best = nullptr; + for (auto &cmd : this->tx_buffer_) { + if (cmd.state != FrameState::READY) + continue; + if (best == nullptr || cmd.priority() > best->priority() || + (cmd.priority() == best->priority() && older(cmd, *best))) { + best = &cmd; + } + } + return best; +} + +bool ModbusDeviceCommand::sent() { + this->state = FrameState::WAITING; + // on_sent() is not a terminal, so nothing is consumed. + if (this->device == nullptr) + return false; + this->device->on_sent(this->frame.pdu()); + return true; +} + +bool ModbusDeviceCommand::notify_retired() { + if (!this->decrement_pending()) + return false; // nothing owed - stop the sweep draining this entry + if (this->device != nullptr) + this->device->on_not_sent(this->frame.pdu()); + return true; // consumed one debt (delivered, or silent when device-less) - keep draining to zero +} + +bool ModbusDeviceCommand::response(std::span response_pdu) { + this->state = this->state == FrameState::WAITING_RETIRED ? FrameState::RETIRED : FrameState::RECEIVED_RESPONSE; + // A continuous poll is never consumed by its own response; a one-shot consumes one request here. + if (!this->continuous) + this->decrement_pending(); + if (this->device == nullptr) + return false; + this->device->on_response(this->frame.pdu(), response_pdu); + return true; +} + +bool ModbusDeviceCommand::error(ExceptionCode exception_code) { + this->state = this->state == FrameState::WAITING_RETIRED ? FrameState::RETIRED : FrameState::RECEIVED_EXCEPTION; + // An exception ends a continuous poll too, so decrement unconditionally. + this->decrement_pending(); + if (this->device == nullptr) + return false; + this->device->on_error(this->frame.pdu(), exception_code); + return true; +} + +bool ModbusDeviceCommand::interrupt() { + // An unexpected frame distrusts the transaction. A cleared-but-still-waiting shell distrusts too, so + // the interrupt survives the clear in either order (WAITING_RETIRED -> INTERRUPTED_RETIRED). + if (this->state == FrameState::WAITING) { + this->state = FrameState::INTERRUPTED; + return true; + } + if (this->state == FrameState::WAITING_RETIRED) { + this->state = FrameState::INTERRUPTED_RETIRED; + return true; + } + return false; +} + +bool ModbusDeviceCommand::timed_out() { + this->state = FrameState::TIMED_OUT; // advance BEFORE the callback so a clear from inside it wins + this->decrement_pending(); // resolve this request (WAITING-origin, so pending >= 1) + if (this->device == nullptr) + return false; // resolved, no one to tell + if (this->device->on_no_response(this->frame.pdu())) + this->increment_pending(); // granted retry = re-request (capped) + return true; +} + +void ModbusClientHub::sweep_() { + if (!this->sweep_needed_) return; + this->sweep_needed_ = false; + // Serve only the entries present now: a callback may append (a re-send), but those sit beyond + // work_set and are left for the next sweep, which bounds the work and is the termination argument. + // Entries leave the container only in the erase pass below, so indices/references stay valid. + const size_t work_set = this->tx_buffer_.size(); + // Restart the walk after every callback: a handler may have moved any entry to any state. + bool callback_ran = true; + while (callback_ran) { + callback_ran = false; + for (size_t i = 0; i != work_set && !callback_ran; i++) { + ModbusDeviceCommand &cmd = this->tx_buffer_[i]; + switch (cmd.state) { + case FrameState::RECEIVED_RESPONSE: + case FrameState::RECEIVED_EXCEPTION: + case FrameState::TIMED_OUT: + // Off the wire, callback already delivered: reschedule what is still pending, else erase. + if (cmd.pending) + cmd.requeue(this->next_seq_++); + break; + case FrameState::RETIRED: + // Owes one on_not_sent() per accepted request; notify_retired() consumes one and reports + // whether a debt remained, so the restart loop drains the entry to zero - even a device-less + // shell with pending > 1 (no callback fires, but it still drains rather than stranding). + callback_ran = cmd.notify_retired(); + break; + case FrameState::WAITING_RETIRED: + case FrameState::INTERRUPTED_RETIRED: + // Cleared shell: drain only the un-run duplicates; the request in flight keeps pending 1 + // and gets its usual callback when it resolves. + if (cmd.pending > 1) + callback_ran = cmd.notify_retired(); + break; + default: // READY / WAITING / INTERRUPTED: idle or waiting for a response, nothing owed until the timeout + break; + } + } + } + // Erase pass: the only place entries leave the container. Storage order carries no meaning, so a + // finished entry is swap-and-popped; walking backwards means a moved-down entry is already seen. + for (size_t i = this->tx_buffer_.size(); i-- > 0;) { + const ModbusDeviceCommand &cmd = this->tx_buffer_[i]; + // pending == 0 is erasable, but shells still waiting for a response are exempt until it resolves. + if (cmd.pending != 0 || cmd.waiting_state()) + continue; + if (i + 1 != this->tx_buffer_.size()) + this->tx_buffer_[i] = std::move(this->tx_buffer_.back()); + this->tx_buffer_.pop_back(); + } +} + +// Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload. +bool ModbusClientHub::send_pdu(uint8_t address, std::span pdu, ModbusClientDevice *device, + CommandOptions options) { + // Requests refused here never enter the machine and get no callback - the false return is it. + if (pdu.empty()) { + ESP_LOGW(TAG, "Empty PDU refused for address %" PRIu8, address); + return false; + } + // Bound the PDU so the wire frame (address + pdu + CRC) stays within the Modbus RTU 256-byte limit. + if (pdu.size() > MAX_PDU_SIZE) { + ESP_LOGE(TAG, "Frame too large, refused: %" PRIu8 ":%zu bytes", address, pdu.size()); + return false; } - if (this->tx_buffer_.size() < MODBUS_TX_BUFFER_SIZE) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; -#endif - ESP_LOGV(TAG, "Adding frame to tx queue: %" PRIu8 ":%s", address, format_hex_pretty_to(hex_buf, pdu, pdu_len)); - this->tx_buffer_.emplace_back(device, address, pdu, pdu_len); - } else { + // continuous is ignored for every mutating code (re-writing a value forever is never intended). + const bool mutates = ModbusDeviceCommand::classify(pdu[0]) == CommandPriority::WRITE; + bool continuous = false; + if (options.continuous) { + if (mutates) { + ESP_LOGV(TAG, "continuous is ignored for a mutating function (0x%X, address %" PRIu8 ")", pdu[0], address); + } else { + continuous = true; + } + } + + // A duplicate of a live entry with the same owner is not queued twice; it resolves against that + // entry: anonymous -> dropped; continuous incoming -> convert the entry to a poll; one-shot onto a + // poll -> downgrade the poll to one-shot; both one-shots -> pending++ below the cap, else refused. + for (auto &item : this->tx_buffer_) { + if (item.state == FrameState::RETIRED || item.state == FrameState::WAITING_RETIRED || + item.state == FrameState::INTERRUPTED_RETIRED) + continue; // cleared, on their way out: a new identical send queues fresh, never absorbs + if (item.device != device || !item.same_frame(address, pdu)) + continue; + if (device == nullptr) { + // A dropped read is routine (DEBUG); a dropped write/custom warns (unobservable without a device). + const bool requeueable = !helpers::is_function_code_exception(pdu[0]) && helpers::is_function_code_read(pdu[0]); + if (requeueable) { + ESP_LOGD(TAG, "Anonymous duplicate of active frame for %" PRIu8 " (function 0x%X), dropped", address, pdu[0]); + } else { + ESP_LOGW(TAG, + "Anonymous duplicate of active frame for %" PRIu8 " (function 0x%X), dropped - register a " + "device for delivery accounting", + address, pdu[0]); + } + return false; // dropped: no entry, no callbacks - the refusal is the return value + } + if (continuous) { + item.make_continuous(true); + ESP_LOGV(TAG, "Frame already active for %" PRIu8 ", now polled continuously", address); + } else if (item.continuous) { + // A one-shot duplicate downgrades the poll to a one-shot: it runs one more cycle to serve this + // request, then stops (mirrors continuous incoming converting a one-shot the other way). + item.make_continuous(false); + ESP_LOGV(TAG, "Frame already active for %" PRIu8 ", downgraded from continuous to one-shot", address); + } else if (!item.increment_pending()) { + // At the servable cap, so refused. (An absorbed duplicate leaves seq alone - the entry keeps + // its place in line, held by its oldest outstanding request.) + ESP_LOGD(TAG, "Frame already active for %" PRIu8 " with %" PRIu8 " requests pending, refused", address, + item.pending); + return false; + } else { + ESP_LOGV(TAG, "Frame already active for %" PRIu8 ", request absorbed (pending %" PRIu8 ")", address, + item.pending); + } + return true; + } + + // Backstop counts every entry; dead ones are gone by the sweep's end, so at worst they cost one + // refusal at the very cap for one loop. + if (this->tx_buffer_.size() >= MODBUS_TX_BUFFER_SIZE) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; #endif - ESP_LOGE(TAG, "Write buffer full, dropped: %" PRIu8 ":%s", address, format_hex_pretty_to(hex_buf, pdu, pdu_len)); - if (device) - device->on_modbus_not_sent(); + ESP_LOGE(TAG, "Write buffer full, refused: %" PRIu8 ":%s", address, + format_hex_pretty_to(hex_buf, pdu.data(), pdu.size())); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Adding frame to tx queue: %" PRIu8 ":%s", address, + format_hex_pretty_to(hex_buf, pdu.data(), pdu.size())); + this->tx_buffer_.emplace_back(device, address, pdu, continuous, this->next_seq_++); + return true; +} + +void ModbusClientHub::clear_tx_queue_for_address(uint8_t address) { + // A clear is a pure state flip; the sweep delivers every owed on_not_sent() from a quiescent hub. + for (auto &cmd : this->tx_buffer_) { + if (cmd.frame.address() != address) + continue; + cmd.retire(); + this->sweep_needed_ = true; } } -void ModbusClientHub::clear_tx_queue_for_address(uint8_t address, bool clear_sent) { - // Remove any pending commands for this address from the tx buffer - auto &tx_buffer = this->tx_buffer_; - tx_buffer.erase(std::remove_if(tx_buffer.begin(), tx_buffer.end(), - [address](const ModbusDeviceCommand &cmd) { return cmd.frame.data[0] == address; }), - tx_buffer.end()); - - if (clear_sent && this->waiting_for_response_.has_value() && this->waiting_for_response_.value().device) { - if (this->waiting_for_response_.value().frame.data[0] == address) { - ESP_LOGV(TAG, "Clearing waiting for response for address %" PRIu8, address); - // Invalidate the waiting device so it won't process a response. - this->waiting_for_response_.value().device = nullptr; - } - } -} void ModbusClientHub::clear_tx_queue_for_device(ModbusClientDevice *device) { - // Remove any pending commands for this address from the tx buffer - auto &tx_buffer = this->tx_buffer_; - tx_buffer.erase(std::remove_if(tx_buffer.begin(), tx_buffer.end(), - [device](const ModbusDeviceCommand &cmd) { return cmd.device == device; }), - tx_buffer.end()); - - if (this->waiting_for_response_.has_value() && this->waiting_for_response_.value().device) { - if (this->waiting_for_response_.value().device == device) { - ESP_LOGV(TAG, "Clearing waiting for response"); - // Invalidate the waiting device so it won't process a response. - this->waiting_for_response_.value().device = nullptr; - } + // Silent teardown (supersede semantics): the caller's own frames vanish without callbacks; see + // the lifecycle note on ModbusClientDevice. + for (auto &cmd : this->tx_buffer_) { + if (cmd.device != device) + continue; + cmd.silent_retire(); + this->sweep_needed_ = true; } } void ModbusClientHub::send_raw(const std::vector &payload, ModbusClientDevice *device) { if (payload.size() < 2) { - if (device) - device->on_modbus_not_sent(); + ESP_LOGW(TAG, "send_raw() payload too short to contain a PDU, refused"); return; } - this->queue_raw_(payload[0], payload.data() + 1, static_cast(payload.size() - 1), device); + this->send_pdu(payload[0], std::span(payload).subspan(1), device); } // Send raw command for server replies immediately. Except CRC everything must be contained in payload @@ -637,23 +846,26 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) { return; } - // In the rare case that the server is blocked (frame delay has not elapsed), we delay the send. - // This should only happen at low baud rates with long frame delays. + // If blocked now (frame delay not elapsed at low baud, or a frame arriving), defer rather than + // busy-waiting the loop; send_frame_ itself re-checks after its delay, so the deferred callback + // just reports whatever it returns. if (this->tx_blocked()) { // Stash the raw payload in a single member buffer so the deferred callback can rebuild the frame - // without a heap allocation. Only one server reply is ever in flight, and the named timeout ensures - // only one deferred send is pending, so a single buffer is sufficient. + // without a heap allocation. Only one server reply is ever waiting, so a single buffer suffices. std::memcpy(this->deferred_payload_.data(), payload, len); this->deferred_payload_len_ = len; this->set_timeout("deferred_send", this->tx_delay_remaining(), [this]() { ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1, this->deferred_payload_len_ - 1); - this->send_frame_(frame); + if (!this->send_frame_(frame)) + ESP_LOGE(TAG, "Deferred server reply dropped: transmission still blocked"); }); - } else { - ModbusFrame frame(payload[0], payload + 1, len - 1); - this->send_frame_(frame); + return; } + + ModbusFrame frame(payload[0], payload + 1, len - 1); + if (!this->send_frame_(frame)) + ESP_LOGE(TAG, "Server reply dropped: a frame arrived during the send delay"); } void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_to_clear) { @@ -676,4 +888,150 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_t } } +void ModbusClientDevice::dispatch_response_(std::span request_pdu, std::span response_pdu, + ResponseStatus status) { + if (request_pdu.empty()) + return; + auto function_code = static_cast(request_pdu[0]); + // All standard requests handled below are function code + start address + count/value (5 bytes); + // anything shorter cannot be parsed and is handed to the catch-all. + if (request_pdu.size() < READ_PDU_SIZE) { + this->on_custom_response(request_pdu, response_pdu, status); + return; + } + const uint16_t start_address = helpers::get_data(request_pdu.data(), 1); + // count for reads/multi-writes, value for single writes + const uint16_t count_or_value = helpers::get_data(request_pdu.data(), 3); + + // Gatekeeper for the typed dispatch below: anything that is not a standard-conformant transaction is + // handed to on_custom_response() with the raw PDUs, so the decode cases can trust every length, byte + // count, and quantity field without re-clamping. + // - The REQUEST must be standard: nothing upstream validates a caller-built request PDU, so its + // internal byte count, quantity, and address range are checked here (is_client_pdu_standard()). + // - On success, the RESPONSE must be standard (self-consistent; the frame parser already guarantees + // most of this, but the check keeps the safety proof local), and a read response's length must also + // match the REQUESTED count - the per-PDU checks cannot see that relationship, and a short but + // self-consistent response must be diverted, never silently clamped and delivered as complete. + // - On failure (status engaged) the response is empty by design (see on_error()), so only the request + // is validated. + bool custom = !helpers::is_client_pdu_standard(request_pdu.data(), request_pdu.size()); + if (!custom && !status.has_value()) { + custom = !helpers::is_server_pdu_standard(response_pdu.data(), response_pdu.size()); + if (!custom && helpers::is_function_code_read(static_cast(function_code))) { + const bool bits = + function_code == FunctionCode::READ_COILS || function_code == FunctionCode::READ_DISCRETE_INPUTS; + const size_t expected_data_size = + bits ? packed_bit_bytes(count_or_value) : static_cast(count_or_value) * 2; + if (response_pdu.size() != expected_data_size + 2) { + ESP_LOGD(TAG, "Response length %zu does not match request (expected %zu) for function code 0x%X", + response_pdu.size(), expected_data_size + 2, static_cast(function_code)); + custom = true; + } + } + } + if (custom) { + this->on_custom_response(request_pdu, response_pdu, status); + return; + } + + switch (function_code) { + case FunctionCode::READ_HOLDING_REGISTERS: + case FunctionCode::READ_INPUT_REGISTERS: { + // Decode the big-endian register words into host byte order. The gate guarantees a success response + // carries exactly count_or_value registers (and count_or_value <= MAX_NUM_OF_REGISTERS_TO_READ, the + // capacity of RegisterValues); a mismatch was diverted to on_custom_response(), never clamped. On + // failure the registers span is empty. + RegisterValues registers; + if (!status.has_value()) { + for (size_t i = 0; i != count_or_value; i++) { + registers.push_back(helpers::get_data(response_pdu.data(), 2 + 2 * i)); + } + } + std::span register_span(registers.data(), registers.size()); + if (function_code == FunctionCode::READ_HOLDING_REGISTERS) { + this->on_read_holding_registers(start_address, register_span, status); + } else { + this->on_read_input_registers(start_address, register_span, status); + } + break; + } + case FunctionCode::READ_COILS: + case FunctionCode::READ_DISCRETE_INPUTS: { + // Deliver the bits packed as on the wire; the gate guarantees a success response carries exactly + // (count_or_value + 7) / 8 data bytes. On failure the view is empty AND the count is zero - + // PackedBits::operator[] is unchecked, so size() must never promise bits with no bytes behind them. + std::span packed_bytes; + uint16_t count = 0; + if (!status.has_value()) { + packed_bytes = response_pdu.subspan(2); + count = count_or_value; + } + PackedBits bits(packed_bytes, count); + if (function_code == FunctionCode::READ_COILS) { + this->on_read_coils(start_address, bits, status); + } else { + this->on_read_discrete_inputs(start_address, bits, status); + } + break; + } + // Single-write acks echo the value: on success that echo is device-confirmed state - the one + // write whose acknowledgement carries a real read-back - so it is preferred over the request + // copy. On an exception the response has no value and the request copy is the only one. + case FunctionCode::WRITE_SINGLE_REGISTER: + case FunctionCode::WRITE_SINGLE_COIL: { + const uint16_t value = (!status.has_value() && response_pdu.size() >= WRITE_SINGLE_PDU_SIZE) + ? helpers::get_data(response_pdu.data(), 3) + : count_or_value; + if (function_code == FunctionCode::WRITE_SINGLE_REGISTER) { + this->on_write_single_register(start_address, value, status); + } else { + this->on_write_single_coil(start_address, value == 0xFF00, status); + } + break; + } + case FunctionCode::WRITE_MULTIPLE_REGISTERS: { + // Request layout: [0] function code, [1..2] start address, [3..4] register count, [5] byte count, + // [6..] register data. The gate guarantees the request carries exactly count_or_value registers + // (<= MAX_NUM_OF_REGISTERS_TO_WRITE, within RegisterValues capacity). Decoded from the request and + // delivered regardless of status - see the write-acknowledgement note in modbus.h. + RegisterValues registers; + for (size_t i = 0; i != count_or_value; i++) { + registers.push_back(helpers::get_data(request_pdu.data(), 6 + 2 * i)); + } + std::span register_span(registers.data(), registers.size()); + this->on_write_multiple_registers(start_address, register_span, status); + break; + } + case FunctionCode::WRITE_MULTIPLE_COILS: { + // Request layout: [0] function code, [1..2] start address, [3..4] coil count, [5] byte count, + // [6..] packed bits. The gate guarantees the request carries exactly (count_or_value + 7) / 8 packed + // bytes. Decoded from the request and delivered regardless of status - see the write-acknowledgement + // note in modbus.h. + std::span packed_bytes = request_pdu.subspan(6); + PackedBits bits(packed_bytes, count_or_value); + this->on_write_multiple_coils(start_address, bits, status); + break; + } + default: + this->on_custom_response(request_pdu, response_pdu, status); + break; + } +} + +// Default on_custom_response handler to warn when responses unexpectedly trigger on_custom_response +void ModbusClientDevice::on_custom_response(std::span request_pdu, std::span response_pdu, + ResponseStatus status) { + // The dispatcher never calls this with an empty request, but this is a public virtual - stay safe. + const uint8_t function_code = request_pdu.empty() ? 0 : request_pdu[0]; + // Warn once per device, then drop to VERBOSE: a mildly non-conformant peer answers every poll, + // and an unhandled-response warning per transaction would flood the log permanently. + if (!this->custom_response_warned_) { + this->custom_response_warned_ = true; + ESP_LOGW(TAG, "Non-standard request or response for function code 0x%X. No on_custom_response handler declared", + function_code); + } else { + ESP_LOGV(TAG, "Non-standard request or response for function code 0x%X (unhandled)", function_code); + } +} + } // namespace esphome::modbus diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index b0f2aed9f8..c49f52df55 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -9,28 +9,50 @@ #include #include #include +#include #include #include #include namespace esphome::modbus { -static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15; +// Tx queue backstop. Duplicate frames dedup into one entry, so reads can never approach this in a +// sane config - it exists to stop a runaway generator of distinct frames (e.g. a loop writing a +// changing value) from growing the heap unboundedly. The deque grows on demand; this reserves nothing. +// Worst case the cap permits: 128 distinct max-size frames = ~32 kB of spilled frame data plus +// ~3 kB of deque node storage (typical 8-byte frames stay inline; large PDUs spill to one +// allocation each) - pathological configs only, but the numbers matter when tuning for ESP8266. +static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 128; static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5; -struct ModbusFrame { - // Frame with exact-size allocation to avoid std::vector overhead - std::unique_ptr data; - uint16_t size; // Modbus RTU max is 256 bytes +// Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes +// (address + 5-byte PDU + 2-byte CRC) and fit inline with no heap allocation. +static constexpr uint16_t MODBUS_FRAME_INLINE_SIZE = 8; - ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len) - : data(std::make_unique(pdu_len + 3)), size(pdu_len + 3) { - data[0] = address; - memcpy(data.get() + 1, pdu, pdu_len); - auto crc = crc16(data.get(), pdu_len + 1); - data[pdu_len + 1] = crc >> 0; - data[pdu_len + 2] = crc >> 8; +struct ModbusFrame { + // Frame held in a small-buffer-optimized buffer. Typical frames fit inline; only larger + // multi-register or custom frames spill to a single heap allocation. This keeps the common, + // high-frequency tx traffic off the heap entirely, avoiding per-frame alloc/free churn. + // The buffer tracks its own length, so no separate size field is needed. + SmallInlineBuffer data; // Modbus RTU max is 256 bytes + + ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len) { + uint8_t *buf = this->data.init(pdu_len + 3); + buf[0] = address; + memcpy(buf + 1, pdu, pdu_len); + auto crc = crc16(buf, pdu_len + 1); + buf[pdu_len + 1] = crc >> 0; + buf[pdu_len + 2] = crc >> 8; } + + uint16_t size() const { return static_cast(this->data.size()); } + + // A frame is [address][PDU...][CRC lo][CRC hi]. These are the only places that need to know that layout + uint8_t address() const { return this->data.data()[0]; } + /// The PDU: function code + data, without address or CRC. Only valid while the frame is alive. + /// Requires a complete frame (size() >= MIN_FRAME_SIZE, guaranteed by the constructors) - the + /// subtraction would wrap on anything shorter. + std::span pdu() const { return std::span(this->data.data() + 1, this->size() - 3u); } }; class Modbus : public uart::UARTDevice, public Component { @@ -51,9 +73,12 @@ class Modbus : public uart::UARTDevice, public Component { virtual int32_t tx_delay_remaining(); virtual void parse_modbus_frames() = 0; bool parse_modbus_server_frame_(); - virtual void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, - uint16_t len) = 0; + // pdu is the whole PDU (function code + payload, no address/CRC); pdu[0] is the (standard or custom) function code. + virtual void process_modbus_server_frame(uint8_t address, std::span pdu) = 0; void clear_rx_buffer_(const LogString *reason, bool warn = false, size_t bytes_to_clear = 0); + // Transmit a frame. Callers gate on tx_blocked() first, but the pre-send delay can span several ms, + // so this re-checks after the delay and returns false without transmitting if a byte arrived in that + // window (the caller then leaves its entry to retry). Returns true once the frame has been transmitted. bool send_frame_(const ModbusFrame &frame); // Scans forward from min_length to find a frame boundary by CRC match for custom function codes. // Returns the matched frame length, or 0 if no valid CRC was found within MAX_FRAME_SIZE. @@ -74,13 +99,158 @@ class Modbus : public uart::UARTDevice, public Component { class ModbusClientDevice; class ModbusServerDevice; +// Transmit ordering, highest first: writes before one-shot reads before continuous polls. Derived +// at selection time, never caller-chosen or stored. +enum class CommandPriority : uint8_t { CONTINUOUS = 0, READ, WRITE }; + +// Per-entry lifecycle state. Waiting states (see waiting_state()) hold the bus; the sweep delivers owed +// callbacks from a quiescent hub, and an entry is erased once pending == 0 && !waiting_state(). +enum class FrameState : uint8_t { + READY = 0, + WAITING, + RECEIVED_RESPONSE, + RECEIVED_EXCEPTION, + TIMED_OUT, // on_no_response delivered at the send-wait timeout; awaiting reschedule/erase + INTERRUPTED, // unexpected frame arrived; ignores this transaction, waits out the timeout + WAITING_RETIRED, // cleared while WAITING: a late response is still delivered as its usual terminal + INTERRUPTED_RETIRED, // cleared while INTERRUPTED: still distrusts late frames, ends in on_no_response + RETIRED, // cleared, off the wire +}; + +// Per-command send options. Append-only; pass via designated initializers ({.continuous = true}). +struct CommandOptions { + // A continuous poll lives in the queue until cancelled or failed; ignored for mutating codes. + bool continuous{false}; +}; + struct ModbusDeviceCommand { ModbusClientDevice *device; ModbusFrame frame; - bool interrupted{false}; + FrameState state{FrameState::READY}; + // A continuous poll is a subscription: pending fixed at 1, removed only by cancellation or failure. + bool continuous{false}; + // Accepted requests this entry stands for, capped at max_pending(); drains one terminal each. + uint8_t pending{1}; + // Place-in-line stamp (hub's free-running counter); selection takes the oldest for round-robin + // fairness within a class. Meant to wrap. + uint16_t seq{0}; - ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, const uint8_t *src, uint16_t len) - : device(device), frame(address, src, len) {} + // Build a command from a PDU span (caller bounds it to MAX_PDU_SIZE); fully initialized here. + ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, std::span pdu, + bool continuous = false, uint16_t seq = 0) + : device(device), + frame(address, pdu.data(), static_cast(pdu.size())), + continuous(continuous), + seq(seq) {} + + // Transmit ordering class, derived (never stored): a continuous poll ranks below every one-shot. + CommandPriority priority() const { + return this->continuous ? CommandPriority::CONTINUOUS : classify(this->frame.pdu()[0]); + } + // Wire-derived class: mutating codes rank WRITE; exception-flagged codes are excluded. + static CommandPriority classify(uint8_t function_code) { + if (helpers::is_function_code_exception(function_code)) + return CommandPriority::READ; + const auto code = static_cast(function_code); + if (helpers::is_function_code_write(function_code) || code == FunctionCode::MASK_WRITE_REGISTER || + code == FunctionCode::READ_WRITE_MULTIPLE_REGISTERS) { + return CommandPriority::WRITE; + } + return CommandPriority::READ; + } + + // Requests this entry can serve: a standard read twice (run plus one re-run), everything else once. + uint8_t max_pending() const { + const uint8_t fc = this->frame.pdu()[0]; + const bool requeueable = !helpers::is_function_code_exception(fc) && helpers::is_function_code_read(fc); + return (requeueable && !this->continuous) ? 2 : 1; + } + // Device-scoped clear: detach with no callback (device-less, pending 0). An entry still waiting for + // a response keeps its state as a reply-ignoring shell that resolves silently; any other goes RETIRED. + void silent_retire() { + if (!this->waiting_state()) + this->state = FrameState::RETIRED; + this->pending = 0; + this->device = nullptr; + } + // Re-ready for another transmission, restamped to the tail of its class (hub passes next_seq_++). + void requeue(uint16_t seq) { + this->state = FrameState::READY; + this->seq = seq; + } + // Re-task a frame that lives on: upgrade a one-shot to a continuous poll, or downgrade a poll back to + // a one-shot. Either way the entry keeps running and owes a request, so this is not a plain setter - + // to tear an entry down instead, use retire()/silent_retire(), which leave pending as the count owed. + // On: the entry becomes a continuous poll, superseding any absorbed requests (pending resets to the + // single subscription). Off: a one-shot duplicate has cancelled the poll, but the entry must still run + // once to serve that request - so restore one first. While the flag is still set max_pending() is 1, + // so the restore lifts a terminated poll (pending 0, after an error/timeout) back to 1 and is a no-op + // on a live poll already at 1; the flag drops afterwards, when a read's cap can widen to 2 without + // retroactively inflating that no-op. + void make_continuous(bool continuous) { + if (continuous) { + this->continuous = true; + this->pending = 1; + } else { + this->increment_pending(); + this->continuous = false; + } + } + // Address-scoped clear: keep pending and device so the sweep delivers one on_not_sent() per un-run + // request. An entry still waiting for a response keeps its in-flight request (whose usual terminal is + // still coming) and drains only its duplicates: WAITING -> WAITING_RETIRED, and INTERRUPTED -> + // INTERRUPTED_RETIRED which keeps distrusting late frames (they were already interrupted). Any other + // state -> RETIRED, draining everything. A cleared frame that then times out still honors a retry: + // the clear is address-scoped (any device may call it) while the retry is the owning device's call + // via on_no_response - the bus obeys the owner. + void retire() { + if (this->state == FrameState::WAITING) { + this->state = FrameState::WAITING_RETIRED; + } else if (this->state == FrameState::INTERRUPTED) { + this->state = FrameState::INTERRUPTED_RETIRED; + } else if (!this->waiting_state()) { // an already-retired shell stays put; off the wire -> RETIRED + this->state = FrameState::RETIRED; + } + this->continuous = false; + } + + // True while the entry is still waiting for a response; the erase pass exempts these even at pending 0. + bool waiting_state() const { + return this->state == FrameState::WAITING || this->state == FrameState::INTERRUPTED || + this->state == FrameState::WAITING_RETIRED || this->state == FrameState::INTERRUPTED_RETIRED; + } + + bool decrement_pending() { + if (this->pending > 0) { + this->pending--; + return true; + } + return false; + } + // Add one request, honouring the cap; false = already at cap (absorb a duplicate, restore a retry). + bool increment_pending() { + if (this->pending < this->max_pending()) { + this->pending++; + return true; + } + return false; + } + + // Terminal/lifecycle methods: each owns its transition, callback, and pending accounting and + // returns whether a callback ran. Out-of-line: ModbusClientDevice is incomplete here. + bool sent(); + bool response(std::span response_pdu); + bool error(ExceptionCode exception_code); + bool interrupt(); + bool timed_out(); + bool notify_retired(); + + /// True if this command carries the same wire frame (address + PDU) as the given one. + bool same_frame(uint8_t address, std::span pdu) const { + const auto own_pdu = this->frame.pdu(); + return own_pdu.size() == pdu.size() && this->frame.address() == address && + memcmp(own_pdu.data(), pdu.data(), pdu.size()) == 0; + } }; class ModbusClientHub : public Modbus { @@ -96,32 +266,49 @@ class ModbusClientHub : public Modbus { void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr, ModbusClientDevice *device = nullptr) { this->send_pdu(address, - helpers::create_client_pdu((ModbusFunctionCode) function_code, start_address, number_of_entities, - payload, payload_len), + helpers::create_client_pdu((FunctionCode) function_code, start_address, number_of_entities, payload, + payload_len), device); }; - void send_pdu(uint8_t address, const StaticVector &pdu, ModbusClientDevice *device = nullptr) { - this->queue_raw_(address, pdu.data(), pdu.size(), device); - } + // Queue a request; true once it is a live entry (resolving in one terminal), false if it never + // entered the machine (empty/oversize PDU, full queue, anonymous or over-cap duplicate) - no callback. + bool send_pdu(uint8_t address, std::span pdu, ModbusClientDevice *device = nullptr, + CommandOptions options = {}); + ESPDEPRECATED("Use send_pdu(payload[0], , device) instead. Removed in 2027.2.0", "2026.8.0") void send_raw(const std::vector &payload, ModbusClientDevice *device = nullptr); - void clear_tx_queue_for_address(uint8_t address, bool clear_sent = true); + // Clear an address's commands; each un-run request resolves via on_not_sent(), but a frame on the + // wire still runs to its usual terminal. clear_tx_queue_for_device() instead discards silently. + void clear_tx_queue_for_address(uint8_t address); void clear_tx_queue_for_device(ModbusClientDevice *device); protected: int32_t tx_delay_remaining() override; void parse_modbus_frames() override; - // Parsers need to handle standard (ModbusFunctionCode) and custom (uint8_t) function codes, so we use uint8_t here. - void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override; + void process_modbus_server_frame(uint8_t address, std::span pdu) override; void send_next_frame_(); - void queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t pdu_len, ModbusClientDevice *device = nullptr); + // Deliver owed callbacks from a quiescent hub and apply lifecycle bookkeeping; see FrameState. + void sweep_(); + // The selection function: best READY entry (WRITE class first, then one-shot reads, then the + // least-recently-served continuous; FIFO by seq within each group), or nullptr. + ModbusDeviceCommand *select_next_ready_(); + // Locate the single entry waiting for a response (WAITING/INTERRUPTED/WAITING_RETIRED/INTERRUPTED_RETIRED). + ModbusDeviceCommand *find_waiting_(); + // End the wait for a response on send-wait timeout (the loop() watchdog body); see FrameState. + void expire_waiting_(); uint16_t send_wait_time_{2000}; uint16_t turnaround_delay_ms_{0}; - std::optional waiting_for_response_; - // std::deque is appropriate here since we need a FIFO buffer, and we can't know ahead of time how many - // requests will be queued. Each modbus component may queue multiple requests, and the sequence of scheduling - // may change at run time. + // Set on transmit, cleared on the transaction-ending transition; send_next_frame_ won't select + // while it is set, so at most one frame is awaiting a response. + bool waiting_for_response_{false}; + + // Set whenever a transition leaves owed callbacks behind; quiet loop() passes skip the sweep. + bool sweep_needed_{false}; + // Monotonic stamp source for ModbusDeviceCommand::seq. + uint16_t next_seq_{0}; + + // Plain append-order container; ordering lives in select_next_ready_(), lifecycle in FrameState. std::deque tx_buffer_; }; @@ -134,8 +321,7 @@ class ModbusServerHub : public Modbus { protected: void parse_modbus_frames() override; bool parse_modbus_client_frame_(); - // Parsers need to handle standard (ModbusFunctionCode) and custom (uint8_t) function codes, so we use uint8_t here. - void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override; + void process_modbus_server_frame(uint8_t address, std::span pdu) override; void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data); ModbusServerDevice *find_device_(uint8_t address); // Returns true if [start_address, start_address + number_of_registers) fits in the 16-bit address space. @@ -143,17 +329,38 @@ class ModbusServerHub : public Modbus { bool check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_registers); void send_raw_(const uint8_t *payload, uint16_t len); - void send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code); + void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code); void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len); uint8_t expecting_peer_response_{0}; std::vector devices_; // Holds the raw payload of a single reply deferred for sending when tx was blocked at send time. - // Only one server reply can be in flight at once, so a single fixed buffer avoids heap allocation. + // Only one server reply can be waiting at once, so a single fixed buffer avoids heap allocation. std::array deferred_payload_; uint16_t deferred_payload_len_{0}; }; +// Transaction status: std::nullopt on success, otherwise a Modbus exception code +using ResponseStatus = std::optional; + +/// Callback contract. Each accepted request ends in exactly ONE terminal: on_response() (data), +/// on_error() (exception), on_no_response() (timeout/interruption), or on_not_sent() (dropped by +/// clear_tx_queue_for_address before transmission). A request refused at send_pdu() (false return) +/// gets none. on_sent() is additional, once per transmission, never for an on_not_sent() request. +/// on_response()/on_error() fire at parse time and on_no_response() at the send-wait watchdog, all +/// from a quiescent hub; only on_not_sent() is delivered by the sweep. Sending or clearing from +/// inside a callback is safe (picked up by the next sweep). Exceptions to "exactly one terminal": +/// clear_tx_queue_for_device() drops the caller's own frames silently; a continuous poll's cycles are +/// its own accounting (a one-shot duplicate downgrades the poll to a one-shot; a continuous duplicate +/// merges into it). +/// +/// Invariants: +/// - Public entry points (send_pdu/clear_tx_queue_*) only append to the queue or mutate an existing +/// entry through its callback-free transition methods. +/// - Public entry points can never trigger a callback synchronously. +/// - Callbacks are delivered only from within loop(). +/// - At most one callback is ever issued between calls to sweep_(): +/// sweep_ -> parse (response OR error) OR timeout (no_response) -> sweep_ -> send (sent) -> sweep_ (next loop) class ModbusClientDevice { public: ModbusClientDevice() = default; @@ -168,22 +375,150 @@ class ModbusClientDevice { ModbusClientDevice &operator=(ModbusClientDevice &&) = delete; void set_parent(ModbusClientHub *parent) { this->parent_ = parent; } void set_address(uint8_t address) { this->address_ = address; } - virtual void on_modbus_data(const std::vector &data) {} - virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {} + /// Low-level response hook: called with the request PDU this device sent and the response PDU received + /// The spans are only valid for the duration of the call - copy the bytes if they must outlive it. + /// The default implementation decodes standard responses and dispatches to on_read_* / on_write_* callbacks below. + /// Override it to handle raw PDUs directly. + virtual void on_response(std::span request_pdu, std::span response_pdu) { + this->dispatch_response_(request_pdu, response_pdu, std::nullopt); + } + /// Low-level error hook: called with the request PDU and the modbus exception code from the error response. + /// The default implementation dispatches to the same typed callbacks with the exception code as status. + /// Devices implementing the High-level typed callbacks see success and failure through one interface. + virtual void on_error(std::span request_pdu, ExceptionCode exception_code) { + this->dispatch_response_(request_pdu, {}, exception_code); + } + /// Called when an accepted request was dropped before transmission by clear_tx_queue_for_address(). + /// (on_modbus_* below are deprecated pre-rename spellings; the defaults forward during deprecation.) + virtual void on_not_sent(std::span request_pdu) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->on_modbus_not_sent(); +#pragma GCC diagnostic pop + } + /// Called when this device's frame is actually written to the wire + virtual void on_sent(std::span request_pdu) {} + /// Called when no matching, uninterrupted response arrived; return true to have the hub re-queue the frame for a + /// retry. The hub does not bound retries: the device is responsible for limiting them. + virtual bool on_no_response(std::span request_pdu) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + return this->on_modbus_no_response(); +#pragma GCC diagnostic pop + } + // Remove before 2027.2.0 + ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0") virtual void on_modbus_not_sent() {} - virtual void on_modbus_no_response() {} + // Remove before 2027.2.0 + ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0") + virtual bool on_modbus_no_response() { return false; } + + /// High-level typed response callbacks, fired by the default on_response()/on_error() with arguments + /// parsed from the request and response PDUs. + /// Status is std::nullopt on success; holds the exception code on failure. + /// Register values are in host byte order; spans are only valid for the duration of the call. + virtual void on_read_registers(EntityType entity_type, uint16_t start_address, std::span registers, + ResponseStatus status) {} + virtual void on_read_holding_registers(uint16_t start_address, std::span registers, + ResponseStatus status) { + this->on_read_registers(EntityType::HOLDING, start_address, registers, status); + } + virtual void on_read_input_registers(uint16_t start_address, std::span registers, + ResponseStatus status) { + this->on_read_registers(EntityType::INPUT_REGISTER, start_address, registers, status); + } + /// Coil/discrete-input reads are delivered as a PackedBits view (bit 0 = the bit at start_address, + /// bits.size() = the count requested). The view points into the hub's receive buffer and is only + /// valid during the call. + virtual void on_read_bits(EntityType entity_type, uint16_t start_address, PackedBits bits, ResponseStatus status) {} + virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) { + this->on_read_bits(EntityType::COIL, start_address, bits, status); + } + virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status) { + this->on_read_bits(EntityType::DISCRETE_INPUT, start_address, bits, status); + } + /// Write acknowledgements. These deliberately mirror the read callbacks' shapes, so a write ack can be fed + /// through the same handler as a read (registers.size() / bits.size() gives the count) + /// + /// IMPORTANT - for the multi-writes these are the values that were REQUESTED, not device-confirmed + /// state: a multi-write ack only echoes the start address and count, so the values are decoded from + /// the request PDU, and they are delivered even when status holds an exception code. Always check + /// status, and treat publishing them as an optimistic update rather than a read-back. The single + /// writes are the exception: their successful ack echoes the value, so on success the delivered + /// value is the device's echo (on an exception it falls back to the request copy). + virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status) {} + virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status) {} + virtual void on_write_multiple_registers(uint16_t start_address, std::span registers, + ResponseStatus status) {} + virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) {} + /// Catch-all for custom function codes and anything that is not a standard-conformant transaction + /// (see dispatch_response_()); on failure the response is empty and the exception code is in status. + /// The default implementation only logs a warning that the response is going unhandled - override it + /// to handle custom traffic (which also silences the warning). + virtual void on_custom_response(std::span request_pdu, std::span response_pdu, + ResponseStatus status); + ESPDEPRECATED("Use the typed read_*/write_* helpers or send_pdu() instead. Removed in 2027.2.0", "2026.8.0") void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr) { - this->parent_->send_pdu(this->address_, - helpers::create_client_pdu((ModbusFunctionCode) function, start_address, number_of_entities, - payload, payload_len), - this); + this->parent_->send_pdu( + this->address_, + helpers::create_client_pdu((FunctionCode) function, start_address, number_of_entities, payload, payload_len), + this); } - void send_pdu(const StaticVector &pdu) { this->parent_->send_pdu(this->address_, pdu, this); } - void send_raw(const std::vector &payload) { this->parent_->send_raw(payload, this); } - inline void clear_tx_queue_for_address(bool clear_sent = true) { - this->parent_->clear_tx_queue_for_address(this->address_, clear_sent); + /// See ModbusClientHub::send_pdu(): true = accepted (a terminal callback will follow), + /// false = refused at the door (no callback). + bool send_pdu(std::span pdu, CommandOptions options = {}) { + return this->parent_->send_pdu(this->address_, pdu, this, options); } + ESPDEPRECATED("Use send_pdu() instead (the device address is prepended for you). Removed in 2027.2.0", "2026.8.0") + bool send_raw(const std::vector &payload) { + if (payload.empty()) + return false; // too short to contain a PDU; refused at the door like any invalid send + return this->parent_->send_pdu(payload[0], std::span(payload).subspan(1), this); + } + // Reads via the table-appropriate function code; an unreadable entity type maps to INVALID, which + // create_read_pdu() rejects into an empty PDU and send_pdu() refuses with a false return. + bool read_entities(EntityType entity_type, uint16_t start_address, uint16_t number_of_entities, + CommandOptions options = {}) { + return this->send_pdu(helpers::create_read_pdu(helpers::modbus_register_read_function(entity_type), start_address, + number_of_entities), + options); + } + bool read_input_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) { + return this->send_pdu( + helpers::create_read_pdu(FunctionCode::READ_INPUT_REGISTERS, start_address, number_of_registers), options); + } + bool read_holding_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) { + return this->send_pdu( + helpers::create_read_pdu(FunctionCode::READ_HOLDING_REGISTERS, start_address, number_of_registers), options); + } + bool read_coils(uint16_t start_address, uint16_t number_of_coils, CommandOptions options = {}) { + return this->send_pdu(helpers::create_read_pdu(FunctionCode::READ_COILS, start_address, number_of_coils), options); + } + bool read_discrete_inputs(uint16_t start_address, uint16_t number_of_inputs, CommandOptions options = {}) { + return this->send_pdu(helpers::create_read_pdu(FunctionCode::READ_DISCRETE_INPUTS, start_address, number_of_inputs), + options); + } + bool write_single_register(uint16_t start_address, uint16_t value) { + return this->send_pdu(helpers::create_write_single_register_pdu(start_address, value)); + } + bool write_single_coil(uint16_t address, bool value) { + return this->send_pdu(helpers::create_write_single_coil_pdu(address, value)); + } + bool write_multiple_registers(uint16_t start_address, std::span values) { + return this->send_pdu(helpers::create_write_registers_pdu(start_address, values)); + } + /// Note: std::vector cannot bind to std::span; use a contiguous bool container or the packed + /// overload. + bool write_multiple_coils(uint16_t start_address, std::span values) { + return this->send_pdu(helpers::create_write_coils_pdu(start_address, values)); + } + /// Packed variant: a PackedBits view (the same layout on_read_coils() delivers), so + /// read-modify-write needs no unpack/repack. + bool write_multiple_coils(uint16_t start_address, PackedBits bits) { + return this->send_pdu(helpers::create_write_coils_pdu(start_address, bits)); + } + inline void clear_tx_queue_for_address() { this->parent_->clear_tx_queue_for_address(this->address_); } inline void clear_tx_queue_for_device() { this->parent_->clear_tx_queue_for_device(this); } // If more than one device is connected block sending a new command before a response is received @@ -192,17 +527,37 @@ class ModbusClientDevice { bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); } protected: + /// Parses the request/response PDU pair and dispatches to the matching high-level typed callback + void dispatch_response_(std::span request_pdu, std::span response_pdu, + ResponseStatus status); + ModbusClientHub *parent_{nullptr}; uint8_t address_{0}; + bool custom_response_warned_{false}; // first unhandled custom response warns; repeats log at VERBOSE }; -// This is for compatibility with external components using the former class name -// Remove before 2026.12.0 -using ModbusDevice ESPDEPRECATED("Use ModbusClientDevice instead. Removed in 2026.12.0", - "2026.6.0") = ModbusClientDevice; +// Compatibility shim for external components written against the pre-2026.8 API, which subclassed +// ModbusDevice and overrode on_modbus_data()/on_modbus_error(). The name is free (nothing in-tree +// uses it), so instead of a plain alias it adapts the new span-based hooks back to the old +// signatures: on_modbus_data() receives the response payload as an owning vector (the heap copy +// exists only on this deprecated path) and on_modbus_error() the function code and exception code. +// Remove before 2027.2.0 (window restarted when the plain alias became a behavior shim in 2026.8.0) +class ESPDEPRECATED("Subclass ModbusClientDevice and override on_response()/on_error() instead. Removed in 2027.2.0", + "2026.8.0") ModbusDevice : public ModbusClientDevice { + public: + using ModbusClientDevice::ModbusClientDevice; + virtual void on_modbus_data(const std::vector &data) {} + virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {} + + void on_response(std::span request_pdu, std::span response_pdu) override { + auto payload = helpers::server_pdu_payload(response_pdu); + this->on_modbus_data(std::vector(payload.begin(), payload.end())); + } + void on_error(std::span request_pdu, ExceptionCode exception_code) override { + this->on_modbus_error(request_pdu.empty() ? 0 : request_pdu[0], static_cast(exception_code)); + } +}; -// Result of a server register handler: std::nullopt means success, otherwise the Modbus exception code to return. -using ServerResponseStatus = std::optional; // Register values exchanged with server handlers, in host byte order. Sized at the larger of the two protocol // maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by // the capacity of this type. @@ -219,20 +574,20 @@ class ModbusServerDevice { ModbusServerDevice &operator=(ModbusServerDevice &&) = delete; void set_address(uint8_t address) { this->address_ = address; } uint8_t get_address() const { return this->address_; } - virtual ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers, - RegisterValues ®isters) { - return ModbusExceptionCode::ILLEGAL_FUNCTION; + virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, + RegisterValues ®isters) { + return ExceptionCode::ILLEGAL_FUNCTION; }; - virtual ServerResponseStatus on_modbus_read_input_registers(uint16_t start_address, uint16_t number_of_registers, - RegisterValues ®isters) { - return this->on_modbus_read_registers(start_address, number_of_registers, registers); + virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers, + RegisterValues ®isters) { + return this->on_read_registers(start_address, number_of_registers, registers); }; - virtual ServerResponseStatus on_modbus_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, - RegisterValues ®isters) { - return this->on_modbus_read_registers(start_address, number_of_registers, registers); + virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, + RegisterValues ®isters) { + return this->on_read_registers(start_address, number_of_registers, registers); }; - virtual ServerResponseStatus on_modbus_write_registers(uint16_t start_address, const RegisterValues ®isters) { - return ModbusExceptionCode::ILLEGAL_FUNCTION; + virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues ®isters) { + return ExceptionCode::ILLEGAL_FUNCTION; }; protected: diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h index 1c03498f1d..f883bfff30 100644 --- a/esphome/components/modbus/modbus_definitions.h +++ b/esphome/components/modbus/modbus_definitions.h @@ -1,6 +1,11 @@ #pragma once +#include +#include +#include + #include "esphome/core/component.h" +#include "esphome/core/helpers.h" namespace esphome::modbus { @@ -13,7 +18,7 @@ const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_END = 72; // 0x48 const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT = 100; // 0x64 const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END = 110; // 0x6E -enum class ModbusFunctionCode : uint8_t { +enum class FunctionCode : uint8_t { INVALID = 0x00, // 0x00 is not a valid function code (even for custom functions). CUSTOM = 0x00, // The CUSTOM alias should be removed in future. READ_COILS = 0x01, @@ -36,26 +41,38 @@ enum class ModbusFunctionCode : uint8_t { READ_FIFO_QUEUE = 0x18, // not implemented }; -/*Allow direct comparison operators between ModbusFunctionCode and uint8_t*/ -inline bool operator==(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) == rhs; } -inline bool operator==(uint8_t lhs, ModbusFunctionCode rhs) { return lhs == static_cast(rhs); } -inline bool operator!=(ModbusFunctionCode lhs, uint8_t rhs) { return !(static_cast(lhs) == rhs); } -inline bool operator!=(uint8_t lhs, ModbusFunctionCode rhs) { return !(lhs == static_cast(rhs)); } +// Remove before 2027.2.0 +using ModbusFunctionCode ESPDEPRECATED("Use modbus::FunctionCode instead. Removed in 2027.2.0", + "2026.8.0") = FunctionCode; -// 4.3 MODBUS Data model -enum class ModbusRegisterType : uint8_t { +/*Allow direct comparison operators between FunctionCode and uint8_t*/ +inline bool operator==(FunctionCode lhs, uint8_t rhs) { return static_cast(lhs) == rhs; } +inline bool operator==(uint8_t lhs, FunctionCode rhs) { return lhs == static_cast(rhs); } +inline bool operator!=(FunctionCode lhs, uint8_t rhs) { return !(static_cast(lhs) == rhs); } +inline bool operator!=(uint8_t lhs, FunctionCode rhs) { return !(lhs == static_cast(rhs)); } + +// 4.3 MODBUS Data model. "Entity" is the spec's umbrella for the four primary tables; only the +// 16-bit tables are registers (coils and discrete inputs are bits), so the enum is not named +// RegisterType. +enum class EntityType : uint8_t { CUSTOM = 0x00, COIL = 0x01, DISCRETE_INPUT = 0x02, HOLDING = 0x03, - READ = 0x04, + // Named INPUT_REGISTER (not INPUT) because Arduino cores define INPUT as a macro. + INPUT_REGISTER = 0x04, + // Remove before 2027.2.0 + READ ESPDEPRECATED("Use EntityType::INPUT_REGISTER instead. Removed in 2027.2.0", "2026.7.0") = INPUT_REGISTER, }; +// Remove before 2027.2.0 +using ModbusRegisterType ESPDEPRECATED("Use modbus::EntityType instead. Removed in 2027.2.0", "2026.8.0") = EntityType; + // 7 MODBUS Exception Responses: const uint8_t FUNCTION_CODE_MASK = 0x7F; const uint8_t FUNCTION_CODE_EXCEPTION_MASK = 0x80; -enum class ModbusExceptionCode : uint8_t { +enum class ExceptionCode : uint8_t { ILLEGAL_FUNCTION = 0x01, ILLEGAL_DATA_ADDRESS = 0x02, ILLEGAL_DATA_VALUE = 0x03, @@ -67,9 +84,19 @@ enum class ModbusExceptionCode : uint8_t { GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0B, }; +// Remove before 2027.2.0 +using ModbusExceptionCode ESPDEPRECATED("Use modbus::ExceptionCode instead. Removed in 2027.2.0", + "2026.8.0") = ExceptionCode; + +// 6.11 15 (0x0F) Write Multiple Coils +static constexpr uint16_t MAX_NUM_OF_COILS_TO_WRITE = 1968; // 0x7B0 + // 6.12 16 (0x10) Write Multiple registers: static constexpr uint16_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B +// 6.17 23 (0x17) Read/Write Multiple Registers: +static constexpr uint16_t MAX_NUM_OF_REGISTERS_TO_WRITE_RW = 121; // 0x79 + // 6.1 01 (0x01) Read Coils // 6.2 02 (0x02) Read Discrete Inputs static constexpr uint16_t MAX_NUM_OF_COILS_TO_READ = 2000; // 0x7D0 @@ -77,12 +104,77 @@ static constexpr uint16_t MAX_NUM_OF_DISCRETE_INPUTS_TO_READ = 2000; // 0x7D0 // 6.3 03 (0x03) Read Holding Registers // 6.4 04 (0x04) Read Input Registers -static constexpr uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D +static constexpr uint16_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D // Smallest possible frame is 4 bytes (custom function with no data): address(1) + function(1) + CRC(2) static constexpr uint16_t MIN_FRAME_SIZE = 4; +static constexpr uint16_t MIN_PDU_SIZE = 1; static constexpr uint16_t MAX_PDU_SIZE = 253; // Max PDU size is 256 - address(1) - CRC(2) = 253 static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 256 - CRC(2) = 254 +// A read request PDU is always function code(1) + start address(2) + quantity(2) +static constexpr uint16_t READ_PDU_SIZE = 5; +// A single-write PDU is always function code(1) + address(2) + value(2) +static constexpr uint16_t WRITE_SINGLE_PDU_SIZE = 5; static constexpr uint16_t MAX_FRAME_SIZE = 256; +// Both send paths bound their payload so the framed result lands exactly on the RTU limit: a client +// PDU gains an address byte and a CRC, a raw server frame gains a CRC. send_frame_() therefore never +// has to check the framed size - it cannot be exceeded. +static_assert(MAX_PDU_SIZE + 3 == MAX_FRAME_SIZE, "a framed client PDU must fill the RTU frame limit"); +static_assert(MAX_RAW_SIZE + 2 == MAX_FRAME_SIZE, "a framed raw server payload must fill the RTU frame limit"); +/// Bits pack 8 per data byte, rounded up to whole bytes. +constexpr size_t packed_bit_bytes(size_t bits) { return (bits + 7) / 8; } + +/** Read-only view of Modbus-packed bits: bit 0 of byte 0 is the first bit (LSB first), the layout + * coil/discrete-input values use on the wire. Bundles the bit count with the packed bytes so the + * two cannot desynchronize. The view does not own the bytes - it is only valid while they are. + * Reads (operator[]) are unchecked by design - the caller owns the bit < size() precondition, as + * with any subscript. Writes and forwarding are defensive: set() drops out-of-range bits and + * bytes() clamps to the real span, because those paths touch buffers and the wire directly. + */ +class PackedBits { + public: + PackedBits(std::span data, uint16_t count) : data_(data), count_(count) {} + /// Value of the given bit; bit must be < size(). + bool operator[](size_t bit) const { return (this->data_[bit / 8] & (1 << (bit % 8))) != 0; } + /// Number of bits in the view. + uint16_t size() const { return this->count_; } + /// The underlying packed bytes: exactly ceil(size() / 8) bytes, even when the view was constructed + /// over a larger buffer - forwarding this span onto the wire can never leak trailing buffer content. + /// Clamped to the actual span so a view over a too-short buffer stays detectable instead of UB. + std::span bytes() const { + return this->data_.first(std::min(packed_bit_bytes(this->count_), this->data_.size())); + } + + private: + std::span data_; // must cover ceil(count_ / 8) bytes + uint16_t count_; +}; + +/** Mutable counterpart of PackedBits: set() writes bits in place (deliberately no proxy operator[]=). + * Converts implicitly to PackedBits for read access. + */ +class MutablePackedBits { + public: + MutablePackedBits(std::span data, uint16_t count) : data_(data), count_(count) {} + bool operator[](size_t bit) const { return (this->data_[bit / 8] & (1 << (bit % 8))) != 0; } + /// Set or clear the given bit. Out-of-range bits are dropped: on the server read path the span wraps a + /// stack response buffer, so a handler looping past size() must not be able to smash the frame. + void set(size_t bit, bool value) { + if (bit >= this->count_ || bit / 8 >= this->data_.size()) + return; + if (value) { + this->data_[bit / 8] |= (1 << (bit % 8)); + } else { + this->data_[bit / 8] &= ~(1 << (bit % 8)); + } + } + uint16_t size() const { return this->count_; } + operator PackedBits() const { return PackedBits(this->data_, this->count_); } + + private: + std::span data_; // must cover ceil(count_ / 8) bytes + uint16_t count_; +}; + /// End of Modbus definitions } // namespace esphome::modbus diff --git a/esphome/components/modbus/modbus_helpers.cpp b/esphome/components/modbus/modbus_helpers.cpp index 53fa6afacb..8428ea27ea 100644 --- a/esphome/components/modbus/modbus_helpers.cpp +++ b/esphome/components/modbus/modbus_helpers.cpp @@ -7,74 +7,170 @@ namespace esphome::modbus::helpers { static const char *const TAG = "modbus_helpers"; -uint16_t server_frame_length(const uint8_t *frame, size_t size) { - if (size < 2) - return MIN_FRAME_SIZE; - if (is_function_code_exception(frame[1])) { - return 5; // address(1) + function(1) + exception(1) + CRC(2) +// A quantity/address pair is standard when the quantity is non-zero, within the per-table maximum, +// and the range [start_address, start_address + quantity) stays inside the 16-bit address space +// (the 32-bit promotion is the overflow guard - a 16-bit sum could wrap and pass). +static bool quantity_in_range(uint16_t start_address, uint16_t quantity, uint16_t max_quantity) { + return quantity != 0 && quantity <= max_quantity && uint32_t(start_address) + quantity <= 0x10000u; +} + +// The spec allows exactly ON (0xFF00) and OFF (0x0000) for a single-coil value, on the request and +// on its echoed response alike. +static bool is_canonical_coil_value(uint8_t high_byte, uint8_t low_byte) { + return (high_byte == 0xFF || high_byte == 0x00) && low_byte == 0x00; +} + +uint16_t server_pdu_length(const uint8_t *frame, size_t size) { + if (size < MIN_PDU_SIZE) + return MIN_PDU_SIZE; + if (is_function_code_exception(frame[0])) { + return 2; // function(1) + exception(1) } - switch (static_cast(frame[1])) { - case ModbusFunctionCode::READ_COILS: - case ModbusFunctionCode::READ_DISCRETE_INPUTS: - case ModbusFunctionCode::READ_HOLDING_REGISTERS: - case ModbusFunctionCode::READ_INPUT_REGISTERS: - // address(1) + function(1) + byte count(1) + data + CRC(2) - return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0); - case ModbusFunctionCode::WRITE_SINGLE_COIL: - case ModbusFunctionCode::WRITE_SINGLE_REGISTER: - case ModbusFunctionCode::WRITE_MULTIPLE_COILS: - case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS: - return 8; // address(1) + function(1) + output/register address(2) + value(2) + CRC(2) + switch (static_cast(frame[0])) { + case FunctionCode::READ_COILS: + case FunctionCode::READ_DISCRETE_INPUTS: + case FunctionCode::READ_HOLDING_REGISTERS: + case FunctionCode::READ_INPUT_REGISTERS: + // function(1) + byte count(1) + data + return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0); + case FunctionCode::WRITE_SINGLE_COIL: + case FunctionCode::WRITE_SINGLE_REGISTER: + case FunctionCode::WRITE_MULTIPLE_COILS: + case FunctionCode::WRITE_MULTIPLE_REGISTERS: + return 5; // function(1) + output/register address(2) + value(2) // Unsupported function codes. Included here to prevent parser failures. Excluding Serial Line specific functions. - case ModbusFunctionCode::READ_FILE_RECORD: - case ModbusFunctionCode::WRITE_FILE_RECORD: - // address(1) + function(1) + byte count(1) + data + CRC(2) - return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_FRAME_SIZE - 5)) : 0); - case ModbusFunctionCode::MASK_WRITE_REGISTER: - return 10; // address(1) + function(1) + reference address(2) + AND mask(2) + OR mask(2) + CRC(2) - case ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS: - // address(1) + function(1) + byte count(1) + data + CRC(2) - return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0); - case ModbusFunctionCode::READ_FIFO_QUEUE: - // address(1) + function(1) + fifo address(2) CRC(2) - return 6; + case FunctionCode::READ_FILE_RECORD: + case FunctionCode::WRITE_FILE_RECORD: + // function(1) + byte count(1) + data + return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_PDU_SIZE - 2)) : 0); + case FunctionCode::MASK_WRITE_REGISTER: + return 7; // function(1) + reference address(2) + AND mask(2) + OR mask(2) + case FunctionCode::READ_WRITE_MULTIPLE_REGISTERS: + // function(1) + byte count(1) + data + return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0); + case FunctionCode::READ_FIFO_QUEUE: + // function(1) + fifo address(2) + return 3; default: - return MIN_FRAME_SIZE; // unknown length + return MIN_PDU_SIZE; // unknown length } } -uint16_t client_frame_length(const uint8_t *frame, size_t size) { - if (size < 2) - return MIN_FRAME_SIZE; - switch (static_cast(frame[1])) { - case ModbusFunctionCode::READ_COILS: - case ModbusFunctionCode::READ_DISCRETE_INPUTS: - case ModbusFunctionCode::READ_HOLDING_REGISTERS: - case ModbusFunctionCode::READ_INPUT_REGISTERS: - // address(1) + function(1) + start address(2) + quantity(2) + CRC(2) - case ModbusFunctionCode::WRITE_SINGLE_COIL: - case ModbusFunctionCode::WRITE_SINGLE_REGISTER: - return 8; // address(1) + function(1) + output/register address(2) + value(2) + CRC(2) - case ModbusFunctionCode::WRITE_MULTIPLE_COILS: - case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS: - // address(1) + function(1) + start address(2) + quantity(2) + byte count(1) + data + CRC(2) - return 9 + (size > 6 ? std::min(frame[6], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE * 2)) : 0); +uint16_t client_pdu_length(const uint8_t *frame, size_t size) { + if (size < MIN_PDU_SIZE) + return MIN_PDU_SIZE; + switch (static_cast(frame[0])) { + case FunctionCode::READ_COILS: + case FunctionCode::READ_DISCRETE_INPUTS: + case FunctionCode::READ_HOLDING_REGISTERS: + case FunctionCode::READ_INPUT_REGISTERS: + // function(1) + start address(2) + quantity(2) + case FunctionCode::WRITE_SINGLE_COIL: + case FunctionCode::WRITE_SINGLE_REGISTER: + return 5; // function(1) + output/register address(2) + value(2) + case FunctionCode::WRITE_MULTIPLE_COILS: + case FunctionCode::WRITE_MULTIPLE_REGISTERS: + // function(1) + start address(2) + quantity(2) + byte count(1) + data + return 6 + (size > 5 ? std::min(frame[5], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE * 2)) : 0); // Unsupported function codes. Included here to prevent parser failures. Excluding Serial Line specific functions. - case ModbusFunctionCode::READ_FILE_RECORD: - case ModbusFunctionCode::WRITE_FILE_RECORD: - // address(1) + function(1) + byte count(1) + data + CRC(2) - return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_FRAME_SIZE - 5)) : 0); - case ModbusFunctionCode::MASK_WRITE_REGISTER: - return 10; // address(1) + function(1) + reference address(2) + AND mask(2) + OR mask(2) + CRC(2) - case ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS: - // address(1) + function(1) + read start address(2) + read quantity(2) + write start address(2) + - // write quantity(2) + byte count(1) + data + CRC(2) - return 13 + (size > 10 ? std::min(frame[10], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE * 2)) : 0); - case ModbusFunctionCode::READ_FIFO_QUEUE: - // address(1) + function(1) + fifo address(2) CRC(2) - return 6; + case FunctionCode::READ_FILE_RECORD: + case FunctionCode::WRITE_FILE_RECORD: + // function(1) + byte count(1) + data + return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_PDU_SIZE - 2)) : 0); + case FunctionCode::MASK_WRITE_REGISTER: + return 7; // function(1) + reference address(2) + AND mask(2) + OR mask(2) + case FunctionCode::READ_WRITE_MULTIPLE_REGISTERS: + // function(1) + read start address(2) + read quantity(2) + write start address(2) + + // write quantity(2) + byte count(1) + data + return 10 + (size > 9 ? std::min(frame[9], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE_RW * 2)) : 0); + case FunctionCode::READ_FIFO_QUEUE: + // function(1) + fifo address(2) + return 3; default: - return MIN_FRAME_SIZE; // unknown length + return MIN_PDU_SIZE; // unknown length + } +} + +bool is_server_pdu_standard(const uint8_t *pdu, size_t size) { + if (server_pdu_length(pdu, size) != size) + return false; + + const auto function_code = static_cast(pdu[0]); + switch (function_code) { + case FunctionCode::READ_COILS: + case FunctionCode::READ_DISCRETE_INPUTS: + // A conformant bit-read response carries at least one packed byte (up to 2000 bits = 250 bytes). + return pdu[1] != 0 && pdu[1] <= uint8_t(packed_bit_bytes(MAX_NUM_OF_COILS_TO_READ)); + case FunctionCode::READ_HOLDING_REGISTERS: + case FunctionCode::READ_INPUT_REGISTERS: + // Registers are 2 bytes each: the byte count must be a non-zero even count within the read maximum. + return pdu[1] != 0 && pdu[1] % 2 == 0 && pdu[1] <= uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2); + case FunctionCode::READ_FILE_RECORD: + case FunctionCode::WRITE_FILE_RECORD: + return pdu[1] <= uint8_t(MAX_PDU_SIZE - 2); + case FunctionCode::READ_WRITE_MULTIPLE_REGISTERS: + return pdu[1] != 0 && pdu[1] % 2 == 0 && pdu[1] <= uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2); + case FunctionCode::WRITE_MULTIPLE_COILS: + case FunctionCode::WRITE_MULTIPLE_REGISTERS: { + // The response echoes start address and quantity: bound them like the request side does. + const bool bits = function_code == FunctionCode::WRITE_MULTIPLE_COILS; + const uint16_t start_address = get_data(pdu, 1); + const uint16_t quantity = get_data(pdu, 3); + const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_WRITE : MAX_NUM_OF_REGISTERS_TO_WRITE; + return quantity_in_range(start_address, quantity, max_quantity); + } + case FunctionCode::WRITE_SINGLE_COIL: + // The response echoes the request, so the same ON/OFF constraint applies. + return is_canonical_coil_value(pdu[3], pdu[4]); + default: + return true; // All other function codes validated by length alone + } +} + +bool is_client_pdu_standard(const uint8_t *pdu, size_t size) { + if (client_pdu_length(pdu, size) != size) + return false; + + const auto function_code = static_cast(pdu[0]); + switch (function_code) { + case FunctionCode::READ_COILS: + case FunctionCode::READ_DISCRETE_INPUTS: + case FunctionCode::READ_HOLDING_REGISTERS: + case FunctionCode::READ_INPUT_REGISTERS: { + const bool bits = + function_code == FunctionCode::READ_COILS || function_code == FunctionCode::READ_DISCRETE_INPUTS; + const uint16_t start_address = get_data(pdu, 1); + const uint16_t quantity = get_data(pdu, 3); + const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_READ : MAX_NUM_OF_REGISTERS_TO_READ; + return quantity_in_range(start_address, quantity, max_quantity); + } + case FunctionCode::WRITE_MULTIPLE_COILS: + case FunctionCode::WRITE_MULTIPLE_REGISTERS: { + const bool bits = function_code == FunctionCode::WRITE_MULTIPLE_COILS; + const uint16_t start_address = get_data(pdu, 1); + const uint16_t quantity = get_data(pdu, 3); + const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_WRITE : MAX_NUM_OF_REGISTERS_TO_WRITE; + // Coils are packed 8 per data byte; registers are 2 bytes each. + const size_t expected_data_bytes = bits ? packed_bit_bytes(quantity) : quantity * 2; + return quantity_in_range(start_address, quantity, max_quantity) && pdu[5] == expected_data_bytes; + } + case FunctionCode::READ_FILE_RECORD: + case FunctionCode::WRITE_FILE_RECORD: + return pdu[1] <= MAX_PDU_SIZE - 2; + case FunctionCode::READ_WRITE_MULTIPLE_REGISTERS: { + const uint16_t start_address_read = get_data(pdu, 1); + const uint16_t quantity_read = get_data(pdu, 3); + const uint16_t start_address_write = get_data(pdu, 5); + const uint16_t quantity_write = get_data(pdu, 7); + return quantity_in_range(start_address_read, quantity_read, MAX_NUM_OF_REGISTERS_TO_READ) && + quantity_in_range(start_address_write, quantity_write, MAX_NUM_OF_REGISTERS_TO_WRITE_RW) && + pdu[9] == quantity_write * 2; + } + case FunctionCode::WRITE_SINGLE_COIL: + // The one variable field in an otherwise fixed-shape PDU: the spec allows exactly ON/OFF. + return is_canonical_coil_value(pdu[3], pdu[4]); + default: + return true; // All other function codes validated by length alone } } @@ -105,8 +201,8 @@ void log_unsupported_value_type(SensorValueType value_type) { ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast(value_type)); } -int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask, bool *error_return) { +std::optional payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, + uint8_t offset, uint32_t bitmask) { int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits // Validate offset against the buffer for all types, including RAW/unsupported, so @@ -114,9 +210,7 @@ int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sens if (static_cast(offset) > size) { ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast(sensor_value_type), static_cast(offset), size); - if (error_return) - *error_return = true; - return value; + return std::nullopt; } const size_t required_size = required_payload_size(sensor_value_type); @@ -127,9 +221,7 @@ int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sens if (size - offset < required_size) { ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu", static_cast(sensor_value_type), static_cast(offset), size, required_size); - if (error_return) - *error_return = true; - return value; + return std::nullopt; } switch (sensor_value_type) { @@ -179,8 +271,7 @@ int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sens return value; } -int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type, - bool *error_return) { +std::optional registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type) { const size_t required_size = required_payload_size(sensor_value_type); if (required_size == 0) { return 0; // RAW/unsupported: nothing to read @@ -189,9 +280,7 @@ int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValue if (required_words > count) { ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%zu", static_cast(sensor_value_type), count, required_words); - if (error_return) - *error_return = true; - return 0; + return std::nullopt; } // Serialize the needed words back to big-endian bytes and reuse the audited byte decoder so the // sign-extension behaviour stays identical to the wire path. @@ -201,104 +290,255 @@ int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValue bytes[i * 2] = static_cast(reg >> 8); bytes[i * 2 + 1] = static_cast(reg & 0xFF); } - return payload_to_number(bytes, required_size, sensor_value_type, 0, 0xFFFFFFFF, error_return); + return payload_to_number(bytes, required_size, sensor_value_type, 0, 0xFFFFFFFF); } -StaticVector create_client_pdu(ModbusFunctionCode function_code, uint16_t start_address, - uint16_t number_of_entities, const uint8_t *values, - size_t values_len) { +// Every request PDU opens with the same 5-byte layout: function code, then two big-endian 16-bit +// fields (start address + quantity for reads and multi-writes, address + value for single writes). +template +static void append_pdu_header(StaticVector &pdu, FunctionCode function_code, uint16_t first, + uint16_t second) { + pdu.push_back(static_cast(function_code)); + pdu.push_back(first >> 8); + pdu.push_back(first >> 0); + pdu.push_back(second >> 8); + pdu.push_back(second >> 0); +} + +// Zero the unused bits of a multi-coil write's final data byte, as the spec requires. Kept in one +// place so the generic and typed coil builders produce identical wire bytes for the same write. +// The caller must pass a span whose LAST byte is the final packed-bit byte - both builders pass the +// whole PDU, which qualifies because the coil data is always the PDU's tail. +static void mask_trailing_pad_bits(std::span data, uint16_t bit_count) { + if (data.empty() || bit_count % 8 == 0) + return; + data.back() &= static_cast((1 << (bit_count % 8)) - 1); +} + +ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities) { + ReadPdu pdu; // declared before every return so NRVO fires (all paths return the same object) + if (number_of_entities == 0) { + ESP_LOGE(TAG, "Number of entities is zero for function code %02X", static_cast(function_code)); + return pdu; + } + if (uint32_t(start_address) + number_of_entities > 0x10000u) { + ESP_LOGE(TAG, "Read of %u entities at %u runs past the 16-bit address space, dropping request", number_of_entities, + start_address); + return pdu; + } + + switch (function_code) { + case FunctionCode::READ_COILS: + if (number_of_entities > MAX_NUM_OF_COILS_TO_READ) { + ESP_LOGE(TAG, "number_of_entities %u exceeds maximum coils to read %u for function code %02X", + number_of_entities, MAX_NUM_OF_COILS_TO_READ, static_cast(function_code)); + return pdu; + } + break; + case FunctionCode::READ_DISCRETE_INPUTS: + if (number_of_entities > MAX_NUM_OF_DISCRETE_INPUTS_TO_READ) { + ESP_LOGE(TAG, "number_of_entities %u exceeds maximum discrete inputs to read %u for function code %02X", + number_of_entities, MAX_NUM_OF_DISCRETE_INPUTS_TO_READ, static_cast(function_code)); + return pdu; + } + break; + case FunctionCode::READ_HOLDING_REGISTERS: + case FunctionCode::READ_INPUT_REGISTERS: + if (number_of_entities > MAX_NUM_OF_REGISTERS_TO_READ) { + ESP_LOGE(TAG, "number_of_entities %u exceeds maximum registers to read %u for function code %02X", + number_of_entities, MAX_NUM_OF_REGISTERS_TO_READ, static_cast(function_code)); + return pdu; + } + break; + default: + ESP_LOGE(TAG, "Unsupported function code %02X for read PDU creation", static_cast(function_code)); + return pdu; + } + + append_pdu_header(pdu, function_code, start_address, number_of_entities); + return pdu; +} + +PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities, + const uint8_t *values, size_t values_len) { + PduBuffer pdu; // declared before every return so NRVO fires (all paths return the same object) + // Generic entry point; prefer the direction- and type-specific builders (create_read_pdu(), + // create_write_registers_pdu(), etc.) which bound their inputs per spec. if (is_function_code_read(static_cast(function_code))) { if (values != nullptr || values_len > 0) { ESP_LOGW(TAG, "Values provided for read function code %02X, but will be ignored", static_cast(function_code)); } - } else if (is_function_code_write(static_cast(function_code))) { - if (values == nullptr || values_len == 0) { - ESP_LOGE(TAG, "No values provided for write function code %02X", static_cast(function_code)); - return {}; - } - } else { + auto read_pdu = create_read_pdu(function_code, start_address, number_of_entities); + pdu.assign(read_pdu.begin(), read_pdu.end()); + return pdu; + } + // Exact codes only: is_function_code_write() masks the exception bit, which would let the + // exception-flagged forms (0x85/0x86/0x8F/0x90) build a request announcing itself as an exception. + const bool is_single = + function_code == FunctionCode::WRITE_SINGLE_COIL || function_code == FunctionCode::WRITE_SINGLE_REGISTER; + const bool is_multi = + function_code == FunctionCode::WRITE_MULTIPLE_COILS || function_code == FunctionCode::WRITE_MULTIPLE_REGISTERS; + if (!is_single && !is_multi) { ESP_LOGE(TAG, "Unsupported function code %02X for client PDU creation", static_cast(function_code)); - return {}; + return pdu; } + // Generic write builder: raw caller-supplied bytes, so we can only guard against the PDU byte capacity here. + if (values == nullptr || values_len == 0) { + ESP_LOGE(TAG, "No values provided for write function code %02X", static_cast(function_code)); + return pdu; + } if (number_of_entities == 0) { ESP_LOGE(TAG, "Number of entities is zero for function code %02X", static_cast(function_code)); - return {}; + return pdu; + } + // number_of_entities is ignored for single write, so only validate it for the multiple variants. + // The bound is per function code (coils pack 8 per byte, so their quantity limit is far higher) - + // the same limits is_client_pdu_standard() accepts, so builder and validator agree. + const uint16_t max_entities = + function_code == FunctionCode::WRITE_MULTIPLE_COILS ? MAX_NUM_OF_COILS_TO_WRITE : MAX_NUM_OF_REGISTERS_TO_WRITE; + if (!is_single && number_of_entities > max_entities) { + ESP_LOGE(TAG, "number_of_entities %u exceeds maximum %u for function code %02X", number_of_entities, max_entities, + static_cast(function_code)); + return pdu; + } + if (!is_single && uint32_t(start_address) + number_of_entities > 0x10000u) { + ESP_LOGE(TAG, "Write of %u entities at %u runs past the 16-bit address space, dropping request", number_of_entities, + start_address); + return pdu; } - switch (function_code) { - case ModbusFunctionCode::READ_COILS: - if (number_of_entities > MAX_NUM_OF_COILS_TO_READ) { - ESP_LOGE(TAG, "number_of_entities %u exceeds maximum coils to read %u for function code %02X", - number_of_entities, MAX_NUM_OF_COILS_TO_READ, static_cast(function_code)); - return {}; - } - break; - case ModbusFunctionCode::READ_DISCRETE_INPUTS: - if (number_of_entities > MAX_NUM_OF_DISCRETE_INPUTS_TO_READ) { - ESP_LOGE(TAG, "number_of_entities %u exceeds maximum discrete inputs to read %u for function code %02X", - number_of_entities, MAX_NUM_OF_DISCRETE_INPUTS_TO_READ, static_cast(function_code)); - return {}; - } - break; - case ModbusFunctionCode::READ_HOLDING_REGISTERS: - case ModbusFunctionCode::READ_INPUT_REGISTERS: - if (number_of_entities > MAX_NUM_OF_REGISTERS_TO_READ) { - ESP_LOGE(TAG, "number_of_entities %u exceeds maximum registers to read %u for function code %02X", - number_of_entities, MAX_NUM_OF_REGISTERS_TO_READ, static_cast(function_code)); - return {}; - } - break; - case ModbusFunctionCode::WRITE_SINGLE_COIL: - case ModbusFunctionCode::WRITE_SINGLE_REGISTER: - break; // number_of_entities is ignored for single write, so no need to validate - case ModbusFunctionCode::WRITE_MULTIPLE_COILS: - case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS: - if (number_of_entities > MAX_NUM_OF_REGISTERS_TO_WRITE) { - ESP_LOGE(TAG, "number_of_entities %u exceeds maximum registers to write %u for function code %02X", - number_of_entities, MAX_NUM_OF_REGISTERS_TO_WRITE, static_cast(function_code)); - return {}; - } - break; - default: - ESP_LOGE(TAG, "Unsupported function code %u for client PDU creation", static_cast(function_code)); - return {}; - } - - StaticVector pdu; - pdu.push_back(static_cast(function_code)); - pdu.push_back(start_address >> 8); - pdu.push_back(start_address >> 0); - if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL && - function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) { - pdu.push_back(number_of_entities >> 8); - pdu.push_back(number_of_entities >> 0); - } - - if (is_function_code_write(static_cast(function_code))) { - if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS || - function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { - // 6 bytes of overhead (fc + start_addr×2 + qty×2 + byte_count) leave MAX_PDU_SIZE-6 bytes for values - static constexpr size_t MAX_WRITE_MULTIPLE_VALUES_LEN = MAX_PDU_SIZE - 6; - if (values_len > MAX_WRITE_MULTIPLE_VALUES_LEN) { - ESP_LOGE(TAG, "values_len %zu exceeds PDU capacity %zu, dropping request", values_len, - MAX_WRITE_MULTIPLE_VALUES_LEN); - return {}; - } - pdu.push_back(values_len); // Byte count is required for write multiple - for (size_t i = 0; i < values_len; i++) - pdu.push_back(values[i]); - } else { - // Write single register or coil (2 bytes) - if (values_len < 2) { - ESP_LOGE(TAG, "values_len %zu too small for write-single command (need 2), dropping request", values_len); - return {}; - } - pdu.push_back(values[0]); - pdu.push_back(values[1]); + if (is_single) { + // Write single register or coil: the two value bytes are the header's second field. + if (values_len < 2) { + ESP_LOGE(TAG, "values_len %zu too small for write-single command (need 2), dropping request", values_len); + return pdu; } + // The spec allows exactly ON (0xFF00) and OFF (0x0000) for a single-coil write - the same rule + // is_client_pdu_standard() enforces, so a built frame cannot be misclassified on reply. + if (function_code == FunctionCode::WRITE_SINGLE_COIL && !is_canonical_coil_value(values[0], values[1])) { + ESP_LOGE(TAG, "Invalid single-coil value %02X%02X (must be FF00 or 0000), dropping request", values[0], + values[1]); + return pdu; + } + append_pdu_header(pdu, function_code, start_address, uint16_t((values[0] << 8) | values[1])); + return pdu; + } + // The quantity is spec-bounded above, so the data length just has to agree with it exactly + // (registers are 2 bytes each, coils pack 8 per byte). This is the same consistency the response + // dispatch enforces via is_client_pdu_standard(), so a frame built here can never be classified + // non-standard on reply, and the spec bound keeps the PDU within capacity by construction. + // Checked before the header append: a failed check must return an empty PDU, not a 5-byte partial one. + const bool bits = function_code == FunctionCode::WRITE_MULTIPLE_COILS; + const size_t expected_len = bits ? packed_bit_bytes(number_of_entities) : static_cast(number_of_entities) * 2; + if (values_len != expected_len) { + ESP_LOGE(TAG, "values_len %zu does not match %u entities (expected %zu) for function code %02X, dropping request", + values_len, number_of_entities, expected_len, static_cast(function_code)); + return pdu; + } + append_pdu_header(pdu, function_code, start_address, number_of_entities); + pdu.push_back(values_len); // Byte count is required for write multiple + for (size_t i = 0; i < values_len; i++) + pdu.push_back(values[i]); + if (bits) + mask_trailing_pad_bits(pdu, number_of_entities); + return pdu; +} + +PduBuffer create_write_registers_pdu(uint16_t start_address, std::span values) { + PduBuffer pdu; // declared before every return so NRVO fires (all paths return the same object) + if (values.empty()) { + ESP_LOGE(TAG, "No values provided for write multiple registers, dropping request"); + return pdu; + } + // Byte count is registers × 2 (per spec); bounding the register count keeps the PDU within MAX_PDU_SIZE. + if (values.size() > MAX_NUM_OF_REGISTERS_TO_WRITE) { + ESP_LOGE(TAG, "values.size() %zu exceeds maximum registers to write %u, dropping request", values.size(), + MAX_NUM_OF_REGISTERS_TO_WRITE); + return pdu; + } + if (uint32_t(start_address) + values.size() > 0x10000u) { + ESP_LOGE(TAG, "Write of %zu registers at %u runs past the 16-bit address space, dropping request", values.size(), + start_address); + return pdu; + } + append_pdu_header(pdu, FunctionCode::WRITE_MULTIPLE_REGISTERS, start_address, values.size()); + pdu.push_back(static_cast(values.size() * 2)); // byte count + for (auto v : values) { + auto decoded_value = decode_value(v); + pdu.push_back(decoded_value[0]); + pdu.push_back(decoded_value[1]); } return pdu; } + +WriteSinglePdu create_write_single_register_pdu(uint16_t start_address, uint16_t value) { + WriteSinglePdu pdu; + append_pdu_header(pdu, FunctionCode::WRITE_SINGLE_REGISTER, start_address, value); + return pdu; +} + +WriteSinglePdu create_write_single_coil_pdu(uint16_t address, bool value) { + WriteSinglePdu pdu; + append_pdu_header(pdu, FunctionCode::WRITE_SINGLE_COIL, address, value ? 0xFF00 : 0x0000); + return pdu; +} + +// Shared core for the two coil-write overloads: validates, then builds into the caller's named +// pdu (left empty on failure). Each overload's returns all name one local, so NRVO fires. +static void build_write_coils_pdu(PduBuffer &pdu, uint16_t start_address, PackedBits bits) { + const uint16_t count = bits.size(); + const std::span packed_bits = bits.bytes(); + if (count == 0) { + ESP_LOGE(TAG, "No coils requested for write multiple coils, dropping request"); + return; + } + if (count > MAX_NUM_OF_COILS_TO_WRITE) { + ESP_LOGE(TAG, "count %u exceeds maximum coils to write %u, dropping request", count, MAX_NUM_OF_COILS_TO_WRITE); + return; + } + if (uint32_t(start_address) + count > 0x10000u) { + ESP_LOGE(TAG, "Write of %u coils at %u runs past the 16-bit address space, dropping request", count, start_address); + return; + } + const size_t byte_count = packed_bit_bytes(count); + if (packed_bits.size() < byte_count) { + ESP_LOGE(TAG, "packed_bits (%zu bytes) does not cover %u coils (%zu bytes), dropping request", packed_bits.size(), + count, byte_count); + return; + } + append_pdu_header(pdu, FunctionCode::WRITE_MULTIPLE_COILS, start_address, count); + pdu.push_back(static_cast(byte_count)); + for (size_t i = 0; i != byte_count; i++) { + pdu.push_back(packed_bits[i]); + } + mask_trailing_pad_bits(pdu, count); +} + +PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits) { + PduBuffer pdu; + build_write_coils_pdu(pdu, start_address, bits); + return pdu; +} + +PduBuffer create_write_coils_pdu(uint16_t start_address, std::span values) { + PduBuffer pdu; // declared before every return so NRVO fires (all paths return the same object) + // Bound before packing so the transient buffer below cannot overflow; the shared core validates the rest. + if (values.size() > MAX_NUM_OF_COILS_TO_WRITE) { + ESP_LOGE(TAG, "values.size() %zu exceeds maximum coils to write %u, dropping request", values.size(), + MAX_NUM_OF_COILS_TO_WRITE); + return pdu; + } + StaticVector packed; + for (size_t i = 0; i != values.size(); i++) { + if (i % 8 == 0) + packed.push_back(0); + if (values[i]) + packed[i / 8] |= (1 << (i % 8)); + } + build_write_coils_pdu(pdu, start_address, + PackedBits(std::span(packed.data(), packed.size()), values.size())); + return pdu; +} } // namespace esphome::modbus::helpers diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index b7b9020945..3dd933c4d7 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -1,8 +1,10 @@ #pragma once +#include +#include +#include #include #include -#include #include "esphome/core/helpers.h" #include "esphome/components/modbus/modbus_definitions.h" @@ -10,19 +12,19 @@ namespace esphome::modbus::helpers { inline bool is_function_code_read(uint8_t function_code) { - ModbusFunctionCode masked_function_code = static_cast(function_code & FUNCTION_CODE_MASK); - return masked_function_code == ModbusFunctionCode::READ_COILS || - masked_function_code == ModbusFunctionCode::READ_DISCRETE_INPUTS || - masked_function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS || - masked_function_code == ModbusFunctionCode::READ_INPUT_REGISTERS; + FunctionCode masked_function_code = static_cast(function_code & FUNCTION_CODE_MASK); + return masked_function_code == FunctionCode::READ_COILS || + masked_function_code == FunctionCode::READ_DISCRETE_INPUTS || + masked_function_code == FunctionCode::READ_HOLDING_REGISTERS || + masked_function_code == FunctionCode::READ_INPUT_REGISTERS; } inline bool is_function_code_write(uint8_t function_code) { - ModbusFunctionCode masked_function_code = static_cast(function_code & FUNCTION_CODE_MASK); - return masked_function_code == ModbusFunctionCode::WRITE_SINGLE_COIL || - masked_function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER || - masked_function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS || - masked_function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS; + FunctionCode masked_function_code = static_cast(function_code & FUNCTION_CODE_MASK); + return masked_function_code == FunctionCode::WRITE_SINGLE_COIL || + masked_function_code == FunctionCode::WRITE_SINGLE_REGISTER || + masked_function_code == FunctionCode::WRITE_MULTIPLE_COILS || + masked_function_code == FunctionCode::WRITE_MULTIPLE_REGISTERS; } inline bool is_function_code_exception(uint8_t function_code) { @@ -37,28 +39,70 @@ inline bool is_function_code_custom(uint8_t function_code) { masked_function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END); } -// Returns the expected length of a server response frame based on the function code -// If the frame is too short to determine the length, returns the minimum length -uint16_t server_frame_length(const uint8_t *frame, size_t size); +// Returns the expected length of a server response PDU based on the function code. +// If too few bytes have arrived to determine the length, returns the minimum length. `size` is the +// number of bytes available so far, which may exceed the eventual PDU (e.g. include the frame's CRC +// bytes): only fixed header positions are interpreted, so surplus bytes are never misread. +uint16_t server_pdu_length(const uint8_t *frame, size_t size); +// Frame counterpart: address(1) + PDU + CRC(2). Passes every received byte after the address through, +// so header fields (e.g. a byte count) are interpreted as soon as they arrive. +inline uint16_t server_frame_length(const uint8_t *frame, size_t size) { + if (size < 2) + return MIN_FRAME_SIZE; // function code not received yet + return server_pdu_length(frame + 1, size - 1) + 3; +} -// Returns the expected length of a client request frame based on the function code -// If the frame is too short to determine the length, returns the minimum length -uint16_t client_frame_length(const uint8_t *frame, size_t size); +// Returns the expected length of a client request PDU based on the function code. +// Same contract as server_pdu_length(): `size` is bytes available so far, may exceed the PDU. +uint16_t client_pdu_length(const uint8_t *frame, size_t size); +inline uint16_t client_frame_length(const uint8_t *frame, size_t size) { + if (size < 2) + return MIN_FRAME_SIZE; // function code not received yet + return client_pdu_length(frame + 1, size - 1) + 3; +} +// Returns true if pdu is a complete transaction whose shape is consistent with its function code. +// Unlike *_pdu_length(), `size` here is the exact PDU length: a size mismatch is non-conformant. +// Function codes with nothing variable to cross-check are validated by their fixed length alone: the +// single writes (except 0x05's value field, which must be 0x0000 or 0xFF00), mask-write and FIFO, and +// - deliberately - custom/unknown codes and exception responses, so a dispatcher can still route +// them by function code rather than reject them outright. Tests pin this contract. +bool is_server_pdu_standard(const uint8_t *pdu, size_t size); + +// Client counterpart: additionally checks quantity bounds and address-range arithmetic per function code. +// The same acceptance rule applies to custom/unknown function codes. +bool is_client_pdu_standard(const uint8_t *pdu, size_t size); + +// Remove before 2027.2.0 +ESPDEPRECATED("Use server_pdu_payload() on the response PDU instead. Removed in 2027.2.0", "2026.8.0") inline uint8_t server_frame_data_offset(const uint8_t *frame, size_t size) { if (size < 2) return 0; - switch (static_cast(frame[1])) { - case ModbusFunctionCode::READ_COILS: - case ModbusFunctionCode::READ_DISCRETE_INPUTS: - case ModbusFunctionCode::READ_HOLDING_REGISTERS: - case ModbusFunctionCode::READ_INPUT_REGISTERS: + switch (static_cast(frame[1])) { + case FunctionCode::READ_COILS: + case FunctionCode::READ_DISCRETE_INPUTS: + case FunctionCode::READ_HOLDING_REGISTERS: + case FunctionCode::READ_INPUT_REGISTERS: return 3; // address(1) + function(1) + byte count(1) + data + CRC(2) default: return 2; } } +/** Returns the payload portion of a server response PDU: the bytes after the function code, and for the + * standard read responses (0x01-0x04) also after the byte-count byte. Responses to 0x14/0x17 also carry a + * byte-count byte, but those codes are not implemented and their count byte is left in the payload. For + * an exception PDU the payload is the exception code byte (the read check must not see the masked + * function code, or an exception-of-read would classify as a read and return an empty span). Returns an + * empty span if the PDU is too short. + */ +inline std::span server_pdu_payload(std::span pdu) { + if (pdu.empty()) + return {}; + const size_t offset = (!is_function_code_exception(pdu[0]) && is_function_code_read(pdu[0])) ? 2 : 1; + return pdu.size() > offset ? pdu.subspan(offset) : std::span(); +} + inline uint8_t client_frame_data_offset(const uint8_t *, size_t) { return 2; } enum class SensorValueType : uint8_t { @@ -82,32 +126,32 @@ inline bool value_type_is_float(SensorValueType v) { return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; } -inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { +inline FunctionCode modbus_register_read_function(EntityType reg_type) { switch (reg_type) { - case ModbusRegisterType::COIL: - return ModbusFunctionCode::READ_COILS; - case ModbusRegisterType::DISCRETE_INPUT: - return ModbusFunctionCode::READ_DISCRETE_INPUTS; - case ModbusRegisterType::HOLDING: - return ModbusFunctionCode::READ_HOLDING_REGISTERS; - case ModbusRegisterType::READ: - return ModbusFunctionCode::READ_INPUT_REGISTERS; + case EntityType::COIL: + return FunctionCode::READ_COILS; + case EntityType::DISCRETE_INPUT: + return FunctionCode::READ_DISCRETE_INPUTS; + case EntityType::HOLDING: + return FunctionCode::READ_HOLDING_REGISTERS; + case EntityType::INPUT_REGISTER: + return FunctionCode::READ_INPUT_REGISTERS; default: - return ModbusFunctionCode::INVALID; + return FunctionCode::INVALID; } } -inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type, bool multiple = false) { +inline FunctionCode modbus_register_write_function(EntityType reg_type, bool multiple = false) { switch (reg_type) { - case ModbusRegisterType::COIL: - return multiple ? ModbusFunctionCode::WRITE_MULTIPLE_COILS : ModbusFunctionCode::WRITE_SINGLE_COIL; - case ModbusRegisterType::HOLDING: - return multiple ? ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS : ModbusFunctionCode::WRITE_SINGLE_REGISTER; + case EntityType::COIL: + return multiple ? FunctionCode::WRITE_MULTIPLE_COILS : FunctionCode::WRITE_SINGLE_COIL; + case EntityType::HOLDING: + return multiple ? FunctionCode::WRITE_MULTIPLE_REGISTERS : FunctionCode::WRITE_SINGLE_REGISTER; // These register types can't be written (per spec) - case ModbusRegisterType::READ: - case ModbusRegisterType::DISCRETE_INPUT: + case EntityType::INPUT_REGISTER: + case EntityType::DISCRETE_INPUT: default: - return ModbusFunctionCode::INVALID; + return FunctionCode::INVALID; } } @@ -197,11 +241,15 @@ template T get_data(const std::vector &data, size_t buffer_ * @param data modbus response buffer (uint8_t) * @return content of coil register */ -inline bool coil_from_vector(int coil, const std::vector &data) { - auto data_byte = coil / 8; - return (data[data_byte] & (1 << (coil % 8))) > 0; +inline bool bit_from_packed(int bit, std::span data) { + auto data_byte = bit / 8; + return (data[data_byte] & (1 << (bit % 8))) > 0; } +// Remove before 2027.2.0 +ESPDEPRECATED("Use bit_from_packed() instead. Removed in 2027.2.0", "2026.8.0") +inline bool coil_from_vector(int coil, std::span data) { return bit_from_packed(coil, data); } + /** Extract bits from value and shift right according to the bitmask * if the bitmask is 0x00F0 we want the values frrom bit 5 - 8. * the result is then shifted right by the position if the first right set bit in the mask @@ -276,13 +324,21 @@ template void number_to_payload(Container &data, int64_t val * @param bitmask bitmask used for masking and shifting * @return 64-bit number of the payload */ -int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask, bool *error_return = nullptr); +std::optional payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, + uint8_t offset, uint32_t bitmask); -/** Convert vector response payload to number. */ +/** Convert a response payload span to number; std::nullopt if the payload is too short. */ +inline std::optional payload_to_number(std::span data, SensorValueType sensor_value_type, + uint8_t offset, uint32_t bitmask) { + return payload_to_number(data.data(), data.size(), sensor_value_type, offset, bitmask); +} + +// Remove before 2027.2.0 +ESPDEPRECATED("Use the std::span overload returning std::optional instead. Removed in 2027.2.0", "2026.8.0") inline int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask, bool *error_return = nullptr) { - return payload_to_number(data.data(), data.size(), sensor_value_type, offset, bitmask, error_return); + uint32_t bitmask) { + // Released behavior: a too-short payload logs an error and decodes to 0. + return payload_to_number(std::span(data), sensor_value_type, offset, bitmask).value_or(0); } /** Reconstruct a number from register words (host byte order). Inverse of number_to_payload. @@ -292,10 +348,26 @@ inline int64_t payload_to_number(const std::vector &data, SensorValueTy * @param sensor_value_type defines if 16/32/64 bits or FP32 is used * @return 64-bit number of the registers */ -int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type, - bool *error_return = nullptr); +std::optional registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type); -/** Create a modbus clinet pdu for reading/writing single/multiple coils/register/inputs. +// Named PDU buffer types: the builders' storage strategy (currently stack-allocated StaticVector, +// right-sized per shape) can be swapped in one place without touching every signature. +using PduBuffer = StaticVector; +using ReadPdu = StaticVector; +using WriteSinglePdu = StaticVector; + +/** Create a modbus read request PDU. + * @param function_code one of READ_COILS, READ_DISCRETE_INPUTS, READ_HOLDING_REGISTERS, READ_INPUT_REGISTERS + * @param start_address coil/register/input starting address + * @param number_of_entities number of coils/registers/inputs to read + * @return PDU (function code + data, no address, no CRC); empty on invalid input + */ +ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities); + +/** Create a modbus client pdu for reading/writing single/multiple coils/register/inputs. + * Generic entry point; prefer the direction- and type-specific builders (create_read_pdu(), + * create_write_registers_pdu(), create_write_single_register_pdu(), create_write_coils_pdu(), + * create_write_single_coil_pdu()) which bound their inputs per spec. * @param function_code the modbus function code to use. One of: * READ_COILS * READ_DISCRETE_INPUTS @@ -311,11 +383,59 @@ int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValue * @param values_len length of values array * @return PDU (function code + data, no address, no CRC) */ -StaticVector create_client_pdu(ModbusFunctionCode function_code, uint16_t start_address, - uint16_t number_of_entities, const uint8_t *values = nullptr, - size_t values_len = 0); +PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities, + const uint8_t *values = nullptr, size_t values_len = 0); -inline std::vector float_to_payload(float value, SensorValueType value_type) { +/** Create modbus write multiple registers command + * Function 0x10 Write Multiple Registers + * @param start_address modbus address of the first register to write + * @param values register values to write; the register count is values.size() (at most + * MAX_NUM_OF_REGISTERS_TO_WRITE, an over-long set is rejected and an empty PDU is returned). + * Any contiguous uint16_t container converts (std::vector, std::array). + * @return PDU (function code + data, no address, no CRC) + */ +PduBuffer create_write_registers_pdu(uint16_t start_address, std::span values); + +/** Create modbus write single register command + * Function 0x06 Write Single Register + * @param start_address modbus address of the register to write + * @param value uint16_t value to write + * @return PDU (function code + data, no address, no CRC) + */ +WriteSinglePdu create_write_single_register_pdu(uint16_t start_address, uint16_t value); + +/** Create modbus write single coil command + * Function 0x05 Write Single Coil + * @param address modbus address of the coil to write + * @param value coil value to write + * @return PDU (function code + data, no address, no CRC) + */ +WriteSinglePdu create_write_single_coil_pdu(uint16_t address, bool value); + +/** Create modbus write multiple coils command + * Function 0x0F Write Multiple Coils + * @param start_address modbus address of the first coil to write + * @param values coil values to write; the coil count is values.size() (at most MAX_NUM_OF_COILS_TO_WRITE, an + * over-long set is rejected and an empty PDU is returned). Note std::vector is bit-packed and + * does not convert to a span; pass a std::array or other contiguous bool container. + * @return PDU (function code + data, no address, no CRC) + */ +PduBuffer create_write_coils_pdu(uint16_t start_address, std::span values); + +/** Create modbus write multiple coils command (function 0x0F) from bits packed as on the wire. + * @param start_address modbus address of the first coil to write + * @param bits PackedBits view of the coils to write (at most MAX_NUM_OF_COILS_TO_WRITE); invalid + * input returns an empty PDU + * @return PDU (function code + data, no address, no CRC) + */ +PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits); + +/** Append a float converted to register words to any push_back container (heap-free with StaticVector). + * @param data container the register words are appended to + * @param value value to convert + * @param value_type defines if 16/32/64 bits or FP32 is used + */ +template void float_to_payload(Container &data, float value, SensorValueType value_type) { int64_t val; if (value_type_is_float(value_type)) { @@ -324,8 +444,14 @@ inline std::vector float_to_payload(float value, SensorValueType value val = llroundf(value); } - std::vector data; number_to_payload(data, val, value_type); +} + +// Remove before 2027.2.0 +ESPDEPRECATED("Use the container overload of float_to_payload() instead. Removed in 2027.2.0", "2026.8.0") +inline std::vector float_to_payload(float value, SensorValueType value_type) { + std::vector data; + float_to_payload(data, value, value_type); return data; } diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index cdbba54c1f..35a5479ecb 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -6,7 +6,7 @@ from esphome.components import modbus from esphome.components.modbus.helpers import ( MODBUS_REGISTER_TYPE, TYPE_REGISTER_MAP, - ModbusRegisterType, + EntityType, ) import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_NAME, CONF_OFFSET @@ -217,13 +217,13 @@ async def register_modbus_device(var, config): def function_code_to_register(function_code): FUNCTION_CODE_TYPE_MAP = { - "read_coils": ModbusRegisterType.COIL, - "read_discrete_inputs": ModbusRegisterType.DISCRETE_INPUT, - "read_holding_registers": ModbusRegisterType.HOLDING, - "read_input_registers": ModbusRegisterType.READ, - "write_single_coil": ModbusRegisterType.COIL, - "write_single_register": ModbusRegisterType.HOLDING, - "write_multiple_coils": ModbusRegisterType.COIL, - "write_multiple_registers": ModbusRegisterType.HOLDING, + "read_coils": EntityType.COIL, + "read_discrete_inputs": EntityType.DISCRETE_INPUT, + "read_holding_registers": EntityType.HOLDING, + "read_input_registers": EntityType.INPUT_REGISTER, + "write_single_coil": EntityType.COIL, + "write_single_register": EntityType.HOLDING, + "write_multiple_coils": EntityType.COIL, + "write_multiple_registers": EntityType.HOLDING, } return FUNCTION_CODE_TYPE_MAP[function_code] diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp index 60c19bb66a..d3caaaa3d9 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp @@ -11,10 +11,10 @@ void ModbusBinarySensor::parse_and_publish(const std::vector &data) { bool value; switch (this->register_type) { - case ModbusRegisterType::DISCRETE_INPUT: - case ModbusRegisterType::COIL: + case modbus::EntityType::DISCRETE_INPUT: + case modbus::EntityType::COIL: // offset for coil is the actual number of the coil not the byte offset - value = modbus::helpers::coil_from_vector(this->offset, data); + value = modbus::helpers::bit_from_packed(this->offset, data); break; default: value = modbus::helpers::get_data(data, this->offset) & this->bitmask; diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h index 98c6840e15..f56a32a5ec 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -8,9 +8,9 @@ namespace esphome::modbus_controller { -class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, public SensorItem { +class ModbusBinarySensor final : public Component, public binary_sensor::BinarySensor, public SensorItem { public: - ModbusBinarySensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + ModbusBinarySensor(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; @@ -20,7 +20,7 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, this->skip_updates = skip_updates; this->force_new_range = force_new_range; - if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) { + if (register_type == modbus::EntityType::COIL || register_type == modbus::EntityType::DISCRETE_INPUT) { this->register_count = offset + 1; } else { this->register_count = 1; diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 9246239ef9..8822b7b40a 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -57,7 +57,7 @@ bool ModbusController::send_next_command_() { } // Queue incoming response -void ModbusController::on_modbus_data(const std::vector &data) { +void ModbusController::on_response(std::span request_pdu, std::span response_pdu) { if (this->command_queue_.empty()) { ESP_LOGW(TAG, "Received modbus data but command queue is empty"); return; @@ -78,8 +78,10 @@ void ModbusController::on_modbus_data(const std::vector &data) { this->online_callback_.call((int) current_command->function_code, current_command->register_address); } - // Move the commandItem to the response queue - current_command->payload = data; + // Move the commandItem to the response queue. The span points into the hub's receive buffer, so + // copy the payload into the command for deferred processing in loop(). + auto data = modbus::helpers::server_pdu_payload(response_pdu); + current_command->payload.assign(data.begin(), data.end()); this->incoming_queue_.push(std::move(current_command)); ESP_LOGV(TAG, "Modbus response queued"); this->command_queue_.pop_front(); @@ -93,8 +95,11 @@ void ModbusController::process_modbus_data_(const ModbusCommandItem *response) { response->on_data_func(response->register_type, response->register_address, response->payload); } -void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) { - ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code); +void ModbusController::on_error(std::span request_pdu, modbus::ExceptionCode exception_code) { + // The request function code (request_pdu[0]) already carries what the log needs; the exception bit only + // ever appears on the response, so no masking is needed here. + const uint8_t function_code = request_pdu.empty() ? 0 : request_pdu[0]; + ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, static_cast(exception_code)); if (this->command_queue_.empty()) { return; } @@ -111,7 +116,7 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_ } } -SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const { +SensorSet ModbusController::find_sensors_(modbus::EntityType register_type, uint16_t start_address) const { auto reg_it = std::find_if( std::begin(this->register_ranges_), std::end(this->register_ranges_), [=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); }); @@ -125,7 +130,7 @@ SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint // not found return {}; } -void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address, +void ModbusController::on_register_data(modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address); @@ -159,18 +164,18 @@ void ModbusController::update_range_(RegisterRange &r) { r.skip_updates_counter); if (r.skip_updates_counter == 0) { // if a custom command is used the user supplied custom_data is only available in the SensorItem. - if (r.register_type == ModbusRegisterType::CUSTOM) { + if (r.register_type == modbus::EntityType::CUSTOM) { auto sensors = this->find_sensors_(r.register_type, r.start_address); if (!sensors.empty()) { auto sensor = sensors.cbegin(); auto command_item = ModbusCommandItem::create_custom_command( this, (*sensor)->custom_data, - [this](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { - this->on_register_data(ModbusRegisterType::CUSTOM, start_address, data); + [this](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { + this->on_register_data(modbus::EntityType::CUSTOM, start_address, data); }); command_item.register_address = (*sensor)->start_address; command_item.register_count = (*sensor)->register_count; - command_item.function_code = ModbusFunctionCode::CUSTOM; + command_item.function_code = FunctionCode::CUSTOM; queue_command(command_item); } } else { @@ -232,7 +237,7 @@ size_t ModbusController::create_register_ranges_() { // this is not the first register in range so it might be possible // to reuse the last register or extend the current range if (!curr->force_new_range && r.register_type == curr->register_type && - curr->register_type != ModbusRegisterType::CUSTOM) { + curr->register_type != modbus::EntityType::CUSTOM) { if (curr->start_address == (r.start_address + r.register_count - prev->register_count) && curr->register_count == prev->register_count && curr->get_register_size() == prev->get_register_size()) { // this register can re-use the data from the previous register @@ -342,7 +347,7 @@ void ModbusController::loop() { } } -void ModbusController::on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, +void ModbusController::on_write_register_response(modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { ESP_LOGV(TAG, "Command ACK 0x%X %d ", modbus::helpers::get_data(data, 0), modbus::helpers::get_data(data, 1)); @@ -357,8 +362,8 @@ void ModbusController::dump_sensors_() { } ModbusCommandItem ModbusCommandItem::create_read_command( - ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count, - std::function &data)> + ModbusController *modbusdevice, modbus::EntityType register_type, uint16_t start_address, uint16_t register_count, + std::function &data)> &&handler) { ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; @@ -371,7 +376,7 @@ ModbusCommandItem ModbusCommandItem::create_read_command( } ModbusCommandItem ModbusCommandItem::create_read_command(ModbusController *modbusdevice, - ModbusRegisterType register_type, uint16_t start_address, + modbus::EntityType register_type, uint16_t start_address, uint16_t register_count) { ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; @@ -379,7 +384,7 @@ ModbusCommandItem ModbusCommandItem::create_read_command(ModbusController *modbu cmd.function_code = modbus::helpers::modbus_register_read_function(register_type); cmd.register_address = start_address; cmd.register_count = register_count; - cmd.on_data_func = [modbusdevice](ModbusRegisterType register_type, uint16_t start_address, + cmd.on_data_func = [modbusdevice](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { modbusdevice->on_register_data(register_type, start_address, data); }; @@ -391,11 +396,11 @@ ModbusCommandItem ModbusCommandItem::create_write_multiple_command(ModbusControl const std::vector &values) { ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; - cmd.register_type = ModbusRegisterType::HOLDING; - cmd.function_code = ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS; + cmd.register_type = modbus::EntityType::HOLDING; + cmd.function_code = FunctionCode::WRITE_MULTIPLE_REGISTERS; cmd.register_address = start_address; cmd.register_count = register_count; - cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + cmd.on_data_func = [modbusdevice, cmd](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { modbusdevice->on_write_register_response(cmd.register_type, start_address, data); }; @@ -411,11 +416,11 @@ ModbusCommandItem ModbusCommandItem::create_write_single_coil(ModbusController * bool value) { ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; - cmd.register_type = ModbusRegisterType::COIL; - cmd.function_code = ModbusFunctionCode::WRITE_SINGLE_COIL; + cmd.register_type = modbus::EntityType::COIL; + cmd.function_code = FunctionCode::WRITE_SINGLE_COIL; cmd.register_address = address; cmd.register_count = 1; - cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + cmd.on_data_func = [modbusdevice, cmd](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { modbusdevice->on_write_register_response(cmd.register_type, start_address, data); }; @@ -428,11 +433,11 @@ ModbusCommandItem ModbusCommandItem::create_write_multiple_coils(ModbusControlle const std::vector &values) { ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; - cmd.register_type = ModbusRegisterType::COIL; - cmd.function_code = ModbusFunctionCode::WRITE_MULTIPLE_COILS; + cmd.register_type = modbus::EntityType::COIL; + cmd.function_code = FunctionCode::WRITE_MULTIPLE_COILS; cmd.register_address = start_address; cmd.register_count = values.size(); - cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + cmd.on_data_func = [modbusdevice, cmd](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { modbusdevice->on_write_register_response(cmd.register_type, start_address, data); }; @@ -460,11 +465,11 @@ ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusControlle uint16_t value) { ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; - cmd.register_type = ModbusRegisterType::HOLDING; - cmd.function_code = ModbusFunctionCode::WRITE_SINGLE_REGISTER; + cmd.register_type = modbus::EntityType::HOLDING; + cmd.function_code = FunctionCode::WRITE_SINGLE_REGISTER; cmd.register_address = start_address; cmd.register_count = 1; // not used here anyways - cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + cmd.on_data_func = [modbusdevice, cmd](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { modbusdevice->on_write_register_response(cmd.register_type, start_address, data); }; @@ -477,13 +482,13 @@ ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusControlle ModbusCommandItem ModbusCommandItem::create_custom_command( ModbusController *modbusdevice, const std::vector &values, - std::function &data)> + std::function &data)> &&handler) { ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; - cmd.function_code = ModbusFunctionCode::CUSTOM; + cmd.function_code = FunctionCode::CUSTOM; if (handler == nullptr) { - cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + cmd.on_data_func = [](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { ESP_LOGI(TAG, "Custom Command sent"); }; } else { @@ -496,13 +501,13 @@ ModbusCommandItem ModbusCommandItem::create_custom_command( ModbusCommandItem ModbusCommandItem::create_custom_command( ModbusController *modbusdevice, const std::vector &values, - std::function &data)> + std::function &data)> &&handler) { ModbusCommandItem cmd = {}; cmd.modbusdevice = modbusdevice; - cmd.function_code = ModbusFunctionCode::CUSTOM; + cmd.function_code = FunctionCode::CUSTOM; if (handler == nullptr) { - cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + cmd.on_data_func = [](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { ESP_LOGI(TAG, "Custom Command sent"); }; } else { @@ -517,9 +522,10 @@ ModbusCommandItem ModbusCommandItem::create_custom_command( } bool ModbusCommandItem::send() { - if (this->function_code != ModbusFunctionCode::CUSTOM) { - modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(), - this->payload.empty() ? nullptr : &this->payload[0]); + if (this->function_code != FunctionCode::CUSTOM) { + modbusdevice->send_pdu( + modbus::helpers::create_client_pdu(this->function_code, this->register_address, this->register_count, + this->payload.empty() ? nullptr : &this->payload[0], this->payload.size())); } else { modbusdevice->send_raw(this->payload); } @@ -532,7 +538,7 @@ bool ModbusCommandItem::send() { bool ModbusCommandItem::is_equal(const ModbusCommandItem &other) { // for custom commands we have to check for identical payloads, since // address/count/type fields will be set to zero - return this->function_code == ModbusFunctionCode::CUSTOM + return this->function_code == FunctionCode::CUSTOM ? this->payload == other.payload : other.register_address == this->register_address && other.register_count == this->register_count && other.register_type == this->register_type && other.function_code == this->function_code; diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 4f674b2675..3c789936af 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -16,22 +17,29 @@ namespace esphome::modbus_controller { class ModbusController; +using modbus::ExceptionCode; +using modbus::FunctionCode; +using modbus::helpers::SensorValueType; + +// Remove before 2027.2.0 - deprecated names re-exported so external components keep their warning window +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +using modbus::ModbusExceptionCode; using modbus::ModbusFunctionCode; using modbus::ModbusRegisterType; -using modbus::ModbusExceptionCode; -using modbus::helpers::SensorValueType; +#pragma GCC diagnostic pop // Remove before 2026.10.0 — these helpers have moved to modbus::helpers ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0") inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); } ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0") -inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { +inline FunctionCode modbus_register_read_function(modbus::EntityType reg_type) { return modbus::helpers::modbus_register_read_function(reg_type); } ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0") -inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) { +inline FunctionCode modbus_register_write_function(modbus::EntityType reg_type) { return modbus::helpers::modbus_register_write_function(reg_type); } @@ -64,9 +72,10 @@ T get_data(const std::vector &data, size_t buffer_offset) { return modbus::helpers::get_data(data, buffer_offset); } -ESPDEPRECATED("Use modbus::helpers::coil_from_vector() instead. Removed in 2026.10.0", "2026.4.0") +// Remove before 2027.2.0 (window restarted when the migration target changed to bit_from_packed()) +ESPDEPRECATED("Use modbus::helpers::bit_from_packed() instead. Removed in 2027.2.0", "2026.4.0") inline bool coil_from_vector(int coil, const std::vector &data) { - return modbus::helpers::coil_from_vector(coil, data); + return modbus::helpers::bit_from_packed(coil, data); } template @@ -83,12 +92,15 @@ inline void number_to_payload(std::vector &data, int64_t value, Sensor ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0") inline int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, uint32_t bitmask) { - return modbus::helpers::payload_to_number(data, sensor_value_type, offset, bitmask); + return modbus::helpers::payload_to_number(std::span(data), sensor_value_type, offset, bitmask) + .value_or(0); } ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0") inline std::vector float_to_payload(float value, SensorValueType value_type) { - return modbus::helpers::float_to_payload(value, value_type); + std::vector data; + modbus::helpers::float_to_payload(data, value, value_type); + return data; } class ModbusController; @@ -99,7 +111,7 @@ class SensorItem { void set_custom_data(const std::vector &data) { custom_data = data; } size_t virtual get_register_size() const { - if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) { + if (register_type == modbus::EntityType::COIL || register_type == modbus::EntityType::DISCRETE_INPUT) { return 1; } else { // if CONF_RESPONSE_BYTES is used override the default return response_bytes > 0 ? response_bytes : register_count * 2; @@ -107,7 +119,7 @@ class SensorItem { } // Override register size for modbus devices not using 1 register for one dword void set_register_size(uint8_t register_size) { response_bytes = register_size; } - ModbusRegisterType register_type{ModbusRegisterType::CUSTOM}; + modbus::EntityType register_type{modbus::EntityType::CUSTOM}; SensorValueType sensor_value_type{SensorValueType::RAW}; uint16_t start_address{0}; uint32_t bitmask{0}; @@ -154,7 +166,7 @@ using SensorSet = std::set; struct RegisterRange { uint16_t start_address; - ModbusRegisterType register_type; + modbus::EntityType register_type; uint8_t register_count; uint16_t skip_updates; // the config value SensorSet sensors; // all sensors of this range @@ -167,9 +179,9 @@ class ModbusCommandItem { ModbusController *modbusdevice{nullptr}; uint16_t register_address{0}; uint16_t register_count{0}; - ModbusFunctionCode function_code{ModbusFunctionCode::CUSTOM}; - ModbusRegisterType register_type{ModbusRegisterType::CUSTOM}; - std::function &data)> + FunctionCode function_code{FunctionCode::CUSTOM}; + modbus::EntityType register_type{modbus::EntityType::CUSTOM}; + std::function &data)> on_data_func; std::vector payload = {}; bool send(); @@ -187,8 +199,8 @@ class ModbusCommandItem { * @return ModbusCommandItem with the prepared command */ static ModbusCommandItem create_read_command( - ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count, - std::function &data)> + ModbusController *modbusdevice, modbus::EntityType register_type, uint16_t start_address, uint16_t register_count, + std::function &data)> &&handler); /** Create modbus read command * Function code 02-04 @@ -198,7 +210,7 @@ class ModbusCommandItem { * @param register_count number of registers to read * @return ModbusCommandItem with the prepared command */ - static ModbusCommandItem create_read_command(ModbusController *modbusdevice, ModbusRegisterType register_type, + static ModbusCommandItem create_read_command(ModbusController *modbusdevice, modbus::EntityType register_type, uint16_t start_address, uint16_t register_count); /** Create modbus read command * Function code 02-04 @@ -248,7 +260,7 @@ class ModbusCommandItem { */ static ModbusCommandItem create_custom_command( ModbusController *modbusdevice, const std::vector &values, - std::function &data)> + std::function &data)> &&handler = nullptr); /** Create custom modbus command @@ -260,7 +272,7 @@ class ModbusCommandItem { */ static ModbusCommandItem create_custom_command( ModbusController *modbusdevice, const std::vector &values, - std::function &data)> + std::function &data)> &&handler = nullptr); bool is_equal(const ModbusCommandItem &other); @@ -279,7 +291,7 @@ class ModbusCommandItem { * Responses for the commands are dispatched to the modbus sensor items. */ -class ModbusController : public PollingComponent, public modbus::ModbusClientDevice { +class ModbusController final : public PollingComponent, public modbus::ModbusClientDevice { public: void dump_config() override; void loop() override; @@ -288,17 +300,26 @@ class ModbusController : public PollingComponent, public modbus::ModbusClientDev /// queues a modbus command in the send queue void queue_command(const ModbusCommandItem &command); + /// Sends a raw payload (address byte + PDU, no CRC) with responses routed back to this controller. + /// The payload carries its own address byte, which may differ from this controller's address. + /// Deliberately shadows the deprecated ModbusClientDevice::send_raw() with identical semantics: + /// controller-level raw sends stay supported until the command machinery is replaced. + void send_raw(const std::vector &payload) { + if (payload.empty()) + return; // refused at the door, like every invalid send; no callback follows + this->parent_->send_pdu(payload[0], std::span(payload).subspan(1), this); + } /// Registers a sensor with the controller. Called by esphomes code generator void add_sensor_item(SensorItem *item) { sensorset_.insert(item); } /// called when a modbus response was parsed without errors - void on_modbus_data(const std::vector &data) override; + void on_response(std::span request_pdu, std::span response_pdu) override; /// called when a modbus error response was received - void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; + void on_error(std::span request_pdu, modbus::ExceptionCode exception_code) override; /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue - void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); + void on_register_data(modbus::EntityType register_type, uint16_t start_address, const std::vector &data); /// default delegate called by process_modbus_data when a response for a write response has retrieved from the /// incoming queue - void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, + void on_write_register_response(modbus::EntityType register_type, uint16_t start_address, const std::vector &data); /// Allow a duplicate command to be sent void set_allow_duplicate_commands(bool allow_duplicate_commands) { @@ -335,7 +356,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusClientDev /// parse sensormap_ and create range of sequential addresses size_t create_register_ranges_(); // find register in sensormap. Returns iterator with all registers having the same start address - SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const; + SensorSet find_sensors_(modbus::EntityType register_type, uint16_t start_address) const; /// submit the read command for the address range to the send queue void update_range_(RegisterRange &r); /// parse incoming modbus data @@ -377,8 +398,9 @@ class ModbusController : public PollingComponent, public modbus::ModbusClientDev * @param item SensorItem object * @return float value of data */ -inline float payload_to_float(const std::vector &data, const SensorItem &item) { - int64_t number = modbus::helpers::payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); +inline float payload_to_float(std::span data, const SensorItem &item) { + int64_t number = + modbus::helpers::payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask).value_or(0); float float_value; if (modbus::helpers::value_type_is_float(item.sensor_value_type)) { diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index 2c81dd6830..7b18b9e9fc 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -57,11 +57,12 @@ void ModbusNumber::control(float value) { format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); write_cmd = ModbusCommandItem::create_custom_command( this->parent_, data, - [this, write_cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + [this, write_cmd](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data); }); } else { - data = modbus::helpers::float_to_payload(write_value, this->sensor_value_type); + std::vector payload; + modbus::helpers::float_to_payload(payload, write_value, this->sensor_value_type); ESP_LOGD(TAG, "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", @@ -71,13 +72,13 @@ void ModbusNumber::control(float value) { if (this->register_count == 1 && !this->use_write_multiple_) { // since offset is in bytes and a register is 16 bits we get the start by adding offset/2 write_cmd = ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset / 2, - data[0]); + payload[0]); } else { write_cmd = ModbusCommandItem::create_write_multiple_command( - this->parent_, this->start_address + this->offset / 2, this->register_count, data); + this->parent_, this->start_address + this->offset / 2, this->register_count, payload); } // publish new value - write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address, + write_cmd.on_data_func = [this, write_cmd, value](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { // gets called when the write command is ack'd from the device this->parent_->on_write_register_response(write_cmd.register_type, start_address, data); diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index dd8f418bfc..582b042caf 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -10,9 +10,9 @@ namespace esphome::modbus_controller { using value_to_data_t = std::function(float); -class ModbusNumber : public number::Number, public Component, public SensorItem { +class ModbusNumber final : public number::Number, public Component, public SensorItem { public: - ModbusNumber(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + ModbusNumber(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, SensorValueType value_type, int register_count, uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp index 504e09a093..95618a7505 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -33,12 +33,30 @@ void ModbusFloatOutput::write_state(float value) { } // lambda didn't set payload if (data.empty()) { - data = modbus::helpers::float_to_payload(value, this->sensor_value_type); + modbus::helpers::float_to_payload(data, value, this->sensor_value_type); } ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)", this->start_address, this->register_count, value, original_value); + // The command declares register_count registers, so the payload must be exactly that many words; + // anything else would put a byte count on the wire that disagrees with the quantity field. + // number_to_payload() appends nothing for RAW, so an empty payload must be caught before data[0]. + if (data.empty()) { + ESP_LOGW(TAG, "No payload was created for updating output"); + return; + } + + // register_count declares the READ range width - it may pull neighboring registers into one poll - + // so a write covers exactly the registers the value occupies: the quantity comes from the payload, + // never from register_count (padding to it would zero registers the user only declared for reading). + // A payload wider than the declared range means the config and the lambda disagree - drop it. + if (data.size() > this->register_count) { + ESP_LOGE(TAG, "Payload has %zu registers but register_count is %u; dropping write", data.size(), + this->register_count); + return; + } + // Create and send the write command ModbusCommandItem write_cmd; if (this->register_count == 1 && !this->use_write_multiple_) { @@ -46,7 +64,7 @@ void ModbusFloatOutput::write_state(float value) { ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset, data[0]); } else { write_cmd = ModbusCommandItem::create_write_multiple_command(this->parent_, this->start_address + this->offset, - this->register_count, data); + data.size(), data); } this->parent_->queue_command(write_cmd); } @@ -89,7 +107,7 @@ void ModbusBinaryOutput::write_state(bool state) { format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); cmd = ModbusCommandItem::create_custom_command( this->parent_, data, - [this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + [this, cmd](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { this->parent_->on_write_register_response(cmd.register_type, this->start_address, data); }); } else { diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index c5323e3bf3..c9efd42224 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -8,10 +8,10 @@ namespace esphome::modbus_controller { -class ModbusFloatOutput : public output::FloatOutput, public Component, public SensorItem { +class ModbusFloatOutput final : public output::FloatOutput, public Component, public SensorItem { public: ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) { - this->register_type = ModbusRegisterType::HOLDING; + this->register_type = modbus::EntityType::HOLDING; this->start_address = start_address; this->offset = offset; this->bitmask = 0xFFFFFFFF; @@ -41,10 +41,10 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S bool use_write_multiple_{false}; }; -class ModbusBinaryOutput : public output::BinaryOutput, public Component, public SensorItem { +class ModbusBinaryOutput final : public output::BinaryOutput, public Component, public SensorItem { public: ModbusBinaryOutput(uint16_t start_address, uint8_t offset) { - this->register_type = ModbusRegisterType::COIL; + this->register_type = modbus::EntityType::COIL; this->start_address = start_address; this->bitmask = 0xFFFFFFFF; this->sensor_value_type = SensorValueType::BIT; diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 859828f5f6..daa6b10da4 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -8,7 +8,9 @@ static const char *const TAG = "modbus_controller.select"; void ModbusSelect::dump_config() { LOG_SELECT(TAG, "Modbus Controller Select", this); } void ModbusSelect::parse_and_publish(const std::vector &data) { - int64_t value = modbus::helpers::payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask); + int64_t value = modbus::helpers::payload_to_number(std::span(data), this->sensor_value_type, + this->offset, this->bitmask) + .value_or(0); ESP_LOGD(TAG, "New select value %lld from payload", value); @@ -70,13 +72,26 @@ void ModbusSelect::control(size_t index) { return; } + // The command declares register_count registers, so the payload must be exactly that many words: + // a value type narrower than the declared width is zero-padded (the config deliberately allows + // register_count larger than the value type). Anything else would put a byte count on the wire + // that disagrees with the quantity field, which conformant devices reject. + // register_count declares the READ range width - it may pull neighboring registers into one poll - + // so a write covers exactly the registers the value occupies: the quantity comes from the payload, + // never from register_count (padding to it would zero registers the user only declared for reading). + // A payload wider than the declared range means the config and the lambda disagree - drop it. + if (data.size() > this->register_count) { + ESP_LOGE(TAG, "Payload has %zu registers but register_count is %u; dropping write", data.size(), + this->register_count); + return; + } + const uint16_t write_address = this->start_address + this->offset / 2; ModbusCommandItem write_cmd; if ((this->register_count == 1) && (!this->use_write_multiple_)) { write_cmd = ModbusCommandItem::create_write_single_command(this->parent_, write_address, data[0]); } else { - write_cmd = - ModbusCommandItem::create_write_multiple_command(this->parent_, write_address, this->register_count, data); + write_cmd = ModbusCommandItem::create_write_multiple_command(this->parent_, write_address, data.size(), data); } this->parent_->queue_command(write_cmd); diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index a736abd0db..b4834ba4c6 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -9,11 +9,11 @@ namespace esphome::modbus_controller { -class ModbusSelect : public Component, public select::Select, public SensorItem { +class ModbusSelect final : public Component, public select::Select, public SensorItem { public: ModbusSelect(SensorValueType sensor_value_type, uint16_t start_address, uint8_t register_count, uint16_t skip_updates, bool force_new_range, std::vector mapping) { - this->register_type = ModbusRegisterType::HOLDING; // not configurable + this->register_type = modbus::EntityType::HOLDING; // not configurable this->sensor_value_type = sensor_value_type; this->start_address = start_address; this->offset = 0; // not configurable diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h index 2e6967b07c..1d11aa4d66 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.h +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -8,9 +8,9 @@ namespace esphome::modbus_controller { -class ModbusSensor : public Component, public sensor::Sensor, public SensorItem { +class ModbusSensor final : public Component, public sensor::Sensor, public SensorItem { public: - ModbusSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + ModbusSensor(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, SensorValueType value_type, int register_count, uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index 044ca2f8cc..b8cdbf018d 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -30,10 +30,10 @@ bool ModbusSwitch::assumed_state() { return this->assumed_state_; } void ModbusSwitch::parse_and_publish(const std::vector &data) { bool value = false; switch (this->register_type) { - case ModbusRegisterType::DISCRETE_INPUT: - case ModbusRegisterType::COIL: + case modbus::EntityType::DISCRETE_INPUT: + case modbus::EntityType::COIL: // offset for coil is the actual number of the coil not the byte offset - value = modbus::helpers::coil_from_vector(this->offset, data); + value = modbus::helpers::bit_from_packed(this->offset, data); break; default: value = modbus::helpers::get_data(data, this->offset) & this->bitmask; @@ -82,13 +82,13 @@ void ModbusSwitch::write_state(bool state) { format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); cmd = ModbusCommandItem::create_custom_command( this->parent_, data, - [this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + [this, cmd](modbus::EntityType register_type, uint16_t start_address, const std::vector &data) { this->parent_->on_write_register_response(cmd.register_type, this->start_address, data); }); } else { ESP_LOGV(TAG, "write_state '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(), ONOFF(state), (int) this->register_type, this->start_address, this->offset); - if (this->register_type == ModbusRegisterType::COIL) { + if (this->register_type == modbus::EntityType::COIL) { // offset for coil and discrete inputs is the coil/register number not bytes if (this->use_write_multiple_) { std::vector states{state}; diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index 541a23706d..0d5456aa63 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -8,9 +8,9 @@ namespace esphome::modbus_controller { -class ModbusSwitch : public Component, public switch_::Switch, public SensorItem { +class ModbusSwitch final : public Component, public switch_::Switch, public SensorItem { public: - ModbusSwitch(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + ModbusSwitch(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; @@ -19,7 +19,7 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem this->sensor_value_type = SensorValueType::BIT; this->skip_updates = skip_updates; this->register_count = 1; - if (register_type == ModbusRegisterType::HOLDING || register_type == ModbusRegisterType::COIL) { + if (register_type == modbus::EntityType::HOLDING || register_type == modbus::EntityType::COIL) { this->start_address += offset; this->offset = 0; } diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index a99fea5860..c7381d7ddd 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -10,9 +10,9 @@ namespace esphome::modbus_controller { enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2, ANSI = 3 }; -class ModbusTextSensor : public Component, public text_sensor::TextSensor, public SensorItem { +class ModbusTextSensor final : public Component, public text_sensor::TextSensor, public SensorItem { public: - ModbusTextSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint8_t register_count, + ModbusTextSensor(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint8_t register_count, uint16_t response_bytes, RawEncoding encode, uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; diff --git a/esphome/components/modbus_server/modbus_server.cpp b/esphome/components/modbus_server/modbus_server.cpp index 44b1b160a5..e649635848 100644 --- a/esphome/components/modbus_server/modbus_server.cpp +++ b/esphome/components/modbus_server/modbus_server.cpp @@ -3,7 +3,7 @@ #include "esphome/core/log.h" namespace esphome::modbus_server { -using modbus::ModbusExceptionCode; +using modbus::ExceptionCode; using modbus::helpers::registers_to_number; static const char *const TAG = "modbus_server"; @@ -27,9 +27,8 @@ ServerRegister *ModbusServer::find_containing_register_(uint32_t address) const return nullptr; } -modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t start_address, - uint16_t number_of_registers, - modbus::RegisterValues ®isters) { +modbus::ResponseStatus ModbusServer::on_read_registers(uint16_t start_address, uint16_t number_of_registers, + modbus::RegisterValues ®isters) { ESP_LOGV(TAG, "Received read holding/input registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%X.", this->address_, start_address, number_of_registers); @@ -51,13 +50,13 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t sta } ESP_LOGW(TAG, "No register at 0x%04X and courtesy default not allowed. Sending exception response.", static_cast(current_address)); - return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS; + return ExceptionCode::ILLEGAL_DATA_ADDRESS; } if (!server_register->read_lambda) { // Registered but not readable (write-only); don't mask it with the courtesy default. ESP_LOGW(TAG, "Register at 0x%04X is not readable. Sending exception response.", server_register->address); - return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS; + return ExceptionCode::ILLEGAL_DATA_ADDRESS; } // A multi-register value is normally atomic: the request must start at its first register and cover all of @@ -73,7 +72,7 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t sta "Read clips the multi-register value at 0x%04X, which does not allow partial reads. " "Sending exception response.", server_register->address); - return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS; + return ExceptionCode::ILLEGAL_DATA_ADDRESS; } int64_t value = server_register->read_lambda(); @@ -90,7 +89,7 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t sta // The value encoded to fewer words than its register span (e.g. a RAW register); treat as a device fault. ESP_LOGE(TAG, "Register at 0x%04X did not encode to %u registers", server_register->address, server_register->register_count); - return ModbusExceptionCode::SERVICE_DEVICE_FAILURE; + return ExceptionCode::SERVICE_DEVICE_FAILURE; } for (uint16_t i = 0; i < take; i++) { registers.push_back(value_words[value_offset + i]); @@ -101,8 +100,8 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t sta return {}; } -modbus::ServerResponseStatus ModbusServer::on_modbus_write_registers(uint16_t start_address, - const modbus::RegisterValues ®isters) { +modbus::ResponseStatus ModbusServer::on_write_registers(uint16_t start_address, + const modbus::RegisterValues ®isters) { // registers holds the values to write in host byte order; its size is the register count. ESP_LOGV(TAG, "Received write registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%zX.", this->address_, start_address, registers.size()); @@ -133,16 +132,15 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_write_registers(uint16_t st // so we never apply a partial write before discovering a problem. The commit pass below re-runs // registers_to_number rather than caching the decoded values: using the same function for the check and // the write keeps a single source of truth for the decode bound, independent of how register_count was set. - ModbusExceptionCode precheck = ModbusExceptionCode::ILLEGAL_DATA_ADDRESS; // unmatched or unwritable register + ExceptionCode precheck = ExceptionCode::ILLEGAL_DATA_ADDRESS; // unmatched or unwritable register if (!for_each_register([&precheck, ®isters](ServerRegister *server_register, uint16_t register_offset) -> bool { if (server_register->write_lambda == nullptr) { return false; // unwritable -> ILLEGAL_DATA_ADDRESS } - bool error = false; - registers_to_number(registers.data() + register_offset, registers.size() - register_offset, - server_register->value_type, &error); - if (error) { - precheck = ModbusExceptionCode::ILLEGAL_DATA_VALUE; // request doesn't supply the full value + if (!registers_to_number(registers.data() + register_offset, registers.size() - register_offset, + server_register->value_type) + .has_value()) { + precheck = ExceptionCode::ILLEGAL_DATA_VALUE; // request doesn't supply the full value return false; } return true; @@ -155,11 +153,12 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_write_registers(uint16_t st // rejecting the value at runtime -- which cannot be rolled back. if (!for_each_register([®isters](ServerRegister *server_register, uint16_t register_offset) { int64_t number = registers_to_number(registers.data() + register_offset, registers.size() - register_offset, - server_register->value_type); + server_register->value_type) + .value_or(0); return server_register->write_lambda(number); })) { ESP_LOGW(TAG, "A register write callback failed mid-sequence; earlier writes were already applied."); - return ModbusExceptionCode::SERVICE_DEVICE_FAILURE; + return ExceptionCode::SERVICE_DEVICE_FAILURE; } // Success: the caller builds the write response (an echo of the request header). diff --git a/esphome/components/modbus_server/modbus_server.h b/esphome/components/modbus_server/modbus_server.h index f68d1c4a30..4fddd9854d 100644 --- a/esphome/components/modbus_server/modbus_server.h +++ b/esphome/components/modbus_server/modbus_server.h @@ -95,18 +95,17 @@ class ServerRegister { WriteLambda write_lambda; }; -class ModbusServer : public Component, public modbus::ModbusServerDevice { +class ModbusServer final : public Component, public modbus::ModbusServerDevice { public: void dump_config() override; /// Registers a server register with the controller. Called by esphomes code generator void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); } /// called when a modbus request (function code 0x03 or 0x04) was parsed without errors - modbus::ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers, - modbus::RegisterValues ®isters) final; + modbus::ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, + modbus::RegisterValues ®isters) final; /// called when a modbus request (function code 0x06 or 0x10) was parsed without errors - modbus::ServerResponseStatus on_modbus_write_registers(uint16_t start_address, - const modbus::RegisterValues ®isters) final; + modbus::ResponseStatus on_write_registers(uint16_t start_address, const modbus::RegisterValues ®isters) final; /// Called by esphome generated code to set the server courtesy response object void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) { this->server_courtesy_response_ = server_courtesy_response; diff --git a/esphome/components/monochromatic/monochromatic_light_output.h b/esphome/components/monochromatic/monochromatic_light_output.h index 458140ef09..eb81a10ee4 100644 --- a/esphome/components/monochromatic/monochromatic_light_output.h +++ b/esphome/components/monochromatic/monochromatic_light_output.h @@ -6,7 +6,7 @@ namespace esphome::monochromatic { -class MonochromaticLightOutput : public light::LightOutput { +class MonochromaticLightOutput final : public light::LightOutput { public: void set_output(output::FloatOutput *output) { output_ = output; } light::LightTraits get_traits() override { diff --git a/esphome/components/mopeka_ble/mopeka_ble.h b/esphome/components/mopeka_ble/mopeka_ble.h index cc91ef17d6..e6fae23aee 100644 --- a/esphome/components/mopeka_ble/mopeka_ble.h +++ b/esphome/components/mopeka_ble/mopeka_ble.h @@ -9,7 +9,7 @@ namespace esphome::mopeka_ble { -class MopekaListener : public esp32_ble_tracker::ESPBTDeviceListener { +class MopekaListener final : public esp32_ble_tracker::ESPBTDeviceListener { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void set_show_sensors_without_sync(bool show_sensors_without_sync) { diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index bfdfe80c48..40fb338350 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -27,7 +27,7 @@ enum SensorType { // measurement may be inaccurate. enum SensorReadQuality { QUALITY_HIGH = 0x3, QUALITY_MED = 0x2, QUALITY_LOW = 0x1, QUALITY_ZERO = 0x0 }; -class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class MopekaProCheck final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index a38abeabf0..2f1681f6ea 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -42,7 +42,7 @@ struct mopeka_std_package { // NOLINT(readability-identifier-naming,altera-stru mopeka_std_values val[3]; } __attribute__((packed)); -class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class MopekaStdCheck final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; diff --git a/esphome/components/motion/motion_component.h b/esphome/components/motion/motion_component.h index 00310c16fe..b0a074a17c 100644 --- a/esphome/components/motion/motion_component.h +++ b/esphome/components/motion/motion_component.h @@ -85,7 +85,7 @@ class MotionComponent : public PollingComponent { // --- Actions --- -template class CalibrateLevelAction : public Action { +template class CalibrateLevelAction final : public Action { public: explicit CalibrateLevelAction(MotionComponent *parent) : parent_(parent) {} void set_save(bool save) { this->save_ = save; } @@ -110,7 +110,7 @@ template class CalibrateLevelAction : public Action { bool save_{false}; }; -template class CalibrateHeadingAction : public Action { +template class CalibrateHeadingAction final : public Action { public: explicit CalibrateHeadingAction(MotionComponent *parent) : parent_(parent) {} void set_save(bool save) { this->save_ = save; } @@ -135,7 +135,7 @@ template class CalibrateHeadingAction : public Action { bool save_{false}; }; -template class ClearCalibrationAction : public Action { +template class ClearCalibrationAction final : public Action { public: explicit ClearCalibrationAction(MotionComponent *parent) : parent_(parent) {} void set_save(bool save) { this->save_ = save; } diff --git a/esphome/components/mpl3115a2/mpl3115a2.h b/esphome/components/mpl3115a2/mpl3115a2.h index d78c9d571c..a6163673cb 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.h +++ b/esphome/components/mpl3115a2/mpl3115a2.h @@ -80,7 +80,7 @@ enum { MPL3115A2_CTRL_REG1_OS128 = 0x38, }; -class MPL3115A2Component : public PollingComponent, public i2c::I2CDevice { +class MPL3115A2Component final : public PollingComponent, public i2c::I2CDevice { public: void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_altitude(sensor::Sensor *altitude) { altitude_ = altitude; } diff --git a/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h index 5fa10bf598..c0a4a36f1f 100644 --- a/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h +++ b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h @@ -6,7 +6,9 @@ namespace esphome::mpr121 { -class MPR121BinarySensor : public binary_sensor::BinarySensor, public MPR121Channel, public Parented { +class MPR121BinarySensor final : public binary_sensor::BinarySensor, + public MPR121Channel, + public Parented { public: void set_channel(uint8_t channel) { this->channel_ = channel; } void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; }; diff --git a/esphome/components/mpr121/mpr121.h b/esphome/components/mpr121/mpr121.h index 54b5c8abf4..64c4b291b3 100644 --- a/esphome/components/mpr121/mpr121.h +++ b/esphome/components/mpr121/mpr121.h @@ -57,7 +57,7 @@ class MPR121Channel { virtual void process(uint16_t data) = 0; }; -class MPR121Component : public Component, public i2c::I2CDevice { +class MPR121Component final : public Component, public i2c::I2CDevice { public: void register_channel(MPR121Channel *channel) { this->channels_.push_back(channel); } void set_touch_debounce(uint8_t debounce); @@ -102,7 +102,7 @@ class MPR121Component : public Component, public i2c::I2CDevice { }; /// Helper class to expose a MPR121 pin as an internal input GPIO pin. -class MPR121GPIOPin : public GPIOPin { +class MPR121GPIOPin final : public GPIOPin { public: void setup() override; void pin_mode(gpio::Flags flags) override; diff --git a/esphome/components/mpu6050/mpu6050.h b/esphome/components/mpu6050/mpu6050.h index bac07cb4a5..4410bf0164 100644 --- a/esphome/components/mpu6050/mpu6050.h +++ b/esphome/components/mpu6050/mpu6050.h @@ -6,7 +6,7 @@ namespace esphome::mpu6050 { -class MPU6050Component : public PollingComponent, public i2c::I2CDevice { +class MPU6050Component final : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/mpu6886/mpu6886.h b/esphome/components/mpu6886/mpu6886.h index a23858a7b7..b795d5f690 100644 --- a/esphome/components/mpu6886/mpu6886.h +++ b/esphome/components/mpu6886/mpu6886.h @@ -6,7 +6,7 @@ namespace esphome::mpu6886 { -class MPU6886Component : public PollingComponent, public i2c::I2CDevice { +class MPU6886Component final : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h index 89a0ff1be8..b2da7ed6a2 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.h +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { +class MQTTAlarmControlPanelComponent final : public mqtt::MQTTComponent { public: explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel); diff --git a/esphome/components/mqtt/mqtt_binary_sensor.h b/esphome/components/mqtt/mqtt_binary_sensor.h index 5917a9966c..75c224c65a 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.h +++ b/esphome/components/mqtt/mqtt_binary_sensor.h @@ -9,7 +9,7 @@ namespace esphome::mqtt { -class MQTTBinarySensorComponent : public mqtt::MQTTComponent { +class MQTTBinarySensorComponent final : public mqtt::MQTTComponent { public: /** Construct a MQTTBinarySensorComponent. * diff --git a/esphome/components/mqtt/mqtt_button.h b/esphome/components/mqtt/mqtt_button.h index a2db64d39d..7e2c77e29b 100644 --- a/esphome/components/mqtt/mqtt_button.h +++ b/esphome/components/mqtt/mqtt_button.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTButtonComponent : public mqtt::MQTTComponent { +class MQTTButtonComponent final : public mqtt::MQTTComponent { public: explicit MQTTButtonComponent(button::Button *button); diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 14473f737a..f741be561c 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -99,7 +99,7 @@ enum MQTTClientState { class MQTTComponent; -class MQTTClientComponent : public Component { +class MQTTClientComponent final : public Component { public: MQTTClientComponent(); @@ -340,7 +340,7 @@ class MQTTClientComponent : public Component { extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class MQTTMessageTrigger : public Trigger, public Component { +class MQTTMessageTrigger final : public Trigger, public Component { public: explicit MQTTMessageTrigger(std::string topic); @@ -356,7 +356,7 @@ class MQTTMessageTrigger : public Trigger, public Component { optional payload_; }; -class MQTTJsonMessageTrigger : public Trigger { +class MQTTJsonMessageTrigger final : public Trigger { public: explicit MQTTJsonMessageTrigger(const std::string &topic, uint8_t qos) { global_mqtt_client->subscribe_json( @@ -364,21 +364,21 @@ class MQTTJsonMessageTrigger : public Trigger { } }; -class MQTTConnectTrigger : public Trigger { +class MQTTConnectTrigger final : public Trigger { public: explicit MQTTConnectTrigger(MQTTClientComponent *client) { client->set_on_connect([this](bool session_present) { this->trigger(session_present); }); } }; -class MQTTDisconnectTrigger : public Trigger { +class MQTTDisconnectTrigger final : public Trigger { public: explicit MQTTDisconnectTrigger(MQTTClientComponent *client) { client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(reason); }); } }; -template class MQTTPublishAction : public Action { +template class MQTTPublishAction final : public Action { public: MQTTPublishAction(MQTTClientComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, topic) @@ -395,7 +395,7 @@ template class MQTTPublishAction : public Action { MQTTClientComponent *parent_; }; -template class MQTTPublishJsonAction : public Action { +template class MQTTPublishJsonAction final : public Action { public: MQTTPublishJsonAction(MQTTClientComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, topic) @@ -417,7 +417,7 @@ template class MQTTPublishJsonAction : public Action { MQTTClientComponent *parent_; }; -template class MQTTConnectedCondition : public Condition { +template class MQTTConnectedCondition final : public Condition { public: MQTTConnectedCondition(MQTTClientComponent *parent) : parent_(parent) {} bool check(const Ts &...x) override { return this->parent_->is_connected(); } @@ -426,7 +426,7 @@ template class MQTTConnectedCondition : public Condition MQTTClientComponent *parent_; }; -template class MQTTEnableAction : public Action { +template class MQTTEnableAction final : public Action { public: MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {} @@ -436,7 +436,7 @@ template class MQTTEnableAction : public Action { MQTTClientComponent *parent_; }; -template class MQTTDisableAction : public Action { +template class MQTTDisableAction final : public Action { public: MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {} diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index f0715929d4..b862db85ae 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTClimateComponent : public mqtt::MQTTComponent { +class MQTTClimateComponent final : public mqtt::MQTTComponent { public: MQTTClimateComponent(climate::Climate *device); void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index afc514609c..3bbc1cdfa3 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -319,7 +319,7 @@ bool MQTTComponent::send_discovery_() { device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; #if defined(USE_ESP8266) || defined(USE_ESP32) device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif"; -#elif defined(USE_RP2040) +#elif defined(USE_RP2) device_info[MQTT_DEVICE_MANUFACTURER] = "Raspberry Pi"; #elif defined(USE_BK72XX) device_info[MQTT_DEVICE_MANUFACTURER] = "Beken"; diff --git a/esphome/components/mqtt/mqtt_cover.h b/esphome/components/mqtt/mqtt_cover.h index f801af5d12..3b07733993 100644 --- a/esphome/components/mqtt/mqtt_cover.h +++ b/esphome/components/mqtt/mqtt_cover.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTCoverComponent : public mqtt::MQTTComponent { +class MQTTCoverComponent final : public mqtt::MQTTComponent { public: explicit MQTTCoverComponent(cover::Cover *cover); diff --git a/esphome/components/mqtt/mqtt_date.h b/esphome/components/mqtt/mqtt_date.h index 4a626becb2..1c24422856 100644 --- a/esphome/components/mqtt/mqtt_date.h +++ b/esphome/components/mqtt/mqtt_date.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTDateComponent : public mqtt::MQTTComponent { +class MQTTDateComponent final : public mqtt::MQTTComponent { public: /** Construct this MQTTDateComponent instance with the provided friendly_name and date * diff --git a/esphome/components/mqtt/mqtt_datetime.h b/esphome/components/mqtt/mqtt_datetime.h index d02d6f579c..09af806fe3 100644 --- a/esphome/components/mqtt/mqtt_datetime.h +++ b/esphome/components/mqtt/mqtt_datetime.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTDateTimeComponent : public mqtt::MQTTComponent { +class MQTTDateTimeComponent final : public mqtt::MQTTComponent { public: /** Construct this MQTTDateTimeComponent instance with the provided friendly_name and time * diff --git a/esphome/components/mqtt/mqtt_event.h b/esphome/components/mqtt/mqtt_event.h index e6d5b6f278..424de3f603 100644 --- a/esphome/components/mqtt/mqtt_event.h +++ b/esphome/components/mqtt/mqtt_event.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTEventComponent : public mqtt::MQTTComponent { +class MQTTEventComponent final : public mqtt::MQTTComponent { public: explicit MQTTEventComponent(event::Event *event); diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index 43ef67e733..ff984bb77d 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTFanComponent : public mqtt::MQTTComponent { +class MQTTFanComponent final : public mqtt::MQTTComponent { public: explicit MQTTFanComponent(fan::Fan *state); diff --git a/esphome/components/mqtt/mqtt_light.h b/esphome/components/mqtt/mqtt_light.h index 41981655ef..2ca8d70dd4 100644 --- a/esphome/components/mqtt/mqtt_light.h +++ b/esphome/components/mqtt/mqtt_light.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTJSONLightComponent : public mqtt::MQTTComponent, public light::LightRemoteValuesListener { +class MQTTJSONLightComponent final : public mqtt::MQTTComponent, public light::LightRemoteValuesListener { public: explicit MQTTJSONLightComponent(light::LightState *state); diff --git a/esphome/components/mqtt/mqtt_lock.h b/esphome/components/mqtt/mqtt_lock.h index 666882c73d..7f36a51789 100644 --- a/esphome/components/mqtt/mqtt_lock.h +++ b/esphome/components/mqtt/mqtt_lock.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTLockComponent : public mqtt::MQTTComponent { +class MQTTLockComponent final : public mqtt::MQTTComponent { public: explicit MQTTLockComponent(lock::Lock *a_lock); diff --git a/esphome/components/mqtt/mqtt_number.h b/esphome/components/mqtt/mqtt_number.h index 021a539988..5e21544691 100644 --- a/esphome/components/mqtt/mqtt_number.h +++ b/esphome/components/mqtt/mqtt_number.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTNumberComponent : public mqtt::MQTTComponent { +class MQTTNumberComponent final : public mqtt::MQTTComponent { public: /** Construct this MQTTNumberComponent instance with the provided friendly_name and number * diff --git a/esphome/components/mqtt/mqtt_select.h b/esphome/components/mqtt/mqtt_select.h index aaf174ff72..46140ad456 100644 --- a/esphome/components/mqtt/mqtt_select.h +++ b/esphome/components/mqtt/mqtt_select.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTSelectComponent : public mqtt::MQTTComponent { +class MQTTSelectComponent final : public mqtt::MQTTComponent { public: /** Construct this MQTTSelectComponent instance with the provided friendly_name and select * diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index e8202aa8e2..1d5ee8095c 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTSensorComponent : public mqtt::MQTTComponent { +class MQTTSensorComponent final : public mqtt::MQTTComponent { public: /** Construct this MQTTSensorComponent instance with the provided friendly_name and sensor * diff --git a/esphome/components/mqtt/mqtt_switch.h b/esphome/components/mqtt/mqtt_switch.h index 5f6cb841fd..f35784ed5c 100644 --- a/esphome/components/mqtt/mqtt_switch.h +++ b/esphome/components/mqtt/mqtt_switch.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTSwitchComponent : public mqtt::MQTTComponent { +class MQTTSwitchComponent final : public mqtt::MQTTComponent { public: explicit MQTTSwitchComponent(switch_::Switch *a_switch); diff --git a/esphome/components/mqtt/mqtt_text.h b/esphome/components/mqtt/mqtt_text.h index 8ae0b9e29a..d42eefc690 100644 --- a/esphome/components/mqtt/mqtt_text.h +++ b/esphome/components/mqtt/mqtt_text.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTTextComponent : public mqtt::MQTTComponent { +class MQTTTextComponent final : public mqtt::MQTTComponent { public: /** Construct this MQTTTextComponent instance with the provided friendly_name and text * diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index d8f9315c1e..1fe9651fa1 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTTextSensor : public mqtt::MQTTComponent { +class MQTTTextSensor final : public mqtt::MQTTComponent { public: explicit MQTTTextSensor(text_sensor::TextSensor *sensor); diff --git a/esphome/components/mqtt/mqtt_time.h b/esphome/components/mqtt/mqtt_time.h index cf5780da2d..3e60176e90 100644 --- a/esphome/components/mqtt/mqtt_time.h +++ b/esphome/components/mqtt/mqtt_time.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTTimeComponent : public mqtt::MQTTComponent { +class MQTTTimeComponent final : public mqtt::MQTTComponent { public: /** Construct this MQTTTimeComponent instance with the provided friendly_name and time * diff --git a/esphome/components/mqtt/mqtt_update.h b/esphome/components/mqtt/mqtt_update.h index ec1adb1fcd..04b0b09da1 100644 --- a/esphome/components/mqtt/mqtt_update.h +++ b/esphome/components/mqtt/mqtt_update.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTUpdateComponent : public mqtt::MQTTComponent { +class MQTTUpdateComponent final : public mqtt::MQTTComponent { public: explicit MQTTUpdateComponent(update::UpdateEntity *update); diff --git a/esphome/components/mqtt/mqtt_valve.h b/esphome/components/mqtt/mqtt_valve.h index d3b724a8ba..dd2cca514a 100644 --- a/esphome/components/mqtt/mqtt_valve.h +++ b/esphome/components/mqtt/mqtt_valve.h @@ -10,7 +10,7 @@ namespace esphome::mqtt { -class MQTTValveComponent : public mqtt::MQTTComponent { +class MQTTValveComponent final : public mqtt::MQTTComponent { public: explicit MQTTValveComponent(valve::Valve *valve); diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h index 229c0586ab..739e8456ee 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h @@ -10,7 +10,7 @@ namespace esphome::mqtt_subscribe { -class MQTTSubscribeSensor : public sensor::Sensor, public Component { +class MQTTSubscribeSensor final : public sensor::Sensor, public Component { public: void set_parent(mqtt::MQTTClientComponent *parent) { parent_ = parent; } void set_topic(const std::string &topic) { topic_ = topic; } diff --git a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h index f218bf2a8a..8641825fca 100644 --- a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h +++ b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h @@ -10,7 +10,7 @@ namespace esphome::mqtt_subscribe { -class MQTTSubscribeTextSensor : public text_sensor::TextSensor, public Component { +class MQTTSubscribeTextSensor final : public text_sensor::TextSensor, public Component { public: void set_parent(mqtt::MQTTClientComponent *parent) { parent_ = parent; } void set_topic(const std::string &topic) { topic_ = topic; } diff --git a/esphome/components/ms5611/ms5611.h b/esphome/components/ms5611/ms5611.h index c6ad5b231a..535acdd357 100644 --- a/esphome/components/ms5611/ms5611.h +++ b/esphome/components/ms5611/ms5611.h @@ -6,7 +6,7 @@ namespace esphome::ms5611 { -class MS5611Component : public PollingComponent, public i2c::I2CDevice { +class MS5611Component final : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index d2683e4bba..24e9aa45e1 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,13 +1,20 @@ import ipaddress import logging +from typing import Any import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.psram import is_guaranteed as psram_is_guaranteed from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv -from esphome.const import CONF_ENABLE_IPV6, CONF_ID, CONF_MIN_IPV6_ADDR_COUNT +from esphome.const import ( + CONF_ENABLE_IPV6, + CONF_ID, + CONF_MIN_IPV6_ADDR_COUNT, + CONF_PRIORITY, +) from esphome.core import CORE, CoroPriority, coroutine_with_priority +import esphome.final_validate as fv from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -20,11 +27,67 @@ _LOGGER = logging.getLogger(__name__) KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking" CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" +# Network priority tracking infrastructure +# Components can query this to determine their relative setup priority. +# CORE.data[KEY_NETWORK_PRIORITY] is a list of dicts of the form +# {"interface": "ethernet"}, in user-declared order. +KEY_NETWORK_PRIORITY = "network_priority" + +# Only interfaces whose component already calls get_network_priority() are +# accepted in the priority list. openthread and modem will be added here when +# they wire up their setup-priority consumer in their own to_code — see +# NETWORK_PLAN.md for the full multi-interface roadmap. +VALID_NETWORK_TYPES = ["ethernet", "wifi"] + +# Setup priority base values — first in list gets the highest priority. +# +# The base equals the historical setup_priority::WIFI / ::ETHERNET default +# (250.0), so a single-entry priority list yields exactly the same setup order +# as a config with no priority block. Subsequent entries step down by a small +# amount to break ties without crossing other priority bands. +# +# Important: must stay strictly less than setup_priority::AFTER_BLUETOOTH +# (300.0), which NetworkComponent itself uses — otherwise the highest-priority +# interface could tie with NetworkComponent and run before esp_netif_init(). +NETWORK_PRIORITY_BASE = 250.0 +NETWORK_PRIORITY_STEP = 5.0 + +# Lower-bound guard. The lowest-priority entry gets +# NETWORK_PRIORITY_BASE - (len - 1) * NETWORK_PRIORITY_STEP, which must stay +# strictly above setup_priority::AFTER_WIFI (200.0, see esphome/core/component.h) +# so a long priority list never drops an interface into the band used by +# components that expect to run after the network is up. There is ample headroom +# for the two types today; this check raises if a future expansion of +# VALID_NETWORK_TYPES would silently cross that band. Uses an explicit raise +# rather than a bare assert so the guard isn't stripped under python -O/-OO. +_SETUP_PRIORITY_AFTER_WIFI = 200.0 +if ( + NETWORK_PRIORITY_BASE - (len(VALID_NETWORK_TYPES) - 1) * NETWORK_PRIORITY_STEP + <= _SETUP_PRIORITY_AFTER_WIFI +): + raise ValueError( + "network: priority: list is long enough to cross setup_priority::AFTER_WIFI" + ) + network_ns = cg.esphome_ns.namespace("network") NetworkComponent = network_ns.class_("NetworkComponent", cg.Component) IPAddress = network_ns.class_("IPAddress") +def _register_provisioning_source(config: ConfigType) -> ConfigType: + """Register network connectivity as a provisioning source. + + The network component is auto-loaded whenever an interface (wifi, ethernet, ...) + is configured, so a device with connectivity always has this source: it is + considered provisioned once it has connected via any interface, and + `provisioning:` is valid without another source. + """ + from esphome.components import provisioning + + provisioning.register_source("network") + return config + + def ip_address_literal(ip: str | int | None) -> cg.MockObj: """Generate an IPAddress with compile-time initialization instead of runtime parsing. @@ -59,6 +122,19 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj: return IPAddress(str(ip)) +def add_use_address(var: cg.MockObj, use_address: str) -> None: + """Generate a set_use_address() call only when the address must be baked in. + + The default ".local" is not stored in the firmware; it is rebuilt at + runtime from the device name (see network::get_use_address_to()), which also + picks up the MAC suffix when name_add_mac_suffix is enabled. A compile-time + string could never include that suffix, so baking it in would log the wrong + address. + """ + if use_address != f"{CORE.name}.local": + cg.add(var.set_use_address(use_address)) + + def require_high_performance_networking() -> None: """Request high performance networking for network and WiFi. @@ -115,44 +191,161 @@ def validate_ipv6(value: bool) -> bool: return value -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(NetworkComponent), - cv.SplitDefault( - CONF_ENABLE_IPV6, - bk72xx=False, - esp32=False, - esp8266=False, - host=False, - rp2040=False, - nrf52=True, - ): cv.All( - cv.boolean, - cv.Any( - cv.require_framework_version( - bk72xx_arduino=cv.Version(1, 7, 0), - esp_idf=cv.Version(0, 0, 0), - esp32_arduino=cv.Version(0, 0, 0), - esp8266_arduino=cv.Version(0, 0, 0), - host=cv.Version(0, 0, 0), - rp2040_arduino=cv.Version(0, 0, 0), - nrf52_zephyr=cv.Version(0, 0, 0), - ), - cv.boolean_false, - ), - validate_ipv6, - ), - cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, - cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32), +def get_network_priority(iface: str) -> float | None: + """Get the setup priority for the given network interface type. + + Returns the float setup priority for ``iface`` based on the order declared + under ``network: priority:``. Interfaces listed first receive a higher + setup priority so they are initialised before lower-priority ones. + + If no ``network: priority:`` has been configured this returns ``None`` and + the calling component should fall back to its own default setup priority. + + Args: + iface: Interface type string (case-insensitive). Currently ``"ethernet"`` + or ``"wifi"`` — the only types the priority-list validator + accepts; ``"openthread"`` / ``"modem"`` are planned but not yet + supported. An interface not present in the configured list + returns ``None``. + + Returns: + float setup priority, or None if no priority list was configured. + + Example usage inside a component's ``to_code``. Emit the override before + ``register_component`` so an explicit ``setup_priority:`` on the component + still wins:: + + from esphome.components import network + + async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + prio = network.get_network_priority("ethernet") + if prio is not None: + cg.set_setup_priority(var, prio) + + await cg.register_component(var, config) + ... + """ + priority_list = CORE.data.get(KEY_NETWORK_PRIORITY) + if priority_list is None: + return None + iface_lower = iface.lower() + for idx, entry in enumerate(priority_list): + if entry["interface"] == iface_lower: + return NETWORK_PRIORITY_BASE - (idx * NETWORK_PRIORITY_STEP) + return None + + +def get_priority_interfaces_from_full_config(full_config: ConfigType) -> set[str]: + """Return the set of interface names declared in ``network: priority:``. + + Reads from the full validated config (``fv.full_config.get()``) and is + intended for use inside ``FINAL_VALIDATE_SCHEMA`` hooks, before + ``to_code`` has run and ``CORE.data`` has been populated. Returns an + empty set if no priority list was configured. + """ + return { + entry["interface"] + for entry in full_config.get("network", {}).get(CONF_PRIORITY, []) } + + +def _validate_priority_list(value: Any) -> list[dict[str, str]]: + """Validate and normalize the priority list, rejecting duplicates. + + Each entry is the name of one network interface (one of + ``VALID_NETWORK_TYPES``). Mixed-case input is accepted and normalized + to lowercase. The normalized list is a list of dicts of the form + ``{"interface": "ethernet"}`` so that future per-entry options can be + added without breaking call sites. + """ + raw = cv.ensure_list(cv.one_of(*VALID_NETWORK_TYPES, lower=True))(value) + entries = [{"interface": iface} for iface in raw] + interfaces = [e["interface"] for e in entries] + if len(interfaces) != len(set(interfaces)): + raise cv.Invalid("Duplicate entries are not allowed in 'priority'") + return entries + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(NetworkComponent), + cv.SplitDefault( + CONF_ENABLE_IPV6, + bk72xx=False, + esp32=False, + esp8266=False, + host=False, + rp2=False, + nrf52=True, + ): cv.All( + cv.boolean, + cv.Any( + cv.require_framework_version( + bk72xx_arduino=cv.Version(1, 7, 0), + esp_idf=cv.Version(0, 0, 0), + esp32_arduino=cv.Version(0, 0, 0), + esp8266_arduino=cv.Version(0, 0, 0), + host=cv.Version(0, 0, 0), + rp2_arduino=cv.Version(0, 0, 0), + nrf52_zephyr=cv.Version(0, 0, 0), + ), + cv.boolean_false, + ), + validate_ipv6, + ), + cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All( + cv.boolean, cv.only_on_esp32 + ), + cv.Optional(CONF_PRIORITY): _validate_priority_list, + } + ), + _register_provisioning_source, ) +def _final_validate(config: ConfigType) -> None: + """Check that every interface named in 'priority' has a corresponding component block.""" + full = fv.full_config.get() + for entry in config.get(CONF_PRIORITY, []): + iface = entry["interface"] + if iface not in full: + raise cv.Invalid( + f"'{iface}' is listed in 'network: priority:' but no '{iface}:' " + f"component is configured", + [CONF_PRIORITY], + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.NETWORK) async def to_code(config): cg.add_define("USE_NETWORK") # ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed + # Store the user-declared network priority list in CORE.data so that ethernet, + # wifi and other network components can query it via get_network_priority() + # during their own to_code phase. + if CONF_PRIORITY in config: + priority_list = config[CONF_PRIORITY] + CORE.data[KEY_NETWORK_PRIORITY] = priority_list + # network/util.cpp resolves the reported address (get_use_address_to, + # get_ip_addresses) in a fixed ethernet-first order; a wifi-first priority + # list is the only case that deviates from it, so it is the only case that + # needs a define. Runtime (active-interface) selection is a planned follow-up. + if priority_list[0]["interface"] == "wifi": + cg.add_define("USE_NETWORK_PRIMARY_INTERFACE_WIFI") + + _LOGGER.info( + "Network interface priority: %s", + " > ".join(entry["interface"] for entry in priority_list), + ) + # Apply high performance networking settings # Config can explicitly enable/disable, or default to component-driven behavior enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE) @@ -263,7 +456,7 @@ async def to_code(config): cg.add_build_flag("-DCONFIG_IPV6") if CORE.is_esp8266: cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") - if CORE.is_rp2040: + if CORE.is_rp2: cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") # Pvariable creation lives in a separate coroutine at NETWORK_SERVICES so it # emits after wifi/ethernet at COMMUNICATION. This keeps compile-time config diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index d8a127f4a0..f7aa7daf99 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -19,10 +19,37 @@ #ifdef USE_HOST #include +#if USE_NETWORK_IPV6 +using ip4_addr_t = struct in_addr; +using ip6_addr_t = struct in6_addr; +struct ip_addr_t { + union { + struct in6_addr ip6; + struct in_addr ip4; + } u_addr; + uint8_t type; +}; +enum : uint8_t { IPADDR_TYPE_V4 = 0, IPADDR_TYPE_V6 = 6 }; +static inline int ipaddr_aton(const char *cp, ip_addr_t *addr) { + if (strchr(cp, ':') != nullptr) { + if (inet_pton(AF_INET6, cp, &addr->u_addr.ip6) != 1) { + return 0; + } + addr->type = IPADDR_TYPE_V6; + return 1; + } + if (inet_aton(cp, &addr->u_addr.ip4) != 1) { + return 0; + } + addr->type = IPADDR_TYPE_V4; + return 1; +} +#else using ip_addr_t = in_addr; using ip4_addr_t = in_addr; #define ipaddr_aton(x, y) inet_aton((x), (y)) -#endif +#endif // USE_NETWORK_IPV6 +#endif // USE_HOST #ifdef USE_ZEPHYR #include @@ -71,15 +98,6 @@ struct IPAddress { bool is_ip4() const { return false; } bool is_ip6() const { return this->is_set(); } bool is_multicast() const { return net_ipv6_is_addr_mcast(&ip_addr_); } - // Remove before 2026.8.0 - ESPDEPRECATED( - "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", - "2026.2.0") - std::string str() const { - char buf[IP_ADDRESS_BUFFER_SIZE]; - this->str_to(buf); - return buf; - } char *str_to(char *buf) const { if (inet_ntop(AF_INET6, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE) == nullptr) buf[0] = '\0'; @@ -89,12 +107,46 @@ struct IPAddress { bool operator!=(const IPAddress &other) const { return !net_ipv6_addr_cmp(&ip_addr_, &other.ip_addr_); } #elif defined(USE_HOST) - IPAddress() { ip_addr_.s_addr = 0; } +#if USE_NETWORK_IPV6 + IPAddress() { memset(&this->ip_addr_, 0, sizeof(this->ip_addr_)); } IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { - this->ip_addr_.s_addr = htonl((first << 24) | (second << 16) | (third << 8) | fourth); + memset(&this->ip_addr_, 0, sizeof(this->ip_addr_)); + this->ip_addr_.u_addr.ip4.s_addr = + htonl(((uint32_t) first << 24) | ((uint32_t) second << 16) | ((uint32_t) third << 8) | fourth); + this->ip_addr_.type = IPADDR_TYPE_V4; + } + IPAddress(const char *in_address) { + memset(&this->ip_addr_, 0, sizeof(this->ip_addr_)); + ipaddr_aton(in_address, &this->ip_addr_); + } + IPAddress(const std::string &in_address) : IPAddress(in_address.c_str()) {} + IPAddress(const ip_addr_t *other_ip) { memcpy(&this->ip_addr_, other_ip, sizeof(ip_addr_t)); } + IPAddress(ip4_addr_t *other_ip) { + memset(&this->ip_addr_, 0, sizeof(this->ip_addr_)); + this->ip_addr_.u_addr.ip4 = *other_ip; + this->ip_addr_.type = IPADDR_TYPE_V4; + } + IPAddress(ip6_addr_t *other_ip) { + memset(&this->ip_addr_, 0, sizeof(this->ip_addr_)); + this->ip_addr_.u_addr.ip6 = *other_ip; + this->ip_addr_.type = IPADDR_TYPE_V6; + } + operator ip_addr_t() const { return this->ip_addr_; } + bool is_set() const { + if (this->ip_addr_.type == IPADDR_TYPE_V6) { + static constexpr uint8_t zero[sizeof(struct in6_addr)] = {}; + return memcmp(this->ip_addr_.u_addr.ip6.s6_addr, zero, sizeof(zero)) != 0; + } + return this->ip_addr_.u_addr.ip4.s_addr != 0; + } + bool is_ip4() const { return this->ip_addr_.type == IPADDR_TYPE_V4; } + bool is_ip6() const { return this->ip_addr_.type == IPADDR_TYPE_V6; } + bool is_multicast() const { + if (this->ip_addr_.type == IPADDR_TYPE_V6) { + return this->ip_addr_.u_addr.ip6.s6_addr[0] == 0xff; + } + return (ntohl(this->ip_addr_.u_addr.ip4.s_addr) & 0xF0000000UL) == 0xE0000000UL; } - IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } - IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } // Remove before 2026.8.0 ESPDEPRECATED( "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", @@ -104,11 +156,44 @@ struct IPAddress { this->str_to(buf); return buf; } + char *str_to(char *buf) const { + if (this->ip_addr_.type == IPADDR_TYPE_V6) { + inet_ntop(AF_INET6, &this->ip_addr_.u_addr.ip6, buf, IP_ADDRESS_BUFFER_SIZE); + } else { + inet_ntop(AF_INET, &this->ip_addr_.u_addr.ip4, buf, IP_ADDRESS_BUFFER_SIZE); + } + lowercase_ip_str(buf); + return buf; + } + bool operator==(const IPAddress &other) const { + if (this->ip_addr_.type != other.ip_addr_.type) { + return false; + } + if (this->ip_addr_.type == IPADDR_TYPE_V6) { + return memcmp(&this->ip_addr_.u_addr.ip6, &other.ip_addr_.u_addr.ip6, sizeof(struct in6_addr)) == 0; + } + return this->ip_addr_.u_addr.ip4.s_addr == other.ip_addr_.u_addr.ip4.s_addr; + } + bool operator!=(const IPAddress &other) const { return !(*this == other); } + IPAddress &operator+=(uint8_t increase) { + if (this->ip_addr_.type == IPADDR_TYPE_V4) { + (((uint8_t *) (&this->ip_addr_.u_addr.ip4))[3]) += increase; + } + return *this; + } +#else + IPAddress() { ip_addr_.s_addr = 0; } + IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { + this->ip_addr_.s_addr = htonl((first << 24) | (second << 16) | (third << 8) | fourth); + } + IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } + IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } /// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes. char *str_to(char *buf) const { inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE); return buf; // IPv4 only, no hex letters to lowercase } +#endif // USE_NETWORK_IPV6 #else IPAddress() { ip_addr_set_zero(&ip_addr_); } IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { @@ -186,15 +271,6 @@ struct IPAddress { bool is_ip4() const { return IP_IS_V4(&ip_addr_); } bool is_ip6() const { return IP_IS_V6(&ip_addr_); } bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } - // Remove before 2026.8.0 - ESPDEPRECATED( - "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", - "2026.2.0") - std::string str() const { - char buf[IP_ADDRESS_BUFFER_SIZE]; - this->str_to(buf); - return buf; - } /// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes. /// Output is lowercased per RFC 5952 (IPv6 hex digits a-f). char *str_to(char *buf) const { diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 79ddd3844c..d90c28801e 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -1,5 +1,7 @@ #include "util.h" +#include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #ifdef USE_NETWORK namespace esphome::network { @@ -20,7 +22,46 @@ bool is_disabled() { return false; } +const char *get_use_address_to(std::span buf) { + // Global component pointers are guaranteed to be set by component constructors when USE_* is defined. + // A wifi-first network: priority: list sets USE_NETWORK_PRIMARY_INTERFACE_WIFI to lift + // wifi ahead of the fixed ethernet-first order below; an ethernet-first list already + // matches that order, so no define exists for it. + const char *addr = nullptr; +#if defined(USE_NETWORK_PRIMARY_INTERFACE_WIFI) && defined(USE_WIFI) + addr = wifi::global_wifi_component->get_use_address(); +#elif defined(USE_ETHERNET) + addr = ethernet::global_eth_component->get_use_address(); +#elif defined(USE_MODEM) + addr = modem::global_modem_component->get_use_address(); +#elif defined(USE_WIFI) + addr = wifi::global_wifi_component->get_use_address(); +#elif defined(USE_OPENTHREAD) + addr = openthread::global_openthread_component->get_use_address(); +#endif + if (addr != nullptr && addr[0] != '\0') + return addr; + // No explicit use_address configured: the address is the runtime device name + // (which includes the MAC suffix when name_add_mac_suffix is enabled) plus ".local" + const auto &name = App.get_name(); + make_name_with_suffix_to(buf.data(), buf.size(), name.c_str(), name.size(), '.', "local", 5); + return buf.data(); +} + network::IPAddresses get_ip_addresses() { + // With a wifi-first network: priority: list, prefer wifi while it has a valid IP; + // otherwise fall through to the fixed ethernet-first order below. Selection based + // on the runtime-active interface is a planned follow-up. +#if defined(USE_NETWORK_PRIMARY_INTERFACE_WIFI) && defined(USE_WIFI) + if (wifi::global_wifi_component != nullptr) { + auto ips = wifi::global_wifi_component->get_ip_addresses(); + for (const auto &ip : ips) { + if (ip.is_set()) + return ips; + } + } +#endif + #ifdef USE_ETHERNET if (ethernet::global_eth_component != nullptr) return ethernet::global_eth_component->get_ip_addresses(); diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index e4e8a01f8c..df7e164bda 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK +#include #include #include "esphome/core/helpers.h" #include "ip_address.h" @@ -53,30 +54,13 @@ ESPHOME_ALWAYS_INLINE inline bool is_connected() { /// Return whether the network is disabled (only wifi for now) bool is_disabled(); -/// Get the active network hostname -ESPHOME_ALWAYS_INLINE inline const char *get_use_address() { - // Global component pointers are guaranteed to be set by component constructors when USE_* is defined -#ifdef USE_ETHERNET - return ethernet::global_eth_component->get_use_address(); -#endif - -#ifdef USE_MODEM - return modem::global_modem_component->get_use_address(); -#endif - -#ifdef USE_WIFI - return wifi::global_wifi_component->get_use_address(); -#endif - -#ifdef USE_OPENTHREAD - return openthread::global_openthread_component->get_use_address(); -#endif - -#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) - // Fallback when no network component is defined (e.g., host platform) - return ""; -#endif -} +/// Buffer size for get_use_address_to(): 63-char DNS label + ".local" + null terminator +static constexpr size_t USE_ADDRESS_BUFFER_SIZE = 70; +/// Get the active network address for logging. Returns the explicitly configured +/// use_address when one was set (from the highest-priority interface when +/// network: priority: is configured), otherwise formats ".local" from the runtime +/// device name into buf (so it includes the MAC suffix from name_add_mac_suffix). +const char *get_use_address_to(std::span buf); IPAddresses get_ip_addresses(); } // namespace esphome::network diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index 38f449dc03..efb6c88d28 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -19,10 +19,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( }, "nextion_upload_arduino.cpp": { PlatformFramework.ESP8266_ARDUINO, - PlatformFramework.RP2040_ARDUINO, - PlatformFramework.BK72XX_ARDUINO, - PlatformFramework.RTL87XX_ARDUINO, - PlatformFramework.LN882X_ARDUINO, }, } ) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 89e9b93520..4ab123c354 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -4,7 +4,14 @@ from esphome import automation import esphome.codegen as cg from esphome.components import display, esp32, uart import esphome.config_validation as cv -from esphome.const import CONF_BRIGHTNESS, CONF_ID, CONF_LAMBDA, CONF_ON_TOUCH +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_ID, + CONF_LAMBDA, + CONF_ON_TOUCH, + PLATFORM_ESP32, + PLATFORM_ESP8266, +) from esphome.core import CORE, TimePeriod from . import ( # noqa: F401 pylint: disable=unused-import @@ -135,7 +142,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_TFT_URL): cv.url, + # TFT upload needs an HTTP client and runtime UART reconfiguration, + # neither of which is implemented for the RP2 or host platforms. + cv.Optional(CONF_TFT_URL): cv.All( + cv.url, + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + ] + ), + ), cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any( 0, cv.int_range(min=3, max=65535) ), diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 4ebc717552..bdc66adb70 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1,6 +1,7 @@ #include "nextion.h" #include +#include #include "esphome/core/application.h" #include "esphome/core/helpers.h" @@ -352,8 +353,9 @@ void Nextion::loop() { this->connection_state_.ignore_is_setup_ = false; } - this->process_serial_(); // Receive serial data - this->process_nextion_commands_(); // Process nextion return commands + this->process_serial_(); // Receive serial data + this->process_nextion_commands_(); // Process nextion return commands + this->purge_stale_queue_entries_(); // Drop expired entries even when the display sends no data if (!this->connection_state_.nextion_reports_is_setup_) { if (this->started_ms_ == 0) @@ -902,6 +904,11 @@ void Nextion::process_nextion_commands_() { this->command_data_.erase(0, to_process_length + DELIMITER_SIZE + 1); } + ESP_LOGN(TAG, "Loop end"); + this->process_serial_(); +} // Nextion::process_nextion_commands_() + +void Nextion::purge_stale_queue_entries_() { const uint32_t ms = App.get_loop_component_start_time(); if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() && @@ -927,10 +934,7 @@ void Nextion::process_nextion_commands_() { } } } - ESP_LOGN(TAG, "Loop end"); - // App.feed_wdt(); Remove before master merge - this->process_serial_(); -} // Nextion::process_nextion_commands_() +} void Nextion::set_nextion_sensor_state(int queue_type, const std::string &name, float state) { this->set_nextion_sensor_state(static_cast(queue_type), name, state); @@ -1101,7 +1105,13 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { new (nextion_queue) nextion::NextionQueue(); // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component = new (std::nothrow) nextion::NextionComponentBase; + if (nextion_queue->component == nullptr) { + ESP_LOGW(TAG, "Component alloc failed"); + nextion_queue->~NextionQueue(); + allocator.deallocate(nextion_queue, 1); + return; + } nextion_queue->component->set_variable_name(variable_name); nextion_queue->queue_time = App.get_loop_component_start_time(); @@ -1157,7 +1167,13 @@ void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &va } new (nextion_queue) nextion::NextionQueue(); - nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component = new (std::nothrow) nextion::NextionComponentBase; + if (nextion_queue->component == nullptr) { + ESP_LOGW(TAG, "Component alloc failed"); + nextion_queue->~NextionQueue(); + allocator.deallocate(nextion_queue, 1); + return; + } nextion_queue->component->set_variable_name(variable_name); nextion_queue->queue_time = App.get_loop_component_start_time(); nextion_queue->pending_command = command; // Store command for retry diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index d361d9725b..aa9fe8abb3 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -23,7 +23,9 @@ #elif defined(USE_ESP8266) #include #include -#endif // USE_ESP32 vs USE_ESP8266 +#elif defined(USE_LIBRETINY) +#include +#endif // USE_ESP32 vs USE_ESP8266 vs USE_LIBRETINY #endif // USE_NEXTION_TFT_UPLOAD namespace esphome::nextion { @@ -1486,6 +1488,10 @@ class Nextion final : public NextionBase, public PollingComponent, public uart:: void process_nextion_commands_(); void process_serial_(); + /// Drop queue entries older than max_q_age_ms_. Called from loop() so it also runs when the + /// display sends no data at all (disconnected or asleep), which would otherwise grow the queue + /// without bound. + void purge_stale_queue_entries_(); uint16_t touch_sleep_timeout_ = 0; uint8_t wake_up_page_ = 255; #ifdef USE_NEXTION_CONF_START_UP_PAGE @@ -1560,7 +1566,7 @@ class Nextion final : public NextionBase, public PollingComponent, public uart:: * @return position of last byte transferred, -1 for failure. */ int upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &range_start); -#elif defined(USE_ARDUINO) +#elif defined(USE_ESP8266) || defined(USE_LIBRETINY) /** * will request chunk_size chunks from the web server * and send each to the nextion @@ -1569,7 +1575,7 @@ class Nextion final : public NextionBase, public PollingComponent, public uart:: * @return position of last byte transferred, -1 for failure. */ int upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start); -#endif // USE_ESP32 vs USE_ARDUINO +#endif // USE_ESP32 vs USE_ESP8266/USE_LIBRETINY /** * Ends the upload process, restart Nextion and, if successful, diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 41379c2345..2f3377d950 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -1,7 +1,7 @@ #include "nextion.h" #ifdef USE_NEXTION_TFT_UPLOAD -#ifndef USE_ESP32 +#ifdef USE_ESP8266 #include #include "esphome/components/network/util.h" @@ -209,7 +209,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { http_client.setTimeout(this->tft_upload_http_timeout_); bool begin_status = false; -#ifdef USE_ESP8266 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); #elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) @@ -219,7 +218,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { http_client.setRedirectLimit(3); #endif begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); -#endif // USE_ESP8266 if (!begin_status) { this->connection_state_.is_updating_ = false; ESP_LOGD(TAG, "Connection failed"); @@ -356,7 +354,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return upload_end_(true); } -#ifdef USE_ESP8266 WiFiClient *Nextion::get_wifi_client_() { if (this->tft_url_.starts_with("https:")) { if (this->wifi_client_secure_ == nullptr) { @@ -374,9 +371,8 @@ WiFiClient *Nextion::get_wifi_client_() { } return this->wifi_client_; } -#endif // USE_ESP8266 } // namespace esphome::nextion -#endif // NOT USE_ESP32 +#endif // USE_ESP8266 #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index cd8feab84f..e2d5ae8ad7 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -44,17 +44,60 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r ESP_LOGV(TAG, "Range: %s", range_header); esp_http_client_set_header(http_client, "Range", range_header); ESP_LOGV(TAG, "Open HTTP"); - esp_err_t err = esp_http_client_open(http_client, 0); - if (err != ESP_OK) { - ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err)); - return -1; + int chunk_size = -1; + int status_code = -1; + esp_err_t last_err = ESP_FAIL; + for (uint8_t attempt = 0; attempt < this->tft_upload_http_retries_; attempt++) { + status_code = -1; + last_err = esp_http_client_open(http_client, 0); + if (last_err == ESP_OK) { + ESP_LOGV(TAG, "Fetch length"); + chunk_size = esp_http_client_fetch_headers(http_client); + ESP_LOGV(TAG, "Length: %d", chunk_size); + if (chunk_size >= 0) { + status_code = esp_http_client_get_status_code(http_client); + // Accept the requested range (206 with exact length) or a full-body 200 + // only from offset 0; elsewhere a 200 replays the file and corrupts the display. + if (chunk_size > 0 && + ((status_code == 206 && chunk_size == static_cast(range_end - range_start + 1)) || + (status_code == 200 && range_start == 0 && chunk_size == static_cast(this->tft_size_)))) { + break; + } + if (status_code == 200) { + if (range_start == 0) { + ESP_LOGE(TAG, "Unexpected length for 200 response: %d (expected %d)", chunk_size, + static_cast(this->tft_size_)); + } else { + // A server that ignored the range once will ignore it again + ESP_LOGE(TAG, "Server does not support range requests (got 200 at offset %" PRIu32 ")", range_start); + } + chunk_size = -1; + last_err = ESP_FAIL; + break; + } + ESP_LOGW(TAG, "Bad response: status %d, length %d (expected %" PRIu32 ")", status_code, chunk_size, + range_end - range_start + 1); + chunk_size = -1; + last_err = ESP_FAIL; + // A 4xx (except timeout/rate-limit) won't improve on retry + if (status_code >= 400 && status_code < 500 && status_code != 408 && status_code != 429) { + break; + } + } else { + ESP_LOGW(TAG, "Get length failed: %d", chunk_size); + last_err = ESP_FAIL; + } + } else { + ESP_LOGW(TAG, "HTTP open failed: %s", esp_err_to_name(last_err)); + } + // The server may have dropped the keep-alive connection while the display + // was busy processing a chunk; close so the next attempt reconnects. + esp_http_client_close(http_client); + vTaskDelay(pdMS_TO_TICKS(2)); // NOLINT + App.feed_wdt(); } - - ESP_LOGV(TAG, "Fetch length"); - const int chunk_size = esp_http_client_fetch_headers(http_client); - ESP_LOGV(TAG, "Length: %d", chunk_size); if (chunk_size <= 0) { - ESP_LOGE(TAG, "Get length failed: %d", chunk_size); + ESP_LOGE(TAG, "HTTP request failed, last status: %d, last error: %s", status_code, esp_err_to_name(last_err)); return -1; } @@ -164,6 +207,9 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } else { range_start = range_end + 1; } + // The response body may be only partially read; close so the next + // range request starts on a clean connection. + esp_http_client_close(http_client); // Deallocate buffer allocator.deallocate(buffer, 4096); buffer = nullptr; diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 7c17eadd1a..4002d1cc04 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -69,7 +69,12 @@ from .const import ( BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, ) -from .framework import check_and_install, get_build_env, get_build_paths +from .framework import ( + check_and_install, + get_build_env, + get_build_paths, + setup_platformio_python_env, +) # force import gpio to register pin schema from .gpio import nrf52_pin_to_code # noqa: F401 @@ -79,6 +84,11 @@ AUTO_LOAD = ["zephyr", "preferences"] IS_TARGET_PLATFORM = True _LOGGER = logging.getLogger(__name__) +# Default framework versions per toolchain. The sdk-nrf one also keys the CI +# sdk-nrf install cache and pins the clang-tidy project's SDK. +RECOMMENDED_PLATFORMIO_VERSION = "2.6.1-b" +RECOMMENDED_SDK_NRF_VERSION = "2.9.2" + FAKE_BOARD_MANIFEST = """ { "frameworks": [ @@ -123,7 +133,11 @@ def _resolve_toolchain(config: ConfigType) -> ConfigType: def set_framework(config: ConfigType) -> ConfigType: if CONF_VERSION not in config[CONF_FRAMEWORK]: - default_version = "2.6.1-b" if CORE.using_toolchain_platformio else "2.9.2" + default_version = ( + RECOMMENDED_PLATFORMIO_VERSION + if CORE.using_toolchain_platformio + else RECOMMENDED_SDK_NRF_VERSION + ) config = { **config, CONF_FRAMEWORK: {**config[CONF_FRAMEWORK], CONF_VERSION: default_version}, @@ -191,6 +205,7 @@ DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component) CONF_DFU = "dfu" CONF_DCDC = "dcdc" +CONF_LIBC_NANO = "libc_nano" CONF_REG0 = "reg0" CONF_UICR_ERASE = "uicr_erase" @@ -223,7 +238,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), cv.Optional(CONF_DFU): _dfu_schema, - cv.Optional(CONF_DCDC, default=True): cv.boolean, + cv.Optional(CONF_DCDC): cv.boolean, cv.Optional(CONF_REG0): cv.Schema( { cv.Required(CONF_VOLTAGE): cv.All( @@ -239,7 +254,10 @@ CONFIG_SCHEMA = cv.All( ): cv.Schema( { cv.Optional(CONF_VERSION): cv.string_strict, - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + cv.Optional(CONF_LIBC_NANO, default=True): cv.boolean, + cv.Optional( + CONF_ADVANCED, default={}, visibility=cv.Visibility.YAML_ONLY + ): cv.Schema( { cv.Optional( CONF_ENABLE_OTA_ROLLBACK, default=True @@ -264,6 +282,14 @@ def _validate_mcumgr(config): def _final_validate(config): + + # Remove before 2027.2.0 + if CORE.using_toolchain_platformio: + _LOGGER.warning( + "The 'platformio' toolchain for nRF52 is deprecated and will be removed in ESPHome 2027.2.0. " + "Please use 'toolchain: sdk-nrf' instead." + ) + if CONF_DFU in config: _validate_mcumgr(config) if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT: @@ -274,6 +300,13 @@ def _final_validate(config): conf = config[CONF_FRAMEWORK] advanced = conf[CONF_ADVANCED] + if conf[CONF_LIBC_NANO] and "logger" in CORE.loaded_integrations: + _LOGGER.warning( + "Logger is enabled with newlib-nano (libc_nano: true). Some format specifiers " + "such as %%zu are not supported and will print incorrectly. " + "Set 'libc_nano: false' under 'framework:' to use the full newlib." + ) + if advanced[CONF_ENABLE_OTA_ROLLBACK]: # "disabled: false" means safe mode *is* enabled. safe_mode_config = full_config.get(CONF_SAFE_MODE, {CONF_DISABLED: True}) @@ -348,16 +381,17 @@ async def to_code(config: ConfigType) -> None: if dfu_config := config.get(CONF_DFU): CORE.add_job(_dfu_to_code, dfu_config) framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if framework_ver < cv.Version(2, 9, 2): - zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC]) - else: - zephyr_add_overlay( - f""" - ®1 {{ - regulator-initial-mode = <{"NRF5X_REG_MODE_DCDC" if config[CONF_DCDC] else "NRF5X_REG_MODE_LDO"}>; - }}; - """ - ) + if CONF_DCDC in config: + if framework_ver < cv.Version(2, 9, 2): + zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC]) + else: + zephyr_add_overlay( + f""" + ®1 {{ + regulator-initial-mode = <{"NRF5X_REG_MODE_DCDC" if config[CONF_DCDC] else "NRF5X_REG_MODE_LDO"}>; + }}; + """ + ) if reg0_config := config.get(CONF_REG0): value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE]) @@ -370,6 +404,9 @@ async def to_code(config: ConfigType) -> None: # Enable OTA rollback support if advanced[CONF_ENABLE_OTA_ROLLBACK]: cg.add_define("USE_OTA_ROLLBACK") + zephyr_add_prj_conf("NEWLIB_LIBC", True) + zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) + zephyr_add_prj_conf("NEWLIB_LIBC_NANO", conf[CONF_LIBC_NANO]) # c++ support if framework_ver < cv.Version(2, 9, 2): zephyr_add_prj_conf("CPLUSPLUS", True) @@ -489,6 +526,7 @@ def _upload_using_platformio( ) -> int | str: from esphome.platformio import toolchain + setup_platformio_python_env() if port is not None: upload_args += ["--upload-port", port] return toolchain.run_platformio_cli_run(config, CORE.verbose, *upload_args) @@ -784,6 +822,10 @@ def _copy_if_exists(src: Path, dst: Path) -> None: def run_compile(args, config: ConfigType) -> bool: if CORE.using_toolchain_platformio: + # The actual build is done by PlatformIO (the caller falls through to + # it when this returns False); prepare the Python environment its + # Zephyr build script expects first. + setup_platformio_python_env() return False if not CORE.using_toolchain_sdk_nrf: raise EsphomeError( @@ -835,6 +877,21 @@ def run_compile(args, config: ConfigType) -> bool: zephyr_dir = build_dir / "zephyr" framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + bootloader = zephyr_data()[KEY_BOOTLOADER] + + # (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes + _GENPKG_PARAMS = { + BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"), + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"), + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"), + } + # UF2 family IDs — nRF52832 vs nRF52840 per SoftDevice variant + _UF2_FAMILY_IDS = { + BOOTLOADER_ADAFRUIT_NRF52_SD132: "0x7EAED30A", + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: "0xADA52840", + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: "0xADA52840", + } + # SDK < 2.9.2 places artifacts directly in build_dir/zephyr/. # SDK >= 2.9.2 nests them one level deeper (build_dir/zephyr/zephyr/); # copy files to match get_download_types layout. @@ -846,20 +903,43 @@ def run_compile(args, config: ConfigType) -> bool: _copy_if_exists(west_out / "zephyr.signed.bin", zephyr_dir / "app_update.bin") _copy_if_exists(build_dir / "merged.hex", zephyr_dir / "merged.hex") - # (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes - _GENPKG_PARAMS = { - BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"), - BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"), - BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"), - } - bootloader = zephyr_data()[KEY_BOOTLOADER] + # For Adafruit bootloader builds, regenerate the UF2 from merged.hex, + # whose records carry the correct flash addresses. The build's own + # zephyr.uf2 uses the board's default offset, which is wrong in some cases. + merged_hex = zephyr_dir / "merged.hex" + if bootloader in _UF2_FAMILY_IDS and merged_hex.is_file(): + # Drop the build's own wrong-offset UF2 so it isn't shipped alongside. + app_uf2 = west_out / "zephyr.uf2" + if app_uf2.is_file(): + app_uf2.unlink() + uf2conv = ( + paths["framework_path"] / "zephyr" / "scripts" / "build" / "uf2conv.py" + ) + if not run_command_ok( + [ + str(paths["python_executable"]), + str(uf2conv), + "-f", + _UF2_FAMILY_IDS[bootloader], + "-c", + "-o", + str(zephyr_dir / "zephyr.uf2"), + str(merged_hex), + ], + env=env, + stream_output=True, + ): + raise EsphomeError("Failed to generate UF2 from merged hex") + if bootloader in ( BOOTLOADER_ADAFRUIT, BOOTLOADER_ADAFRUIT_NRF52_SD132, BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, ): - hex_file = west_out / "zephyr.hex" + # no fallback is needed for adafruit case. merged merged.hex is always generated. + # get_download_types needs fallback for mcuboot (non adafruit) + hex_file = zephyr_dir / "merged.hex" dfu_package = build_dir / "firmware.zip" genpkg_cmd = [ str(paths["python_executable"]), diff --git a/esphome/components/nrf52/clang_tidy.py b/esphome/components/nrf52/clang_tidy.py new file mode 100644 index 0000000000..2dd4b7bd09 --- /dev/null +++ b/esphome/components/nrf52/clang_tidy.py @@ -0,0 +1,249 @@ +"""Generate clang-tidy compile commands via the native sdk-nrf toolchain. + +Produces a ``compile_commands.json`` for the nrf52/Zephyr clang-tidy +environment **without an ESPHome YAML config**, mirroring +``esphome.espidf.clang_tidy``: generate a minimal Zephyr application, run a +configure-only west build with the native sdk-nrf toolchain, and let +``script/helpers_zephyr.py`` extract idedata from the resulting compile +commands. + +* the stub app is C++ so the compile commands carry C++ flags, matching how + clang-tidy analyzes ESPHome's sources; +* ``prj.conf`` enables the Kconfig superset ESPHome components need (BT, ADC, + mcumgr, zigbee) so their include paths land in the compile commands; +* the platform defines (USE_ZEPHYR, USE_NRF52) match what a real ESPHome + nrf52 build adds via its generated project. + +``ESPHOME_ZEPHYR_COMPILE_COMMANDS`` may point at an existing build's +``compile_commands.json`` to skip generation (fast iteration). +""" + +from __future__ import annotations + +import logging +from pathlib import Path + +TIDY_PROJECT_NAME = "esphome_tidy" + +# Analyzed against the native toolchain's default SDK version +# (RECOMMENDED_SDK_NRF_VERSION), which also keys the CI install cache. +_TIDY_BOARD = "adafruit_itsybitsy_nrf52840" + +# Never compiled (the build is configure-only): the file exists only so the +# app target emits a C++ compile command to harvest flags/includes from. +_TIDY_MAIN_CPP = "int main() { return 0; }\n" + +# Kconfig superset enabling every subsystem an ESPHome nrf52 component may +# use, so the compile commands carry all of their include paths. +_TIDY_PRJ_CONF = """\ +CONFIG_CPP=y +CONFIG_STD_CPP20=y +CONFIG_REQUIRES_FULL_LIBCPP=y +CONFIG_NEWLIB_LIBC=y +CONFIG_BT=y +CONFIG_ADC=y +# posix (time sets POSIX_CLOCK, socket sets POSIX_API); without it the +# Zephyr POSIX headers clash with the libc ones under analysis +CONFIG_POSIX_API=y +#mcumgr begin +CONFIG_NET_BUF=y +CONFIG_ZCBOR=y +CONFIG_MCUMGR=y +CONFIG_MCUMGR_GRP_IMG=y +CONFIG_IMG_MANAGER=y +CONFIG_STREAM_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_FLASH=y +CONFIG_IMG_ERASE_PROGRESSIVELY=y +CONFIG_BOOTLOADER_MCUBOOT=y +CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS=y +CONFIG_MCUMGR_GRP_IMG_STATUS_HOOKS=y +CONFIG_MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK=y +CONFIG_MCUMGR_TRANSPORT_UART=y +#mcumgr end +#zigbee begin +CONFIG_ZIGBEE=y +CONFIG_CRYPTO=y +CONFIG_NVS=y +CONFIG_SETTINGS=y +#zigbee end +""" + + +def _tidy_cmakelists(library_include_dirs: str) -> str: + # The defines a real ESPHome nrf52 build puts on the app target. + # ESPHOME_LOG_LEVEL must be set up front -- otherwise log.h's ``#ifndef`` + # sets it to NONE, a macro-redefined warning across nearly every source. + return f"""\ +# Auto-generated by ESPHome (clang-tidy compile-commands project) +cmake_minimum_required(VERSION 3.20.0) +set(Zephyr_DIR "$ENV{{ZEPHYR_BASE}}/share/zephyr-package/cmake/") +find_package(Zephyr REQUIRED) +project({TIDY_PROJECT_NAME}) +target_sources(app PRIVATE main.cpp) +target_compile_definitions(app PRIVATE + USE_ZEPHYR + USE_NRF52 + ESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE +) +target_include_directories(app PRIVATE +{library_include_dirs} +) +""" + + +def _parse_lib_deps(platformio_ini: Path) -> list: + """Parse the nrf52 env's ``lib_deps`` from platformio.ini into Library specs. + + These are the PlatformIO libraries ESPHome components pull in via + ``cg.add_library`` (ArduinoJson, dlms_parser, ...); their headers must be + on the tidy translation unit's include path. Mirrors the pio nrf52 env's + ``lib_deps`` composition (``common.lib_deps_base`` + + ``common:idf-component-libs``). + """ + import configparser + + from esphome.core import Library + + parser = configparser.ConfigParser(interpolation=None, strict=False) + parser.read(platformio_ini) + + tokens: list[str] = [] + for section, key in ( + ("common", "lib_deps_base"), + ("common:idf-component-libs", "lib_deps"), + ): + if parser.has_option(section, key): + tokens += parser.get(section, key).splitlines() + + libs: list[Library] = [] + for token in tokens: + token = token.split(";", 1)[0].strip() # drop trailing ; comment + if not token or token.startswith(("${", "+<")): + continue + if "://" in token or ".git" in token: + libs.append(Library(token, None, token)) # git repository (with #ref) + elif "@" in token: + name, _, version = token.partition("@") + libs.append(Library(name, version)) + return libs + + +def _library_include_dirs(platformio_ini: Path) -> list[str]: + """Resolve the pio libraries and return their include roots.""" + from esphome.platformio.library import LibraryBackend, convert_libraries + + dirs: list[str] = [] + + def emit(component) -> None: + build = component.data.get("build", {}) + candidates = {build.get("includeDir", "include"), build.get("srcDir", "src")} + candidates.update({"src", "."}) + for candidate in sorted(candidates): + path = (component.path / candidate).resolve() + if path.is_dir(): + dirs.append(str(path)) + + backend = LibraryBackend( + platform="nordicnrf52", framework="zephyr", emit=emit, cache_key="zephyr" + ) + convert_libraries(_parse_lib_deps(platformio_ini), backend) + return sorted(set(dirs)) + + +def _setup_core(work_dir: Path) -> None: + """Point CORE at the tidy project + SDK version, without any YAML config.""" + from esphome.components.zephyr.const import KEY_ZEPHYR + import esphome.config_validation as cv + from esphome.const import ( + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_NRF52, + Toolchain, + ) + from esphome.core import CORE + + from . import RECOMMENDED_SDK_NRF_VERSION + + CORE.name = TIDY_PROJECT_NAME + # config_path's parent is the data-dir root for per-run artifacts. The + # sdk-nrf install is in the global cache dir, independent of this path. + CORE.config_path = work_dir.parent / "tidy.yaml" + CORE.build_path = work_dir + CORE.toolchain = Toolchain.SDK_NRF + CORE.data.setdefault(KEY_CORE, {})[KEY_TARGET_PLATFORM] = PLATFORM_NRF52 + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( + RECOMMENDED_SDK_NRF_VERSION + ) + + +def generate_compile_commands(work_dir: Path, platformio_ini: Path) -> Path: + """Generate the tidy Zephyr project and run a configure-only west build. + + Returns the path to the generated ``compile_commands.json``. + """ + from esphome.core import EsphomeError + from esphome.framework_helpers import run_command_ok + from esphome.helpers import rmtree + + from .framework import check_and_install, get_build_env, get_build_paths + + # Surface ESPHome's INFO logs (sdk-nrf download/west update) -- they go + # through logging, which the clang-tidy script otherwise leaves at + # WARNING, so the first-run installation looks silent without this. + logging.basicConfig(level=logging.INFO, format="%(message)s") + + _setup_core(work_dir) + check_and_install() + + library_include_dirs = "\n".join( + f' "{d}"' for d in _library_include_dirs(platformio_ini) + ) + source_dir = work_dir / "zephyr" + source_dir.mkdir(parents=True, exist_ok=True) + (source_dir / "CMakeLists.txt").write_text( + _tidy_cmakelists(library_include_dirs), encoding="utf-8" + ) + (source_dir / "main.cpp").write_text(_TIDY_MAIN_CPP, encoding="utf-8") + (source_dir / "prj.conf").write_text(_TIDY_PRJ_CONF, encoding="utf-8") + + # Always configure from scratch: west can't pristine a dir whose CMake + # cache is stale/missing, and a configure-only run is cheap. + build_dir = work_dir / "build" + if build_dir.is_dir(): + rmtree(build_dir) + + paths = get_build_paths() + # Build only the generated-headers target (syscall_list.h, offsets.h, ...) + # on top of the configure: clang-tidy needs those headers to exist, but a + # full firmware build would be wasted work. --no-sysbuild keeps sdk-nrf + # 2.9+ from wrapping the build in a multi-image sysbuild project, which + # would nest the compile commands and hide the headers target. + west_cmd = [ + str(paths["python_executable"]), + "-m", + "west", + "build", + "--no-sysbuild", + "-b", + _TIDY_BOARD, + "-d", + str(build_dir), + str(source_dir), + "-t", + "zephyr_generated_headers", + "--", + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", + ] + if not run_command_ok( + west_cmd, + env=get_build_env(), + stream_output=True, + cwd=str(paths["framework_path"]), + ): + raise EsphomeError("nRF52 clang-tidy configure failed") + + return build_dir / "compile_commands.json" diff --git a/esphome/components/nrf52/framework.py b/esphome/components/nrf52/framework.py index 640aa07fbf..6b32fe1fea 100644 --- a/esphome/components/nrf52/framework.py +++ b/esphome/components/nrf52/framework.py @@ -1,8 +1,10 @@ +import hashlib import logging import os from pathlib import Path import platform import shutil +import sys import tempfile import platformdirs @@ -24,7 +26,12 @@ from esphome.helpers import get_str_env _LOGGER = logging.getLogger(__name__) _REQUIREMENTS = Path(__file__).parent / "requirements.txt" -_TOOLCHAIN_VERSION = "0.17.4" +TOOLCHAIN_VERSION = "0.17.4" + +# Packages the PlatformIO toolchain's Zephyr build script needs beyond west +# (which comes from requirements.txt). Keep the pin in sync with +# framework-sdk-nrf scripts/platformio/platformio-build.py. +_PLATFORMIO_PENV_REQUIREMENTS: tuple[str, ...] = ("cbor2==5.6.5",) SDK_NG_TOOLCHAIN_MIRRORS = str_to_lst_of_str( os.environ.get( @@ -132,10 +139,94 @@ def get_build_env() -> dict: env = os.environ.copy() env["PATH"] = str(venv_bin_dir) + os.pathsep + env.get("PATH", "") env["ZEPHYR_BASE"] = str(_get_framework_path(version) / "zephyr") - env["Zephyr-sdk_DIR"] = str(_get_toolchain_path(_TOOLCHAIN_VERSION) / "cmake") + # ZEPHYR_SDK_INSTALL_DIR is the variable Zephyr documents for pointing at + # the SDK: FindZephyr-sdk.cmake reads it (from the environment, via + # zephyr_get) and passes it straight to find_package as a HINT. This + # matters because the SDK lives in the esphome cache dir, which is not on + # the module's static search path (/usr, /opt, $HOME, ...). A generic + # "Zephyr-sdk_DIR" environment hint proved unreliable here: containerized + # non-root builds failed to locate the SDK with it, while + # ZEPHYR_SDK_INSTALL_DIR fixed the same invocation. + env["ZEPHYR_SDK_INSTALL_DIR"] = str(_get_toolchain_path(TOOLCHAIN_VERSION)) return env +def _get_platformio_penv_path() -> Path: + return get_sdk_nrf_tools_path() / "penvs" / "platformio" + + +def _get_penv_site_packages(penv_path: Path) -> Path: + if os.name == "nt": + return penv_path / "Lib" / "site-packages" + python_dir = f"python{sys.version_info.major}.{sys.version_info.minor}" + return penv_path / "lib" / python_dir / "site-packages" + + +def _prepend_env_path(name: str, entry: str) -> None: + """Prepend ``entry`` to the ``os.pathsep``-separated env var ``name``.""" + current = os.environ.get(name, "") + entries = current.split(os.pathsep) if current else [] + if entry not in entries: + os.environ[name] = os.pathsep.join([entry, *entries]) + + +def setup_platformio_python_env() -> None: + """Make the Zephyr build's Python packages available to PlatformIO. + + The PlatformIO toolchain's Zephyr framework build script pip-installs + west and cbor2 (and pyocd on x86_64) into the Python environment running + PlatformIO whenever they are not importable. That environment is not + always writable — for example the docker image run as a non-root user, + where ESPHome lives in the system Python — so the install fails with + "Permission denied". Instead, pre-install those packages into a dedicated + venv under the sdk-nrf tools dir and expose it to the PlatformIO + subprocesses through the environment: + + * PYTHONPATH makes the venv's packages importable from the interpreter + that runs PlatformIO/SCons, so the build script skips its installs. + * VIRTUAL_ENV redirects any install the build script still performs via + uv (pyocd is fetched on demand) into the writable venv. + * PATH exposes console scripts installed into the venv (e.g. pyocd). + """ + penv_path = _get_platformio_penv_path() + env_python_path = get_python_env_executable_path(penv_path, "python") + sentinel = penv_path / ".ready" + # Include the Python version: the venv breaks when the interpreter it + # was created from is upgraded, so it must be rebuilt. + requirements_hash = hashlib.sha256( + _REQUIREMENTS.read_bytes() + + "\n".join(_PLATFORMIO_PENV_REQUIREMENTS).encode() + + f"python{sys.version_info.major}.{sys.version_info.minor}".encode() + ).hexdigest() + if ( + not sentinel.exists() + or sentinel.read_text(encoding="utf-8") != requirements_hash + ): + rmdir(penv_path, msg="Clean up PlatformIO toolchain Python environment") + + create_venv(penv_path, msg="PlatformIO toolchain") + + _LOGGER.info("Installing PlatformIO toolchain requirements ...") + cmd = [ + str(env_python_path), + "-m", + "pip", + "install", + "-r", + str(_REQUIREMENTS), + *_PLATFORMIO_PENV_REQUIREMENTS, + ] + if not run_command_ok(cmd): + raise EsphomeError( + "Install requirements for PlatformIO toolchain Python environment failure" + ) + sentinel.write_text(requirements_hash, encoding="utf-8") + + os.environ["VIRTUAL_ENV"] = str(penv_path) + _prepend_env_path("PYTHONPATH", str(_get_penv_site_packages(penv_path))) + _prepend_env_path("PATH", str(env_python_path.parent)) + + def _patch_uf2conv_escape_sequences(framework_path: Path) -> None: # SDK v2.6.1 ships uf2conv.py with '\s+' — an unrecognised escape that # Python 3.12+ flags with SyntaxWarning (a future version will reject it). @@ -158,9 +249,10 @@ def check_and_install() -> None: python_env_path = _get_python_env_path(version) env_python_path = get_python_env_executable_path(python_env_path, "python") sentinel = python_env_path / ".ready" + requirements_hash = hashlib.sha256(_REQUIREMENTS.read_bytes()).hexdigest() install_venv = ( not sentinel.exists() - or _REQUIREMENTS.stat().st_mtime > sentinel.stat().st_mtime + or sentinel.read_text(encoding="utf-8") != requirements_hash ) if install_venv: rmdir(python_env_path, msg=f"Clean up {version} Python environment") @@ -182,7 +274,7 @@ def check_and_install() -> None: raise EsphomeError( f"Install requirements for {version} Python environment failure" ) - sentinel.touch() + sentinel.write_text(requirements_hash, encoding="utf-8") framework_path = _get_framework_path(version) sentinel = framework_path / ".ready" @@ -197,6 +289,7 @@ def check_and_install() -> None: "init", "-m", "https://github.com/nrfconnect/sdk-nrf", + "-o=--depth=1", "--mr", version, str(framework_path), @@ -238,19 +331,17 @@ def check_and_install() -> None: raise EsphomeError(f"Install Zephyr requirements for {version} failure") zephyr_sentinel.touch() - toolchains_dir = _get_toolchain_path(_TOOLCHAIN_VERSION) + toolchains_dir = _get_toolchain_path(TOOLCHAIN_VERSION) sentinel = toolchains_dir / ".ready" if not sentinel.exists(): - rmdir( - toolchains_dir, msg=f"Clean up {_TOOLCHAIN_VERSION} toolchain environment" - ) + rmdir(toolchains_dir, msg=f"Clean up {TOOLCHAIN_VERSION} toolchain environment") sysname, machine, extension = _get_toolchain_platform_info() with tempfile.NamedTemporaryFile() as tmp: - _LOGGER.info("Downloading Zephyr SDK %s minimal ...", _TOOLCHAIN_VERSION) + _LOGGER.info("Downloading Zephyr SDK %s minimal ...", TOOLCHAIN_VERSION) download_from_mirrors( SDK_NG_MINIMAL_MIRRORS, { - "VERSION": _TOOLCHAIN_VERSION, + "VERSION": TOOLCHAIN_VERSION, "sysname": sysname, "machine": machine, "extension": extension, @@ -259,11 +350,11 @@ def check_and_install() -> None: ) archive_extract_all(tmp.file, toolchains_dir, progress_header="Extracting") with tempfile.NamedTemporaryFile() as tmp: - _LOGGER.info("Downloading %s toolchain ...", _TOOLCHAIN_VERSION) + _LOGGER.info("Downloading %s toolchain ...", TOOLCHAIN_VERSION) download_from_mirrors( SDK_NG_TOOLCHAIN_MIRRORS, { - "VERSION": _TOOLCHAIN_VERSION, + "VERSION": TOOLCHAIN_VERSION, "sysname": sysname, "machine": machine, "extension": extension, diff --git a/esphome/components/nrf52/ota.py b/esphome/components/nrf52/ota.py index 5d608acbac..cafeda6478 100644 --- a/esphome/components/nrf52/ota.py +++ b/esphome/components/nrf52/ota.py @@ -5,7 +5,7 @@ import logging from pathlib import Path from bleak import BleakScanner -from bleak.exc import BleakDeviceNotFoundError +from bleak.exc import BleakDBusError, BleakDeviceNotFoundError from smp.exceptions import SMPBadStartDelimiter from smpclient import SMPClient from smpclient.generics import error, success @@ -98,6 +98,12 @@ async def _smpmgr_upload(device: str, firmware: Path) -> None: await smp_client.connect() except BleakDeviceNotFoundError as exc: raise EsphomeError(f"Device {device} not found") from exc + except BleakDBusError as exc: + if "NotPermitted" in exc.dbus_error: + raise EsphomeError( + f"Cannot connect to {device}: Make sure the device is paired." + ) from exc + raise EsphomeError(f"BLE error connecting to {device}: {exc}") from exc except SMPBLETransportException as exc: raise EsphomeError(f"Connection error with {device}") from exc diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index bcc609de65..ea0c2d77f6 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -212,9 +212,15 @@ _NUMBER_SCHEMA = ( }, cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), ), - cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, - cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True), - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional( + CONF_UNIT_OF_MEASUREMENT, visibility=cv.Visibility.ADVANCED + ): validate_unit_of_measurement, + cv.Optional( + CONF_MODE, default="AUTO", visibility=cv.Visibility.ADVANCED + ): cv.enum(NUMBER_MODES, upper=True), + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): validate_device_class, } ) ) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index ee4d5abb1c..552a43acad 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -1,150 +1,35 @@ -import logging +# --------------------------------------------------------------------------- +# Legacy top-level `online_image:` deprecation shim -- REMOVE this whole file +# after 2027.1.0. +# +# Online images are now a platform of the `image:` component (`platform: +# online_image`); the real schema, actions and codegen live in `image.py`. This +# module only keeps the deprecated top-level `online_image:` key working during +# the deprecation window: it reuses that schema/codegen and adds a one-shot +# deprecation warning (with a pasteable migrated `image:` block) at validation +# time. Deleting this file drops the top-level form entirely. +# --------------------------------------------------------------------------- -from esphome import automation -import esphome.codegen as cg -from esphome.components import runtime_image -from esphome.components.const import CONF_REQUEST_HEADERS -from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent -from esphome.components.image import CONF_TRANSPARENCY, add_metadata +import esphome.components.image as espImage import esphome.config_validation as cv -from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL -from esphome.core import Lambda + +from .image import ONLINE_IMAGE_CONFIG_SCHEMA, setup_online_image AUTO_LOAD = ["image", "runtime_image"] DEPENDENCIES = ["display", "http_request"] CODEOWNERS = ["@guillempages", "@clydebarrow"] MULTI_CONF = True -CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" -CONF_UPDATE = "update" +DOMAIN = "online_image" -_LOGGER = logging.getLogger(__name__) +LEGACY_REMOVAL_VERSION = "2027.1.0" -online_image_ns = cg.esphome_ns.namespace("online_image") - -OnlineImage = online_image_ns.class_( - "OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage +_capture_legacy_entry, _warn_legacy_online_image = ( + espImage.legacy_platform_migration_warning(DOMAIN, DOMAIN, LEGACY_REMOVAL_VERSION) ) -# Actions -SetUrlAction = online_image_ns.class_( - "OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage) -) -ReleaseImageAction = online_image_ns.class_( - "OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage) -) +CONFIG_SCHEMA = cv.All(_capture_legacy_entry, ONLINE_IMAGE_CONFIG_SCHEMA) +FINAL_VALIDATE_SCHEMA = _warn_legacy_online_image -ONLINE_IMAGE_SCHEMA = ( - runtime_image.runtime_image_schema(OnlineImage) - .extend( - { - # Online Image specific options - cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), - cv.Required(CONF_URL): cv.url, - cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), - cv.Optional(CONF_REQUEST_HEADERS): cv.All( - cv.Schema({cv.string: cv.templatable(cv.string)}) - ), - cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}), - cv.Optional(CONF_ON_ERROR): automation.validate_automation({}), - } - ) - .extend(cv.polling_component_schema("never")) -) - -CONFIG_SCHEMA = cv.Schema( - cv.All( - ONLINE_IMAGE_SCHEMA, - cv.require_framework_version( - # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed - # esp8266_arduino=cv.Version(2, 7, 0), - esp32_arduino=cv.Version(0, 0, 0), - esp_idf=cv.Version(4, 0, 0), - rp2040_arduino=cv.Version(0, 0, 0), - host=cv.Version(0, 0, 0), - ), - runtime_image.validate_runtime_image_settings, - ) -) - -SET_URL_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(OnlineImage), - cv.Required(CONF_URL): cv.templatable(cv.url), - cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool), - } -) - -RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id( - { - cv.GenerateID(): cv.use_id(OnlineImage), - } -) - - -@automation.register_action( - "online_image.set_url", SetUrlAction, SET_URL_SCHEMA, synchronous=True -) -@automation.register_action( - "online_image.release", - ReleaseImageAction, - RELEASE_IMAGE_SCHEMA, - synchronous=True, -) -async def online_image_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - - if CONF_URL in config: - template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) - cg.add(var.set_url(template_)) - if CONF_UPDATE in config: - template_ = await cg.templatable(config[CONF_UPDATE], args, cg.bool_) - cg.add(var.set_update(template_)) - return var - - -_CALLBACK_AUTOMATIONS = ( - automation.CallbackAutomation( - CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")] - ), - automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"), -) - - -async def to_code(config): - # Use the enhanced helper function to get all runtime image parameters - settings = await runtime_image.process_runtime_image_config(config) - add_metadata( - config[CONF_ID], - settings.width, - settings.height, - config[CONF_TYPE], - config[CONF_TRANSPARENCY], - ) - - url = config[CONF_URL] - var = cg.new_Pvariable( - config[CONF_ID], - url, - settings.width, - settings.height, - settings.format_enum, - settings.image_type_enum, - settings.transparent, - settings.placeholder or cg.nullptr, - config[CONF_BUFFER_SIZE], - settings.byte_order_big_endian, - ) - await cg.register_component(var, config) - await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) - - for key, value in config.get(CONF_REQUEST_HEADERS, {}).items(): - if isinstance(value, Lambda): - template_ = await cg.templatable(value, [], cg.std_string) - cg.add(var.add_request_header(key, template_)) - else: - cg.add(var.add_request_header(key, value)) - - await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) +to_code = setup_online_image diff --git a/esphome/components/online_image/image.py b/esphome/components/online_image/image.py new file mode 100644 index 0000000000..cb86f93e29 --- /dev/null +++ b/esphome/components/online_image/image.py @@ -0,0 +1,152 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import runtime_image +from esphome.components.const import CONF_REQUEST_HEADERS +from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent +from esphome.components.image import CONF_TRANSPARENCY, add_metadata +import esphome.config_validation as cv +from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL +from esphome.core import Lambda +from esphome.types import ConfigType + +AUTO_LOAD = ["runtime_image"] +DEPENDENCIES = ["http_request"] +CODEOWNERS = ["@guillempages", "@clydebarrow"] + +CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" +CONF_UPDATE = "update" + +online_image_ns = cg.esphome_ns.namespace("online_image") + +OnlineImage = online_image_ns.class_( + "OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage +) + +# Actions +SetUrlAction = online_image_ns.class_( + "OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage) +) +ReleaseImageAction = online_image_ns.class_( + "OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage) +) + + +ONLINE_IMAGE_SCHEMA = ( + runtime_image.runtime_image_schema(OnlineImage) + .extend( + { + # Online Image specific options + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + cv.Required(CONF_URL): cv.url, + cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), + cv.Optional(CONF_REQUEST_HEADERS): cv.All( + cv.Schema({cv.string: cv.templatable(cv.string)}) + ), + cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}), + cv.Optional(CONF_ON_ERROR): automation.validate_automation({}), + } + ) + .extend(cv.polling_component_schema("never")) +) + +# Shared schema used by both the (deprecated) top-level `online_image:` key and +# the `image:` `platform: online_image` entry. +ONLINE_IMAGE_CONFIG_SCHEMA = cv.All( + ONLINE_IMAGE_SCHEMA, + cv.require_framework_version( + # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed + # esp8266_arduino=cv.Version(2, 7, 0), + esp32_arduino=cv.Version(0, 0, 0), + esp_idf=cv.Version(4, 0, 0), + rp2_arduino=cv.Version(0, 0, 0), + host=cv.Version(0, 0, 0), + ), + runtime_image.validate_runtime_image_settings, +) + + +SET_URL_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(OnlineImage), + cv.Required(CONF_URL): cv.templatable(cv.url), + cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool), + } +) + +RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(OnlineImage), + } +) + + +@automation.register_action( + "online_image.set_url", SetUrlAction, SET_URL_SCHEMA, synchronous=True +) +@automation.register_action( + "online_image.release", + ReleaseImageAction, + RELEASE_IMAGE_SCHEMA, + synchronous=True, +) +async def online_image_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + if CONF_URL in config: + template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) + cg.add(var.set_url(template_)) + if CONF_UPDATE in config: + template_ = await cg.templatable(config[CONF_UPDATE], args, cg.bool_) + cg.add(var.set_update(template_)) + return var + + +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")] + ), + automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"), +) + + +async def setup_online_image(config: ConfigType) -> None: + # Use the enhanced helper function to get all runtime image parameters + settings = await runtime_image.process_runtime_image_config(config) + add_metadata( + config[CONF_ID], + settings.width, + settings.height, + config[CONF_TYPE], + config[CONF_TRANSPARENCY], + ) + + url = config[CONF_URL] + var = cg.new_Pvariable( + config[CONF_ID], + url, + settings.width, + settings.height, + settings.format_enum, + settings.image_type_enum, + settings.transparent, + settings.placeholder or cg.nullptr, + config[CONF_BUFFER_SIZE], + settings.byte_order_big_endian, + ) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) + + for key, value in config.get(CONF_REQUEST_HEADERS, {}).items(): + if isinstance(value, Lambda): + template_ = await cg.templatable(value, [], cg.std_string) + cg.add(var.add_request_header(key, template_)) + else: + cg.add(var.add_request_header(key, value)) + + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) + + +CONFIG_SCHEMA = ONLINE_IMAGE_CONFIG_SCHEMA + +to_code = setup_online_image diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp index e2828a9e30..f8b515fa05 100644 --- a/esphome/components/opentherm/hub.cpp +++ b/esphome/components/opentherm/hub.cpp @@ -27,11 +27,11 @@ uint8_t parse_u8_lb(OpenthermData &data) { return data.valueLB; } uint8_t parse_u8_hb(OpenthermData &data) { return data.valueHB; } int8_t parse_s8_lb(OpenthermData &data) { return (int8_t) data.valueLB; } int8_t parse_s8_hb(OpenthermData &data) { return (int8_t) data.valueHB; } -uint16_t parse_u16(OpenthermData &data) { return data.u16(); } +uint16_t parse_u16(OpenthermData &data) { return data.get_u16(); } uint16_t parse_u8_lb_60(OpenthermData &data) { return data.valueLB * 60; } uint16_t parse_u8_hb_60(OpenthermData &data) { return data.valueHB * 60; } -int16_t parse_s16(OpenthermData &data) { return data.s16(); } -float parse_f88(OpenthermData &data) { return data.f88(); } +int16_t parse_s16(OpenthermData &data) { return data.get_s16(); } +float parse_f88(OpenthermData &data) { return data.get_f88(); } void write_flag8_lb_0(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 0, value); } void write_flag8_lb_1(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 1, value); } @@ -53,9 +53,9 @@ void write_u8_lb(const uint8_t value, OpenthermData &data) { data.valueLB = valu void write_u8_hb(const uint8_t value, OpenthermData &data) { data.valueHB = value; } void write_s8_lb(const int8_t value, OpenthermData &data) { data.valueLB = (uint8_t) value; } void write_s8_hb(const int8_t value, OpenthermData &data) { data.valueHB = (uint8_t) value; } -void write_u16(const uint16_t value, OpenthermData &data) { data.u16(value); } -void write_s16(const int16_t value, OpenthermData &data) { data.s16(value); } -void write_f88(const float value, OpenthermData &data) { data.f88(value); } +void write_u16(const uint16_t value, OpenthermData &data) { data.set_u16(value); } +void write_s16(const int16_t value, OpenthermData &data) { data.set_s16(value); } +void write_f88(const float value, OpenthermData &data) { data.set_f88(value); } } // namespace message_data diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 5cf7c19880..3343871cd0 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -114,6 +114,7 @@ bool OpenTherm::get_protocol_error(OpenThermError &error) { void OpenTherm::stop() { this->stop_timer_(); this->mode_ = OperationMode::IDLE; + this->out_pin_->digital_write(true); } void IRAM_ATTR OpenTherm::read_() { @@ -533,34 +534,34 @@ void OpenTherm::debug_data(OpenthermData &data) { ESP_LOGD(TAG, "%s %s %s %s", format_bin_to(type_buf, data.type), format_bin_to(id_buf, data.id), format_bin_to(hb_buf, data.valueHB), format_bin_to(lb_buf, data.valueLB)); ESP_LOGD(TAG, "type: %s; id: %u; HB: %u; LB: %u; uint_16: %u; float: %f", - this->message_type_to_str((MessageType) data.type), data.id, data.valueHB, data.valueLB, data.u16(), - data.f88()); + this->message_type_to_str((MessageType) data.type), data.id, data.valueHB, data.valueLB, data.get_u16(), + data.get_f88()); } void OpenTherm::debug_error(OpenThermError &error) const { ESP_LOGD(TAG, "data: 0x%08" PRIx32 "; clock: %u; capture: 0x%08" PRIx32 "; bit_pos: %u", error.data, this->clock_, error.capture, error.bit_pos); } -float OpenthermData::f88() { return ((float) this->s16()) / 256.0f; } +float OpenthermData::get_f88() { return ((float) this->get_s16()) / 256.0f; } -void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); } +void OpenthermData::set_f88(float value) { this->set_s16((int16_t) (value * 256)); } -uint16_t OpenthermData::u16() { +uint16_t OpenthermData::get_u16() { uint16_t const value = this->valueHB; return (value << 8) | this->valueLB; } -void OpenthermData::u16(uint16_t value) { +void OpenthermData::set_u16(uint16_t value) { this->valueLB = value & 0xFF; this->valueHB = (value >> 8) & 0xFF; } -int16_t OpenthermData::s16() { +int16_t OpenthermData::get_s16() { int16_t const value = this->valueHB; return (value << 8) | this->valueLB; } -void OpenthermData::s16(int16_t value) { +void OpenthermData::set_s16(int16_t value) { this->valueLB = value & 0xFF; this->valueHB = (value >> 8) & 0xFF; } diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index 3078e92c9d..7aa81cd8a2 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -178,7 +178,7 @@ enum BitPositions { STOP_BIT = 33 }; /** * Structure to hold Opentherm data packet content. - * Use f88(), u16() or s16() functions to get appropriate value of data packet accoridng to id of message. + * Use get_f88(), get_u16() or get_s16() functions to get appropriate value of data packet according to id of message. */ struct OpenthermData { uint8_t type; @@ -191,32 +191,32 @@ struct OpenthermData { /** * @return float representation of data packet value */ - float f88(); + float get_f88(); /** * @param float number to set as value of this data packet */ - void f88(float value); + void set_f88(float value); /** * @return unsigned 16b integer representation of data packet value */ - uint16_t u16(); + uint16_t get_u16(); /** * @param unsigned 16b integer number to set as value of this data packet */ - void u16(uint16_t value); + void set_u16(uint16_t value); /** * @return signed 16b integer representation of data packet value */ - int16_t s16(); + int16_t get_s16(); /** * @param signed 16b integer number to set as value of this data packet */ - void s16(int16_t value); + void set_s16(int16_t value); }; struct OpenThermError { diff --git a/esphome/components/opentherm/schema.py b/esphome/components/opentherm/schema.py index f70c8e24db..7f3ea2df36 100644 --- a/esphome/components/opentherm/schema.py +++ b/esphome/components/opentherm/schema.py @@ -91,7 +91,7 @@ SENSORS: dict[str, SensorSchema] = { ), "dhw_flow_rate": SensorSchema( description="Water flow rate in DHW circuit", - unit_of_measurement="l/min", + unit_of_measurement="L/min", accuracy_decimals=2, icon="mdi:waves-arrow-right", state_class=STATE_CLASS_MEASUREMENT, diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index b54fe2b218..4018ad81e7 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -14,6 +14,7 @@ from esphome.components.esp32 import ( require_vfs_select, ) from esphome.components.mdns import MDNSComponent, enable_mdns_storage +from esphome.components.network import add_use_address from esphome.components.zephyr import zephyr_add_prj_conf from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv @@ -288,7 +289,7 @@ async def to_code(config): enable_mdns_storage() ot = cg.new_Pvariable(config[CONF_ID]) - cg.add(ot.set_use_address(config[CONF_USE_ADDRESS])) + add_use_address(ot, config[CONF_USE_ADDRESS]) await cg.register_component(ot, config) if (poll_period := config.get(CONF_POLL_PERIOD)) is not None: cg.add(ot.set_poll_period(poll_period)) diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index eb48d8a74a..b4654af21f 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -39,6 +39,8 @@ class OpenThreadComponent final : public Component { void on_factory_reset(std::function callback); void defer_factory_reset_external_callback(); + /// Returns nullptr when no explicit use_address is configured and the address is + /// derived at runtime from the device name (see network::get_use_address_to()). const char *get_use_address() const { return this->use_address_; } void set_use_address(const char *use_address) { this->use_address_ = use_address; } #if CONFIG_OPENTHREAD_MTD @@ -76,7 +78,7 @@ class OpenThreadComponent final : public Component { private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. - const char *use_address_{""}; + const char *use_address_{nullptr}; }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread_info/openthread_info_sensor.cpp b/esphome/components/openthread_info/openthread_info_sensor.cpp new file mode 100644 index 0000000000..9beb52d64b --- /dev/null +++ b/esphome/components/openthread_info/openthread_info_sensor.cpp @@ -0,0 +1,24 @@ +#include "openthread_info_sensor.h" +#if defined(USE_OPENTHREAD) && defined(USE_SENSOR) +#include "esphome/core/log.h" + +namespace esphome::openthread_info { + +static const char *const TAG = "openthread_info"; + +void ParentAverageRssiOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Average RSSI", this); } +void ParentLastRssiOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Last RSSI", this); } +void ParentLinkQualityInOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Link Quality In", this); } +void ParentLinkQualityOutOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Link Quality Out", this); } +void TxTotalOpenThreadInfo::dump_config() { LOG_SENSOR("", "TX Total", this); } +void TxRetriesOpenThreadInfo::dump_config() { LOG_SENSOR("", "TX Retries", this); } +void TxErrCcaOpenThreadInfo::dump_config() { LOG_SENSOR("", "TX CCA Errors", this); } +void TxErrAbortOpenThreadInfo::dump_config() { LOG_SENSOR("", "TX Abort Errors", this); } +void RxTotalOpenThreadInfo::dump_config() { LOG_SENSOR("", "RX Total", this); } +void RxErrFcsOpenThreadInfo::dump_config() { LOG_SENSOR("", "RX FCS Errors", this); } +void AttachAttemptsOpenThreadInfo::dump_config() { LOG_SENSOR("", "Attach Attempts", this); } +void ParentChangesOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Changes", this); } +void PartitionIdChangesOpenThreadInfo::dump_config() { LOG_SENSOR("", "Partition ID Changes", this); } + +} // namespace esphome::openthread_info +#endif diff --git a/esphome/components/openthread_info/openthread_info_sensor.h b/esphome/components/openthread_info/openthread_info_sensor.h new file mode 100644 index 0000000000..dcc90da0c0 --- /dev/null +++ b/esphome/components/openthread_info/openthread_info_sensor.h @@ -0,0 +1,131 @@ +#pragma once + +#include "esphome/core/defines.h" +#if defined(USE_OPENTHREAD) && defined(USE_SENSOR) + +#include "openthread_info_text_sensor.h" +#include "esphome/components/sensor/sensor.h" + +#include +#include + +namespace esphome::openthread_info { + +// Parent RSSI (average) in dBm. Only valid when device is a child; skips publish otherwise. +class ParentAverageRssiOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { + int8_t rssi; + if (otThreadGetParentAverageRssi(instance, &rssi) != OT_ERROR_NONE) { + return; + } + this->publish_state(rssi); + } + void dump_config() override; +}; + +// Parent RSSI (last received frame) in dBm. Only valid when device is a child. +class ParentLastRssiOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { + int8_t rssi; + if (otThreadGetParentLastRssi(instance, &rssi) != OT_ERROR_NONE) { + return; + } + this->publish_state(rssi); + } + void dump_config() override; +}; + +// Incoming link quality from parent (0-3). Only valid when device is a child. +class ParentLinkQualityInOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { + otRouterInfo parent_info; + if (otThreadGetParentInfo(instance, &parent_info) != OT_ERROR_NONE) { + return; + } + this->publish_state(parent_info.mLinkQualityIn); + } + void dump_config() override; +}; + +// Outgoing link quality to parent (0-3). Only valid when device is a child. +class ParentLinkQualityOutOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { + otRouterInfo parent_info; + if (otThreadGetParentInfo(instance, &parent_info) != OT_ERROR_NONE) { + return; + } + this->publish_state(parent_info.mLinkQualityOut); + } + void dump_config() override; +}; + +// --- MAC counters (otLinkGetCounters) — cumulative since boot --- + +class TxTotalOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mTxTotal); } + void dump_config() override; +}; + +class TxRetriesOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mTxRetry); } + void dump_config() override; +}; + +class TxErrCcaOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mTxErrCca); } + void dump_config() override; +}; + +class TxErrAbortOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mTxErrAbort); } + void dump_config() override; +}; + +class RxTotalOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mRxTotal); } + void dump_config() override; +}; + +class RxErrFcsOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mRxErrFcs); } + void dump_config() override; +}; + +// --- MLE stability counters (otThreadGetMleCounters) — cumulative since boot --- + +class AttachAttemptsOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { + this->publish_state(otThreadGetMleCounters(instance)->mAttachAttempts); + } + void dump_config() override; +}; + +class ParentChangesOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { + this->publish_state(otThreadGetMleCounters(instance)->mParentChanges); + } + void dump_config() override; +}; + +class PartitionIdChangesOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor { + public: + void update_instance(otInstance *instance) override { + this->publish_state(otThreadGetMleCounters(instance)->mPartitionIdChanges); + } + void dump_config() override; +}; + +} // namespace esphome::openthread_info +#endif diff --git a/esphome/components/openthread_info/sensor.py b/esphome/components/openthread_info/sensor.py new file mode 100644 index 0000000000..4d5b3d54f4 --- /dev/null +++ b/esphome/components/openthread_info/sensor.py @@ -0,0 +1,188 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_DECIBEL_MILLIWATT, + UNIT_EMPTY, +) + +CONF_PARENT_AVERAGE_RSSI = "parent_average_rssi" +CONF_PARENT_LAST_RSSI = "parent_last_rssi" +CONF_PARENT_LINK_QUALITY_IN = "parent_link_quality_in" +CONF_PARENT_LINK_QUALITY_OUT = "parent_link_quality_out" +CONF_TX_TOTAL = "tx_total" +CONF_TX_RETRIES = "tx_retries" +CONF_TX_ERR_CCA = "tx_err_cca" +CONF_TX_ERR_ABORT = "tx_err_abort" +CONF_RX_TOTAL = "rx_total" +CONF_RX_ERR_FCS = "rx_err_fcs" +CONF_ATTACH_ATTEMPTS = "attach_attempts" +CONF_PARENT_CHANGES = "parent_changes" +CONF_PARTITION_ID_CHANGES = "partition_id_changes" + +DEPENDENCIES = ["openthread"] + +openthread_info_ns = cg.esphome_ns.namespace("openthread_info") +ParentAverageRssiOpenThreadInfo = openthread_info_ns.class_( + "ParentAverageRssiOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +ParentLastRssiOpenThreadInfo = openthread_info_ns.class_( + "ParentLastRssiOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +ParentLinkQualityInOpenThreadInfo = openthread_info_ns.class_( + "ParentLinkQualityInOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +ParentLinkQualityOutOpenThreadInfo = openthread_info_ns.class_( + "ParentLinkQualityOutOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +TxTotalOpenThreadInfo = openthread_info_ns.class_( + "TxTotalOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +TxRetriesOpenThreadInfo = openthread_info_ns.class_( + "TxRetriesOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +TxErrCcaOpenThreadInfo = openthread_info_ns.class_( + "TxErrCcaOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +TxErrAbortOpenThreadInfo = openthread_info_ns.class_( + "TxErrAbortOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +RxTotalOpenThreadInfo = openthread_info_ns.class_( + "RxTotalOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +RxErrFcsOpenThreadInfo = openthread_info_ns.class_( + "RxErrFcsOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +AttachAttemptsOpenThreadInfo = openthread_info_ns.class_( + "AttachAttemptsOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +ParentChangesOpenThreadInfo = openthread_info_ns.class_( + "ParentChangesOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) +PartitionIdChangesOpenThreadInfo = openthread_info_ns.class_( + "PartitionIdChangesOpenThreadInfo", sensor.Sensor, cg.PollingComponent +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PARENT_AVERAGE_RSSI): sensor.sensor_schema( + ParentAverageRssiOpenThreadInfo, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("5s")), + cv.Optional(CONF_PARENT_LAST_RSSI): sensor.sensor_schema( + ParentLastRssiOpenThreadInfo, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("5s")), + cv.Optional(CONF_PARENT_LINK_QUALITY_IN): sensor.sensor_schema( + ParentLinkQualityInOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("5s")), + cv.Optional(CONF_PARENT_LINK_QUALITY_OUT): sensor.sensor_schema( + ParentLinkQualityOutOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("5s")), + cv.Optional(CONF_TX_TOTAL): sensor.sensor_schema( + TxTotalOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("30s")), + cv.Optional(CONF_TX_RETRIES): sensor.sensor_schema( + TxRetriesOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("30s")), + cv.Optional(CONF_TX_ERR_CCA): sensor.sensor_schema( + TxErrCcaOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("30s")), + cv.Optional(CONF_TX_ERR_ABORT): sensor.sensor_schema( + TxErrAbortOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("30s")), + cv.Optional(CONF_RX_TOTAL): sensor.sensor_schema( + RxTotalOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("30s")), + cv.Optional(CONF_RX_ERR_FCS): sensor.sensor_schema( + RxErrFcsOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("30s")), + cv.Optional(CONF_ATTACH_ATTEMPTS): sensor.sensor_schema( + AttachAttemptsOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("30s")), + cv.Optional(CONF_PARENT_CHANGES): sensor.sensor_schema( + ParentChangesOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("30s")), + cv.Optional(CONF_PARTITION_ID_CHANGES): sensor.sensor_schema( + PartitionIdChangesOpenThreadInfo, + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("30s")), + } +) + + +async def setup_conf(config: dict, key: str): + if conf := config.get(key): + var = await sensor.new_sensor(conf) + await cg.register_component(var, conf) + + +async def to_code(config): + await setup_conf(config, CONF_PARENT_AVERAGE_RSSI) + await setup_conf(config, CONF_PARENT_LAST_RSSI) + await setup_conf(config, CONF_PARENT_LINK_QUALITY_IN) + await setup_conf(config, CONF_PARENT_LINK_QUALITY_OUT) + await setup_conf(config, CONF_TX_TOTAL) + await setup_conf(config, CONF_TX_RETRIES) + await setup_conf(config, CONF_TX_ERR_CCA) + await setup_conf(config, CONF_TX_ERR_ABORT) + await setup_conf(config, CONF_RX_TOTAL) + await setup_conf(config, CONF_RX_ERR_FCS) + await setup_conf(config, CONF_ATTACH_ATTEMPTS) + await setup_conf(config, CONF_PARENT_CHANGES) + await setup_conf(config, CONF_PARTITION_ID_CHANGES) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 83d8c611d5..8296410f2f 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -99,7 +99,7 @@ async def to_code(config): cg.add_define("USE_OTA") CORE.add_job(final_step) - if CORE.is_rp2040 and CORE.using_arduino: + if CORE.is_rp2 and CORE.using_arduino: cg.add_library("Updater", None) @@ -158,7 +158,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "ota_backend_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, - "ota_backend_arduino_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "ota_backend_arduino_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, "ota_backend_arduino_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 17949de642..0447b968a3 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -2,6 +2,34 @@ namespace esphome::ota { +bool version_is_older(const char *candidate, const char *reference) { + if (candidate == nullptr || reference == nullptr) + return false; + while (true) { + uint32_t a = 0; + while (*candidate >= '0' && *candidate <= '9') { + a = a * 10 + static_cast(*candidate - '0'); + candidate++; + } + uint32_t b = 0; + while (*reference >= '0' && *reference <= '9') { + b = b * 10 + static_cast(*reference - '0'); + reference++; + } + if (a != b) + return a < b; + // Components equal so far; advance past a single separator on each side. + const bool a_more = (*candidate == '.'); + const bool b_more = (*reference == '.'); + if (a_more) + candidate++; + if (b_more) + reference++; + if (!a_more && !b_more) + return false; // Both strings exhausted with all components equal. + } +} + #ifdef USE_OTA_STATE_LISTENER OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index de236c1951..01be46a518 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -46,9 +46,24 @@ enum OTAResponseTypes { OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90, OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91, OTA_RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92, + OTA_RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93, OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, }; +/** Compare two dotted-numeric version strings (such as "1.2.3"). + * + * Returns true when @p candidate represents a strictly older (lower) firmware + * version than @p reference. Each dot-separated component is parsed as an + * integer and compared left-to-right; absent trailing components count as 0, + * so "1.2" and "1.2.0" are equal. Equal versions return false so that + * re-flashing the same version is permitted. + * + * Used for software OTA downgrade protection. Inputs come from the project + * version embedded in the signed firmware image, which is validated to be + * dotted-numeric at config time. Non-digit characters terminate a component. + */ +bool version_is_older(const char *candidate, const char *reference); + enum OTAState { OTA_COMPLETED = 0, OTA_STARTED, diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2.cpp similarity index 70% rename from esphome/components/ota/ota_backend_arduino_rp2040.cpp rename to esphome/components/ota/ota_backend_arduino_rp2.cpp index 0ca0602519..b35eb38c12 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2.cpp @@ -1,9 +1,9 @@ #ifdef USE_ARDUINO -#ifdef USE_RP2040 -#include "ota_backend_arduino_rp2040.h" +#ifdef USE_RP2 +#include "ota_backend_arduino_rp2.h" #include "ota_backend.h" -#include "esphome/components/rp2040/preferences.h" +#include "esphome/components/rp2/preferences.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -11,11 +11,11 @@ namespace esphome::ota { -static const char *const TAG = "ota.arduino_rp2040"; +static const char *const TAG = "ota.arduino_rp2"; -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } -OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size, OTAType ota_type) { +OTAResponseTypes ArduinoRP2OTABackend::begin(size_t image_size, OTAType ota_type) { if (ota_type != OTA_TYPE_UPDATE_APP) { return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; } @@ -23,7 +23,7 @@ OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size, OTAType ota_t // web_server is not supported for RP2040, so this is not an issue. bool ret = Update.begin(image_size, U_FLASH); if (ret) { - rp2040::preferences_prevent_write(true); + rp2::preferences_prevent_write(true); return OTA_RESPONSE_OK; } @@ -42,12 +42,12 @@ OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size, OTAType ota_t return OTA_RESPONSE_ERROR_UNKNOWN; } -void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { +void ArduinoRP2OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); this->md5_set_ = true; } -OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) { +OTAResponseTypes ArduinoRP2OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); if (written == len) { return OTA_RESPONSE_OK; @@ -59,7 +59,7 @@ OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) { return OTA_RESPONSE_ERROR_WRITING_FLASH; } -OTAResponseTypes ArduinoRP2040OTABackend::end() { +OTAResponseTypes ArduinoRP2OTABackend::end() { // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 // This matches the behavior of the old web_server OTA implementation if (Update.end(!this->md5_set_)) { @@ -72,11 +72,11 @@ OTAResponseTypes ArduinoRP2040OTABackend::end() { return OTA_RESPONSE_ERROR_UPDATE_END; } -void ArduinoRP2040OTABackend::abort() { +void ArduinoRP2OTABackend::abort() { Update.end(); - rp2040::preferences_prevent_write(false); + rp2::preferences_prevent_write(false); } } // namespace esphome::ota -#endif // USE_RP2040 +#endif // USE_RP2 #endif // USE_ARDUINO diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2.h similarity index 78% rename from esphome/components/ota/ota_backend_arduino_rp2040.h rename to esphome/components/ota/ota_backend_arduino_rp2.h index d04d5c1a84..f7c0037bd2 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2.h @@ -1,6 +1,6 @@ #pragma once #ifdef USE_ARDUINO -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "ota_backend.h" #include "esphome/core/defines.h" @@ -8,7 +8,7 @@ namespace esphome::ota { -class ArduinoRP2040OTABackend final { +class ArduinoRP2OTABackend final { public: OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); @@ -21,8 +21,8 @@ class ArduinoRP2040OTABackend final { bool md5_set_{false}; }; -std::unique_ptr make_ota_backend(); +std::unique_ptr make_ota_backend(); } // namespace esphome::ota -#endif // USE_RP2040 +#endif // USE_RP2 #endif // USE_ARDUINO diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index ac765d8018..8fd21f42bd 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -9,6 +9,9 @@ #include #include #include +#ifdef USE_OTA_DOWNGRADE_PROTECTION +#include +#endif namespace esphome::ota { @@ -159,6 +162,23 @@ OTAResponseTypes IDFOTABackend::end() { } #endif if (err == ESP_OK) { +#ifdef USE_OTA_DOWNGRADE_PROTECTION + // The image is written and (when signing is enabled) signature-verified by + // esp_ota_end(), so its embedded project version can be trusted. Reject the + // update if it is older than the running version by leaving the boot + // partition unchanged -- the staged image simply never boots. + esp_app_desc_t incoming; + esp_err_t desc_err = esp_ota_get_partition_description(this->partition_, &incoming); + if (desc_err != ESP_OK) { + // Couldn't read the staged image's version, so the comparison is skipped. + // Warn so the bypassed check is observable rather than silent. + ESP_LOGW(TAG, "Downgrade protection: could not read image version (err=0x%X); allowing update", desc_err); + } else if (version_is_older(incoming.version, ESPHOME_PROJECT_VERSION)) { + ESP_LOGE(TAG, "Rejecting downgrade: image version '%s' is older than running version '%s'", incoming.version, + ESPHOME_PROJECT_VERSION); + return OTA_RESPONSE_ERROR_VERSION_DOWNGRADE; + } +#endif err = esp_ota_set_boot_partition(this->partition_); if (err == ESP_OK) { return OTA_RESPONSE_OK; diff --git a/esphome/components/ota/ota_backend_factory.h b/esphome/components/ota/ota_backend_factory.h index 7c79f02702..c543983d8d 100644 --- a/esphome/components/ota/ota_backend_factory.h +++ b/esphome/components/ota/ota_backend_factory.h @@ -8,8 +8,8 @@ #include "ota_backend_esp8266.h" #elif defined(USE_ESP32) #include "ota_backend_esp_idf.h" -#elif defined(USE_RP2040) -#include "ota_backend_arduino_rp2040.h" +#elif defined(USE_RP2) +#include "ota_backend_arduino_rp2.h" #elif defined(USE_LIBRETINY) #include "ota_backend_arduino_libretiny.h" #elif defined(USE_HOST) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 44a1ebf36e..6cb9d5f03a 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,6 +1,7 @@ from collections import UserDict from collections.abc import Callable from functools import reduce +import logging from pathlib import Path from typing import Any @@ -35,6 +36,8 @@ from esphome.const import ( ) from esphome.core import EsphomeError +_LOGGER = logging.getLogger(__name__) + DOMAIN = CONF_PACKAGES # Guard against infinite include chains (e.g. A includes B includes A). MAX_INCLUDE_DEPTH = 20 @@ -267,8 +270,23 @@ def _process_remote_package(config: dict[str, Any]) -> dict[str, Any]: # If loading fails, the cached checkout may be stale — revert and retry once. try: return {CONF_PACKAGES: get_packages(files)} - except cv.Invalid: - revert() + except cv.Invalid as err: + if not revert(): + # The pre-update content is out of reach (lock timeout, the + # checkout moved, or the reset failed; see the log), so a + # retry could not see it. + raise cv.Invalid( + f"Failed to load packages and could not revert the cached " + f"checkout to retry. {err}", + path=err.path, + ) from err + # If the retry succeeds this is the only trace that the + # refreshed upstream content was broken. + _LOGGER.warning( + "Loading packages failed (%s), reverted the cached checkout " + "and retrying", + err, + ) try: return {CONF_PACKAGES: get_packages(files)} except cv.Invalid as err: diff --git a/esphome/components/pcm5122/audio_dac.py b/esphome/components/pcm5122/audio_dac.py index 0017a1ef5a..c18fb3993e 100644 --- a/esphome/components/pcm5122/audio_dac.py +++ b/esphome/components/pcm5122/audio_dac.py @@ -5,6 +5,7 @@ from esphome.components.audio_dac import AudioDac import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, + CONF_ENABLE_PIN, CONF_ID, CONF_INPUT, CONF_INVERTED, @@ -16,6 +17,11 @@ from esphome.const import ( CODEOWNERS = ["@remcom"] DEPENDENCIES = ["i2c"] +CONF_ANALOG_GAIN = "analog_gain" +CONF_CHANNEL_MIX = "channel_mix" +CONF_VOLUME_MIN_DB = "volume_min_db" +CONF_VOLUME_MAX_DB = "volume_max_db" + pcm5122_ns = cg.esphome_ns.namespace("pcm5122") PCM5122 = pcm5122_ns.class_("PCM5122", AudioDac, cg.Component, i2c.I2CDevice) CONF_PCM5122 = "pcm5122" @@ -27,26 +33,60 @@ PCM5122_BITS_PER_SAMPLE_ENUM = { 32: pcm5122_bits_per_sample.PCM5122_BITS_PER_SAMPLE_32, } +pcm5122_analog_gain = pcm5122_ns.enum("PCM5122AnalogGain") +PCM5122_ANALOG_GAIN_ENUM = { + "0db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_0DB, + "-6db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_MINUS_6DB, +} + +pcm5122_channel_mix = pcm5122_ns.enum("PCM5122ChannelMix") +PCM5122_CHANNEL_MIX_ENUM = { + "stereo": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_STEREO, + "left": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_LEFT_ONLY, + "right": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_RIGHT_ONLY, + "swapped": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_SWAPPED, +} + _validate_bits = cv.float_with_unit("bits", "bit") +def _validate_volume_range(config): + if config[CONF_VOLUME_MIN_DB] >= config[CONF_VOLUME_MAX_DB]: + raise cv.Invalid(f"{CONF_VOLUME_MIN_DB} must be less than {CONF_VOLUME_MAX_DB}") + return config + + PCM5122GPIOPin = pcm5122_ns.class_( "PCM5122GPIOPin", cg.GPIOPin, cg.Parented.template(PCM5122), ) -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(PCM5122), cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( _validate_bits, cv.enum(PCM5122_BITS_PER_SAMPLE_ENUM) ), + cv.Optional(CONF_ANALOG_GAIN, default="0db"): cv.enum( + PCM5122_ANALOG_GAIN_ENUM, lower=True + ), + cv.Optional(CONF_CHANNEL_MIX, default="stereo"): cv.enum( + PCM5122_CHANNEL_MIX_ENUM, lower=True + ), + cv.Optional(CONF_VOLUME_MIN_DB, default="-52.5dB"): cv.All( + cv.decibel, cv.float_range(min=-103.0, max=24.0) + ), + cv.Optional(CONF_VOLUME_MAX_DB, default="0dB"): cv.All( + cv.decibel, cv.float_range(min=-103.0, max=24.0) + ), + cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) - .extend(i2c.i2c_device_schema(0x4D)) + .extend(i2c.i2c_device_schema(0x4D)), + _validate_volume_range, ) @@ -96,3 +136,10 @@ async def to_code(config): await i2c.register_i2c_device(var, config) cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) + cg.add(var.set_analog_gain(config[CONF_ANALOG_GAIN])) + cg.add(var.set_channel_mix(config[CONF_CHANNEL_MIX])) + cg.add(var.set_volume_min_db(config[CONF_VOLUME_MIN_DB])) + cg.add(var.set_volume_max_db(config[CONF_VOLUME_MAX_DB])) + if enable_pin_config := config.get(CONF_ENABLE_PIN): + enable_pin = await cg.gpio_pin_expression(enable_pin_config) + cg.add(var.set_enable_pin(enable_pin)) diff --git a/esphome/components/pcm5122/pcm5122.cpp b/esphome/components/pcm5122/pcm5122.cpp index 68bbd50e4f..d178cb83b8 100644 --- a/esphome/components/pcm5122/pcm5122.cpp +++ b/esphome/components/pcm5122/pcm5122.cpp @@ -10,6 +10,12 @@ namespace esphome::pcm5122 { static const char *const TAG = "pcm5122"; void PCM5122::setup() { + // Hold XSMT low (soft mute asserted) until init completes + if (this->enable_pin_ != nullptr) { + this->enable_pin_->setup(); + this->enable_pin_->digital_write(false); + } + // Select page 0 and verify chip presence via I2C ACK if (!this->select_page_(0)) { ESP_LOGE(TAG, "Write failed"); @@ -51,7 +57,22 @@ void PCM5122::setup() { } this->reg(PCM5122_REG_AUDIO_FORMAT) = PCM5122_AUDIO_FORMAT_I2S | alen; + if (!this->write_channel_mix_()) { + this->mark_failed(); + return; + } + + if (!this->write_analog_gain_()) { + this->mark_failed(); + return; + } + // PLL reference clock: BCK + if (!this->select_page_(0)) { + ESP_LOGE(TAG, "Write failed"); + this->mark_failed(); + return; + } optional pll_ref = this->read_byte(PCM5122_REG_PLL_REF); if (!pll_ref.has_value()) { ESP_LOGE(TAG, "Failed to read PLL_REF"); @@ -67,15 +88,40 @@ void PCM5122::setup() { this->mark_failed(); return; } + + // Release XSMT (soft un-mute) now that init has completed + if (this->enable_pin_ != nullptr) { + this->enable_pin_->digital_write(true); + } } void PCM5122::dump_config() { + const char *channel_mix_str; + switch (this->channel_mix_) { + case PCM5122_CHANNEL_MIX_LEFT_ONLY: + channel_mix_str = "left only"; + break; + case PCM5122_CHANNEL_MIX_RIGHT_ONLY: + channel_mix_str = "right only"; + break; + case PCM5122_CHANNEL_MIX_SWAPPED: + channel_mix_str = "swapped"; + break; + default: + channel_mix_str = "stereo"; + break; + } ESP_LOGCONFIG(TAG, "Audio DAC:"); LOG_I2C_DEVICE(this); ESP_LOGCONFIG(TAG, " Bits per sample: %u\n" + " Analog gain: %s\n" + " Channel mix: %s\n" + " Volume range: %.1f dB to %.1f dB\n" " Muted: %s", - this->bits_per_sample_, YESNO(this->is_muted_)); + this->bits_per_sample_, this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? "0 dB" : "-6 dB", + channel_mix_str, this->volume_min_db_, this->volume_max_db_, YESNO(this->is_muted_)); + LOG_PIN(" Enable Pin: ", this->enable_pin_); } bool PCM5122::set_mute_off() { @@ -118,11 +164,11 @@ bool PCM5122::write_mute_() { } bool PCM5122::write_volume_() { - // DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFF = mute (-0.5 dB/step). - // Note: volume=0.0 maps to -52.5 dB (still audible), not true silence. + // DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFE = -103 dB, 0xFF = mute (-0.5 dB/step). + // Note: volume=0.0 maps to volume_min_db_, which is not true silence unless set to -103 dB. // Use set_mute_on() for silence. - const uint8_t dvol_max_volume = 0x30; // 0 dB at full scale - const uint8_t dvol_min_volume = 0x99; // -52.5 dB at minimum + const uint8_t dvol_max_volume = static_cast(lroundf(0x30 - this->volume_max_db_ * 2.0f)); + const uint8_t dvol_min_volume = static_cast(lroundf(0x30 - this->volume_min_db_ * 2.0f)); const uint8_t volume_byte = dvol_max_volume + static_cast(lroundf((1.0f - this->volume_) * (dvol_min_volume - dvol_max_volume))); @@ -137,4 +183,52 @@ bool PCM5122::write_volume_() { return true; } +bool PCM5122::write_analog_gain_() { + uint8_t gain_byte = this->analog_gain_; + if (!this->select_page_(1) || !this->write_byte(PCM5122_REG_ANALOG_GAIN, gain_byte)) { + ESP_LOGE(TAG, "Writing analog gain failed"); + return false; + } + return true; +} + +bool PCM5122::write_channel_mix_() { + uint8_t channel_mix_byte = this->channel_mix_; + if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_DAC_DATA_PATH, channel_mix_byte)) { + ESP_LOGE(TAG, "Writing channel mix failed"); + return false; + } + return true; +} + +bool PCM5122::set_standby(bool enable) { + bool prev_standby = this->standby_; + this->standby_ = enable; + if (!this->write_power_control_()) { + this->standby_ = prev_standby; + return false; + } + return true; +} + +bool PCM5122::set_powerdown(bool enable) { + bool prev_powerdown = this->powerdown_; + this->powerdown_ = enable; + if (!this->write_power_control_()) { + this->powerdown_ = prev_powerdown; + return false; + } + return true; +} + +bool PCM5122::write_power_control_() { + uint8_t power_byte = + (this->standby_ ? PCM5122_POWER_CONTROL_RQST : 0) | (this->powerdown_ ? PCM5122_POWER_CONTROL_RQPD : 0); + if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_POWER_CONTROL, power_byte)) { + ESP_LOGE(TAG, "Writing power control failed"); + return false; + } + return true; +} + } // namespace esphome::pcm5122 diff --git a/esphome/components/pcm5122/pcm5122.h b/esphome/components/pcm5122/pcm5122.h index 3c42e4d8d2..199818b06e 100644 --- a/esphome/components/pcm5122/pcm5122.h +++ b/esphome/components/pcm5122/pcm5122.h @@ -3,6 +3,7 @@ #include "esphome/components/audio_dac/audio_dac.h" #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" +#include "esphome/core/gpio.h" #include "esphome/core/hal.h" namespace esphome::pcm5122 { @@ -10,11 +11,13 @@ namespace esphome::pcm5122 { // Page 0 register addresses static const uint8_t PCM5122_REG_PAGE_SELECT = 0x00; static const uint8_t PCM5122_REG_RESET = 0x01; +static const uint8_t PCM5122_REG_POWER_CONTROL = 0x02; static const uint8_t PCM5122_REG_MUTE = 0x03; static const uint8_t PCM5122_REG_GPIO_ENABLE = 0x08; static const uint8_t PCM5122_REG_PLL_REF = 0x0D; static const uint8_t PCM5122_REG_ERROR_DETECT = 0x25; static const uint8_t PCM5122_REG_AUDIO_FORMAT = 0x28; +static const uint8_t PCM5122_REG_DAC_DATA_PATH = 0x2A; static const uint8_t PCM5122_REG_DVOL_LEFT = 0x3D; static const uint8_t PCM5122_REG_DVOL_RIGHT = 0x3E; static const uint8_t PCM5122_REG_GPIO_OUTPUT_SELECT = 0x50; // Base address; GPIO n uses offset n-1 @@ -23,6 +26,9 @@ static const uint8_t PCM5122_REG_GPIO_OUTPUT = 0x56; static const uint8_t PCM5122_REG_GPIO_INVERT = 0x57; static const uint8_t PCM5122_REG_GPIO_INPUT = 0x77; +// Page 1 register addresses +static const uint8_t PCM5122_REG_ANALOG_GAIN = 0x02; + // Register values for init sequence static const uint8_t PCM5122_RESET_MODULES = 0x10; // RSTM: reset audio modules static const uint8_t PCM5122_AUDIO_FORMAT_I2S = 0x00; // AFMT = I2S (bits [5:4] = 00) @@ -35,12 +41,33 @@ static const uint8_t PCM5122_ERROR_DETECT_DISABLE_DIV_AUTOSET = (1 << 1); static const uint8_t PCM5122_PLL_REF_MASK = (7 << 4); // SREF bits [6:4] static const uint8_t PCM5122_PLL_REF_SOURCE_BCK = (1 << 4); // SREF = 001 (BCK) +// Page 0, Register 2 (Power Control): RQST = standby request, RQPD = powerdown request (§10.5.3) +static const uint8_t PCM5122_POWER_CONTROL_RQST = (1 << 4); +static const uint8_t PCM5122_POWER_CONTROL_RQPD = (1 << 0); + +// Page 1, Register 2 (Analog Gain Control): LAGN/RAGN select 0 dB or -6 dB analog gain (§8.3.5.5) +static const uint8_t PCM5122_ANALOG_GAIN_LAGN = (1 << 4); +static const uint8_t PCM5122_ANALOG_GAIN_RAGN = (1 << 0); + enum PCM5122BitsPerSample : uint8_t { PCM5122_BITS_PER_SAMPLE_16 = 16, PCM5122_BITS_PER_SAMPLE_24 = 24, PCM5122_BITS_PER_SAMPLE_32 = 32, }; +enum PCM5122AnalogGain : uint8_t { + PCM5122_ANALOG_GAIN_0DB = 0x00, + PCM5122_ANALOG_GAIN_MINUS_6DB = PCM5122_ANALOG_GAIN_LAGN | PCM5122_ANALOG_GAIN_RAGN, +}; + +// Page 0, Register 0x2A (DAC Data Path): AUPL/AUPR select which channel's data feeds each output (§7.4.2.42) +enum PCM5122ChannelMix : uint8_t { + PCM5122_CHANNEL_MIX_STEREO = 0x11, // Left data -> left out, right data -> right out + PCM5122_CHANNEL_MIX_LEFT_ONLY = 0x12, // Left data -> both outputs + PCM5122_CHANNEL_MIX_RIGHT_ONLY = 0x21, // Right data -> both outputs + PCM5122_CHANNEL_MIX_SWAPPED = 0x22, // Left/right outputs swapped +}; + class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { public: void setup() override; @@ -48,6 +75,11 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c:: float get_setup_priority() const override { return setup_priority::IO; } void set_bits_per_sample(PCM5122BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } + void set_analog_gain(PCM5122AnalogGain analog_gain) { this->analog_gain_ = analog_gain; } + void set_channel_mix(PCM5122ChannelMix channel_mix) { this->channel_mix_ = channel_mix; } + void set_volume_min_db(float volume_min_db) { this->volume_min_db_ = volume_min_db; } + void set_volume_max_db(float volume_max_db) { this->volume_max_db_ = volume_max_db; } + void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; } bool set_mute_off() override; bool set_mute_on() override; @@ -56,17 +88,30 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c:: bool is_muted() override; float volume() override; + bool set_standby(bool enable); + bool set_powerdown(bool enable); + friend class PCM5122GPIOPin; protected: bool select_page_(uint8_t page); bool write_mute_(); bool write_volume_(); + bool write_analog_gain_(); + bool write_channel_mix_(); + bool write_power_control_(); - float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB) - int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes + GPIOPin *enable_pin_{nullptr}; + float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB) + float volume_min_db_{-52.5f}; // Matches the previous hardcoded minimum (0x99) + float volume_max_db_{0.0f}; // Matches the previous hardcoded maximum (0x30) + int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes bool is_muted_{false}; + bool standby_{false}; + bool powerdown_{false}; PCM5122BitsPerSample bits_per_sample_{PCM5122_BITS_PER_SAMPLE_16}; + PCM5122AnalogGain analog_gain_{PCM5122_ANALOG_GAIN_0DB}; + PCM5122ChannelMix channel_mix_{PCM5122_CHANNEL_MIX_STEREO}; }; } // namespace esphome::pcm5122 diff --git a/esphome/components/pcm5122/switch/__init__.py b/esphome/components/pcm5122/switch/__init__.py new file mode 100644 index 0000000000..10519da895 --- /dev/null +++ b/esphome/components/pcm5122/switch/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import CONF_POWER_MODE, ENTITY_CATEGORY_CONFIG + +from ..audio_dac import CONF_PCM5122, PCM5122, pcm5122_ns + +PCM5122PowerSwitch = pcm5122_ns.class_("PCM5122PowerSwitch", switch.Switch) + +pcm5122_power_switch_mode = pcm5122_ns.enum("PCM5122PowerSwitchMode") +PCM5122_POWER_SWITCH_MODE_ENUM = { + "standby": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_STANDBY, + "powerdown": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_POWERDOWN, +} + +CONFIG_SCHEMA = switch.switch_schema( + PCM5122PowerSwitch, + entity_category=ENTITY_CATEGORY_CONFIG, +).extend( + { + cv.GenerateID(CONF_PCM5122): cv.use_id(PCM5122), + cv.Optional(CONF_POWER_MODE, default="powerdown"): cv.enum( + PCM5122_POWER_SWITCH_MODE_ENUM, lower=True + ), + } +) + + +async def to_code(config): + var = await switch.new_switch(config) + await cg.register_parented(var, config[CONF_PCM5122]) + cg.add(var.set_power_mode(config[CONF_POWER_MODE])) diff --git a/esphome/components/pcm5122/switch/power_switch.cpp b/esphome/components/pcm5122/switch/power_switch.cpp new file mode 100644 index 0000000000..45f0be715d --- /dev/null +++ b/esphome/components/pcm5122/switch/power_switch.cpp @@ -0,0 +1,12 @@ +#include "power_switch.h" + +namespace esphome::pcm5122 { + +void PCM5122PowerSwitch::write_state(bool state) { + bool ok = (this->mode_ == PCM5122_POWER_SWITCH_MODE_STANDBY) ? this->parent_->set_standby(state) + : this->parent_->set_powerdown(state); + if (ok) + this->publish_state(state); +} + +} // namespace esphome::pcm5122 diff --git a/esphome/components/pcm5122/switch/power_switch.h b/esphome/components/pcm5122/switch/power_switch.h new file mode 100644 index 0000000000..47d30f1a9f --- /dev/null +++ b/esphome/components/pcm5122/switch/power_switch.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/switch/switch.h" + +#include "../pcm5122.h" + +namespace esphome::pcm5122 { + +enum PCM5122PowerSwitchMode : uint8_t { + PCM5122_POWER_SWITCH_MODE_STANDBY, + PCM5122_POWER_SWITCH_MODE_POWERDOWN, +}; + +class PCM5122PowerSwitch final : public switch_::Switch, public Parented { + public: + void set_power_mode(PCM5122PowerSwitchMode mode) { this->mode_ = mode; } + + protected: + void write_state(bool state) override; + + PCM5122PowerSwitchMode mode_{PCM5122_POWER_SWITCH_MODE_POWERDOWN}; +}; + +} // namespace esphome::pcm5122 diff --git a/esphome/components/preferences/__init__.py b/esphome/components/preferences/__init__.py index c426872728..f3f2f632c9 100644 --- a/esphome/components/preferences/__init__.py +++ b/esphome/components/preferences/__init__.py @@ -1,3 +1,4 @@ +from esphome import preferences import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID @@ -10,10 +11,17 @@ preferences_ns = cg.esphome_ns.namespace("preferences") IntervalSyncer = preferences_ns.class_("IntervalSyncer", cg.Component) CONF_FLASH_WRITE_INTERVAL = "flash_write_interval" +CONF_RTC_STORAGE = "rtc_storage" CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(IntervalSyncer), cv.Optional(CONF_FLASH_WRITE_INTERVAL, default="60s"): cv.update_interval, + # Compile the RTC-backed storage into the ESP32 preferences backend even + # when no other option selects it, so components (including external + # ones) requesting in_flash=false are honoured instead of falling back + # to NVS. No default: absence means "no request" (see + # preferences.validate_rtc_storage for the per-platform rules). + cv.Optional(CONF_RTC_STORAGE): preferences.validate_rtc_storage, } ).extend(cv.COMPONENT_SCHEMA) @@ -26,4 +34,6 @@ async def to_code(config): cg.add_define("USE_PREFERENCES_SYNC_EVERY_LOOP") else: cg.add(var.set_write_interval(write_interval)) + if config.get(CONF_RTC_STORAGE): + preferences.request_rtc_storage() await cg.register_component(var, config) diff --git a/esphome/components/provisioning/__init__.py b/esphome/components/provisioning/__init__.py new file mode 100644 index 0000000000..36fa69357a --- /dev/null +++ b/esphome/components/provisioning/__init__.py @@ -0,0 +1,104 @@ +from dataclasses import dataclass, field +import logging + +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_ON_TIMEOUT, CONF_TIMEOUT +from esphome.core import CORE +from esphome.types import ConfigType + +CODEOWNERS = ["@esphome/core"] +DOMAIN = "provisioning" + +_LOGGER = logging.getLogger(__name__) + +provisioning_ns = cg.esphome_ns.namespace("provisioning") +ProvisioningManager = provisioning_ns.class_("ProvisioningManager", cg.Component) + + +@dataclass +class ProvisioningData: + # Names of the components that registered as a provisioning source this run. + sources: set[str] = field(default_factory=set) + # Names of source components that have their credentials set in the config. + hardcoded_credentials: set[str] = field(default_factory=set) + + +def _get_data() -> ProvisioningData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = ProvisioningData() + return CORE.data[DOMAIN] + + +def register_source(name: str) -> None: + """Record that ``name`` is a provisioning source for this configuration. + + A provisioning-capable component (a transport that boots unprovisioned and is + set up by the controller on first connection, or a network interface that + provisions once connected) calls this while its own config is being processed, + typically from a schema validator. `provisioning:` then confirms at least one + source is present without inspecting the full config or knowing about any + specific component. State lives in CORE.data, which is cleared between runs. + """ + _get_data().sources.add(name) + + +def report_hardcoded_credentials(name: str) -> None: + """Record that source component ``name`` has its credentials set in the config. + + A source component calls this from its own validator when it finds baked-in + credentials (a WiFi SSID/password, an API encryption key, ...). `provisioning:` + warns about these, since a device that ships with credentials does not need a + provisioning window. The warning is emitted here, by `provisioning:`, so the + source components stay unaware of it. + """ + _get_data().hardcoded_credentials.add(name) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ProvisioningManager), + cv.Required(CONF_TIMEOUT): cv.All( + cv.positive_not_null_time_period, cv.positive_time_period_milliseconds + ), + cv.Optional(CONF_ON_TIMEOUT): automation.validate_automation(single=True), + } +).extend(cv.COMPONENT_SCHEMA) + + +def _final_validate(config: ConfigType) -> ConfigType: + """Validate the provisioning setup once every component has been processed. + + Sources register during their own config validation, so by final validation + both the source set and the hardcoded-credentials set are complete. + """ + data = _get_data() + if not data.sources: + raise cv.Invalid( + "'provisioning' requires at least one provisioning-capable component: " + "configure a network interface such as 'wifi:' or 'ethernet:', or enable " + "'api:' with 'encryption:' and no 'key:' so the device boots " + "unprovisioned and is configured on first connection." + ) + if data.hardcoded_credentials: + _LOGGER.warning( + "'provisioning' is configured, but credentials are set in the " + "configuration for: %s. A device that uses a provisioning window should " + "ship without credentials so they are set on first connection; " + "hardcoding them makes the window pointless.", + ", ".join(sorted(data.hardcoded_credentials)), + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config: ConfigType) -> None: + cg.add_define("USE_PROVISIONING") + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_timeout(config[CONF_TIMEOUT])) + if on_timeout := config.get(CONF_ON_TIMEOUT): + await automation.build_automation(var.get_timeout_trigger(), [], on_timeout) diff --git a/esphome/components/provisioning/provisioning.cpp b/esphome/components/provisioning/provisioning.cpp new file mode 100644 index 0000000000..02c089bfed --- /dev/null +++ b/esphome/components/provisioning/provisioning.cpp @@ -0,0 +1,92 @@ +#include "esphome/components/provisioning/provisioning.h" +#ifdef USE_PROVISIONING +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#ifdef USE_NETWORK +#include "esphome/components/network/util.h" +#endif + +#include + +namespace esphome::provisioning { + +static const char *const TAG = "provisioning"; + +ProvisioningManager *global_provisioning_manager = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + nullptr; + +ProvisioningManager::ProvisioningManager() { + global_provisioning_manager = this; +#ifdef USE_NETWORK + // Network connectivity is a built-in provisioning source. Registered here rather + // than from a source's setup() because connectivity is universal, not a pluggable + // transport; loop() latches it provisioned once the device has connected. + this->network_source_ = this->register_source(); +#endif +} + +uint8_t ProvisioningManager::register_source() { + if (this->source_count_ >= MAX_SOURCES) { + // Defensive: only a handful of sources exist in practice. Fail loudly rather + // than shifting past the mask width (undefined behavior). The returned index is + // ignored by set_source_provisioned()'s bounds check. + ESP_LOGE(TAG, "Too many provisioning sources (max %u)", MAX_SOURCES); + return this->source_count_; + } + uint8_t source = this->source_count_++; + this->registered_mask_ |= (1UL << source); + return source; +} + +void ProvisioningManager::loop() { + // Sources register during their own setup() (at various priorities), and this + // loop() also runs while waiting on a slow component during setup. Evaluating the + // provisioning state before every source has registered could conclude + // "provisioned" prematurely and disable_loop() for good, defeating the window -- + // so do nothing until all setup() calls are done. + if (!App.is_setup_complete()) + return; + +#ifdef USE_NETWORK + // Latch the built-in connectivity source once the device has been reachable via + // any interface. network::is_connected() aggregates wifi/ethernet/modem/... (OR + // across interfaces), and a disabled interface never connects so it never + // contributes. Latched: a later link drop does not un-provision -- the RAM-only + // window still reopens only on reboot. + if ((this->provisioned_mask_ & (1UL << this->network_source_)) == 0 && network::is_connected()) + this->set_source_provisioned(this->network_source_, true); +#endif + + // The window is resolved once the device is provisioned or the window has closed; + // there is nothing left to track, so stop running entirely. Config validation + // guarantees at least one source, so is_provisioned() is never vacuously true here. + if (this->closed_ || this->is_provisioned()) { + this->disable_loop(); + return; + } + // The window timer runs from boot (millis since boot). The closed state is not + // persisted, so a reboot reopens the window. + if (this->timeout_ != 0 && App.get_loop_component_start_time() > this->timeout_) { + this->close_window_(); + } +} + +void ProvisioningManager::close_window_() { + this->closed_ = true; + ESP_LOGW(TAG, "Window expired; cycle power to reopen window"); + // Notify internal consumers first (transports disconnect clients, Improv stops), + // then fire the user-facing automation. + this->closed_callback_.call(); + this->timeout_trigger_.trigger(); +} + +void ProvisioningManager::dump_config() { + ESP_LOGCONFIG(TAG, + "Provisioning:\n" + " Timeout: %" PRIu32 "ms\n" + " Provisioned: %s", + this->timeout_, YESNO(this->is_provisioned())); +} + +} // namespace esphome::provisioning +#endif // USE_PROVISIONING diff --git a/esphome/components/provisioning/provisioning.h b/esphome/components/provisioning/provisioning.h new file mode 100644 index 0000000000..e21b8f3ef0 --- /dev/null +++ b/esphome/components/provisioning/provisioning.h @@ -0,0 +1,96 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_PROVISIONING +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome::provisioning { + +// Central provisioning-window manager (EN18031). A device that ships unprovisioned +// (secure transports enabled with no credentials, configured by the controller on +// first connection) opens a provisioning window at boot. Each transport that needs +// provisioning registers as a "source" and reports its state; the device is +// considered provisioned once every registered source is provisioned. +// +// Network connectivity is a built-in source: a device with a network interface but +// no other provisioning-capable component (no api encryption, etc.) is still +// considered provisioned once it has connected via any interface -- so an +// Improv-only device reports its state correctly. +// +// If the window times out while still unprovisioned it closes: the closed state is +// RAM-only (a power cycle / reset reopens it) and the `on_timeout` automation fires. +// Components query window_pending()/closed() to suppress reboot timeouts and refuse +// further provisioning. This manager owns no transport knowledge; transports +// (api, and later mqtt/wireguard/...) drive it through the source API. +class ProvisioningManager : public Component { + public: + // Maximum number of provisioning sources, limited by the width of the state masks. + static constexpr uint8_t MAX_SOURCES = 32; + + ProvisioningManager(); + + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } + + void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } + + // Register a provisioning source. Returns a bit index the source uses to report + // its state via set_source_provisioned(). Call once, from the source's setup(). + uint8_t register_source(); + // Report whether the given source currently holds valid credentials. + void set_source_provisioned(uint8_t source, bool provisioned) { + if (source >= MAX_SOURCES) + return; + if (provisioned) { + this->provisioned_mask_ |= (1UL << source); + } else { + this->provisioned_mask_ &= ~(1UL << source); + } + } + + // True once every registered source is provisioned. Config validation guarantees + // at least one source, and the built-in connectivity source registers in the + // constructor, so registered_mask_ is never zero in practice. + bool is_provisioned() const { return (this->provisioned_mask_ & this->registered_mask_) == this->registered_mask_; } + // True while provisioning is still pending: the device is unprovisioned, whether + // the window is still open or has already closed. Reboot timeouts are suppressed + // while this holds so the device never auto-reboots (and silently reopens the + // window) while unprovisioned. + bool window_pending() const { return !this->is_provisioned(); } + // True once the window has expired without the device being provisioned. + bool closed() const { return this->closed_; } + + // Register a callback fired once when the window closes (runtime expiry). Used + // internally by transports/Improv to stop accepting provisioning. The user-facing + // on_timeout automation is wired to get_timeout_trigger() instead. + template void add_on_closed_callback(F &&callback) { + this->closed_callback_.add(std::forward(callback)); + } + Trigger<> *get_timeout_trigger() { return &this->timeout_trigger_; } + + protected: + void close_window_(); + + Trigger<> timeout_trigger_; + LazyCallbackManager closed_callback_; + uint32_t timeout_{0}; + uint32_t registered_mask_{0}; + uint32_t provisioned_mask_{0}; + uint8_t source_count_{0}; + bool closed_{false}; +#ifdef USE_NETWORK + // Built-in connectivity source (see loop()): registered in the constructor and + // latched provisioned once the device has connected via any network interface. + uint8_t network_source_{0}; +#endif +}; + +extern ProvisioningManager *global_provisioning_manager; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::provisioning +#endif // USE_PROVISIONING diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index d36e5d0250..5651e07af0 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -5,11 +5,11 @@ namespace esphome::pzemac { static const char *const TAG = "pzemac"; -static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42; static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers -void PZEMAC::on_modbus_data(const std::vector &data) { +void PZEMAC::on_response(std::span request_pdu, std::span response_pdu) { + auto data = modbus::helpers::server_pdu_payload(response_pdu); if (data.size() < 20) { ESP_LOGW(TAG, "Invalid size for PZEM AC!"); return; @@ -61,7 +61,7 @@ void PZEMAC::on_modbus_data(const std::vector &data) { this->power_factor_sensor_->publish_state(power_factor); } -void PZEMAC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, PZEM_REGISTER_COUNT); } +void PZEMAC::update() { this->read_input_registers(0, PZEM_REGISTER_COUNT); } void PZEMAC::dump_config() { ESP_LOGCONFIG(TAG, "PZEMAC:\n" @@ -76,10 +76,8 @@ void PZEMAC::dump_config() { } void PZEMAC::reset_energy_() { - std::vector cmd; - cmd.push_back(this->address_); - cmd.push_back(PZEM_CMD_RESET_ENERGY); - this->send_raw(cmd); + const uint8_t pdu[] = {PZEM_CMD_RESET_ENERGY}; + this->send_pdu(pdu); } } // namespace esphome::pzemac diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index a3ad7e1167..171212d3ee 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -5,7 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" -#include +#include namespace esphome::pzemac { @@ -22,7 +22,7 @@ class PZEMAC final : public PollingComponent, public modbus::ModbusClientDevice void update() override; - void on_modbus_data(const std::vector &data) override; + void on_response(std::span request_pdu, std::span response_pdu) override; void dump_config() override; diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp index 6ded9b3a34..5e505cde0c 100644 --- a/esphome/components/pzemdc/pzemdc.cpp +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -5,11 +5,11 @@ namespace esphome::pzemdc { static const char *const TAG = "pzemdc"; -static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42; static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers -void PZEMDC::on_modbus_data(const std::vector &data) { +void PZEMDC::on_response(std::span request_pdu, std::span response_pdu) { + auto data = modbus::helpers::server_pdu_payload(response_pdu); if (data.size() < 16) { ESP_LOGW(TAG, "Invalid size for PZEM DC!"); return; @@ -51,7 +51,7 @@ void PZEMDC::on_modbus_data(const std::vector &data) { this->energy_sensor_->publish_state(energy); } -void PZEMDC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, 8); } +void PZEMDC::update() { this->read_input_registers(0, 8); } void PZEMDC::dump_config() { ESP_LOGCONFIG(TAG, "PZEMDC:\n" @@ -64,10 +64,8 @@ void PZEMDC::dump_config() { } void PZEMDC::reset_energy() { - std::vector cmd; - cmd.push_back(this->address_); - cmd.push_back(PZEM_CMD_RESET_ENERGY); - this->send_raw(cmd); + const uint8_t pdu[] = {PZEM_CMD_RESET_ENERGY}; + this->send_pdu(pdu); } } // namespace esphome::pzemdc diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index 7d14a5ed4b..b7657608e6 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -5,7 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" -#include +#include namespace esphome::pzemdc { @@ -18,7 +18,7 @@ class PZEMDC final : public PollingComponent, public modbus::ModbusClientDevice void update() override; - void on_modbus_data(const std::vector &data) override; + void on_response(std::span request_pdu, std::span response_pdu) override; void dump_config() override; diff --git a/esphome/components/rc522_i2c/__init__.py b/esphome/components/rc522_i2c/__init__.py index 7c42a12429..c67615e2d8 100644 --- a/esphome/components/rc522_i2c/__init__.py +++ b/esphome/components/rc522_i2c/__init__.py @@ -16,7 +16,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(RC522I2C), } - ).extend(i2c.i2c_device_schema(0x2C)) + ).extend(i2c.i2c_device_schema(0x28)) ) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index cbf82e6f44..19b8549f75 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -2012,7 +2012,14 @@ HaierData, HaierBinarySensor, HaierTrigger, HaierAction, HaierDumper = declare_p HaierAction = ns.class_("HaierAction", RemoteTransmitterActionBase) HAIER_SCHEMA = cv.Schema( { - cv.Required(CONF_CODE): cv.All([cv.hex_uint8_t], cv.Length(min=13, max=13)), + cv.Required(CONF_CODE): cv.All( + [cv.hex_uint8_t], + cv.Any( + cv.Length(min=8, max=8), + cv.Length(min=13, max=13), + msg="must be a list of length 8 or 13", + ), + ), } ) diff --git a/esphome/components/remote_base/haier_protocol.cpp b/esphome/components/remote_base/haier_protocol.cpp index fa4cec773f..8801f1049a 100644 --- a/esphome/components/remote_base/haier_protocol.cpp +++ b/esphome/components/remote_base/haier_protocol.cpp @@ -11,9 +11,12 @@ constexpr uint32_t HEADER_HIGH_US = 4400; constexpr uint32_t BIT_MARK_US = 540; constexpr uint32_t BIT_ONE_SPACE_US = 1650; constexpr uint32_t BIT_ZERO_SPACE_US = 580; -constexpr unsigned int HAIER_IR_PACKET_BIT_SIZE = 112; +// 8 bytes + checksum +constexpr unsigned int HAIER_IR_PACKET_BIT_SIZE_SHORT = 72; +// 13 bytes + checksum +constexpr unsigned int HAIER_IR_PACKET_BIT_SIZE_LONG = 112; // Max data bytes in packet (excluding checksum) -constexpr size_t HAIER_MAX_DATA_BYTES = (HAIER_IR_PACKET_BIT_SIZE / 8); +constexpr size_t HAIER_MAX_DATA_BYTES = HAIER_IR_PACKET_BIT_SIZE_LONG / 8 - 1; void HaierProtocol::encode_byte_(RemoteTransmitData *dst, uint8_t item) { for (uint8_t mask = 1 << 7; mask != 0; mask >>= 1) { @@ -28,7 +31,7 @@ void HaierProtocol::encode_byte_(RemoteTransmitData *dst, uint8_t item) { void HaierProtocol::encode(RemoteTransmitData *dst, const HaierData &data) { dst->set_carrier_frequency(38000); - dst->reserve(5 + ((data.data.size() + 1) * 2)); + dst->reserve(5 + ((data.data.size() + 1) * 16)); dst->mark(HEADER_LOW_US); dst->space(HEADER_LOW_US); dst->mark(HEADER_LOW_US); @@ -50,11 +53,16 @@ optional HaierProtocol::decode(RemoteReceiveData src) { return {}; } size_t size = src.size() - src.get_index() - 1; - if (size < HAIER_IR_PACKET_BIT_SIZE * 2) + if (size >= HAIER_IR_PACKET_BIT_SIZE_LONG * 2) { + size = HAIER_IR_PACKET_BIT_SIZE_LONG * 2; + } else if (size >= HAIER_IR_PACKET_BIT_SIZE_SHORT * 2) { + size = HAIER_IR_PACKET_BIT_SIZE_SHORT * 2; + } else { return {}; - size = HAIER_IR_PACKET_BIT_SIZE * 2; + } uint8_t checksum = 0; HaierData out; + out.data.reserve(size / 16 - 1); while (size > 0) { uint8_t data = 0; for (uint8_t mask = 0x80; mask != 0; mask >>= 1) { diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 53a0f8fb77..ad9c4b5a18 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -118,7 +118,7 @@ CONFIG_SCHEMA = remote_base.validate_triggers( bk72xx="1000b", ln882x="1000b", rtl87xx="1000b", - rp2040="1000b", + rp2="1000b", ): cv.validate_bytes, cv.Optional(CONF_FILTER, default="50us"): cv.All( cv.positive_time_period_microseconds, @@ -248,7 +248,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, - PlatformFramework.RP2040_ARDUINO, + PlatformFramework.RP2_ARDUINO, }, } ) diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index 222dae8f7f..36152d8854 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -3,7 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) namespace esphome::remote_receiver { diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 2ed6a4c251..f9ec054fe3 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -14,7 +14,7 @@ namespace esphome::remote_receiver { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -93,11 +93,11 @@ class RemoteReceiverComponent final : public remote_base::RemoteReceiverBase, std::string error_string_; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || defined(USE_ESP32) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_ESP32) RemoteReceiverComponentStore store_; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 1163fc86eb..521c3daf87 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -185,7 +185,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, - PlatformFramework.RP2040_ARDUINO, + PlatformFramework.RP2_ARDUINO, }, } ) diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 51a3c0b1d4..49c711330b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) namespace esphome::remote_transmitter { diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index bcb07038ea..e2d33d13cc 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -64,7 +64,7 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2/__init__.py similarity index 90% rename from esphome/components/rp2040/__init__.py rename to esphome/components/rp2/__init__.py index e76ce6def8..8bba6bc27d 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2/__init__.py @@ -21,7 +21,7 @@ from esphome.const import ( KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, - PLATFORM_RP2040, + PLATFORM_RP2, ThreadModel, ) from esphome.core import ( @@ -33,6 +33,7 @@ from esphome.core import ( ) from esphome.core.config import BOARD_MAX_LENGTH from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed +from esphome.platformio.toolchain import copy_ccache_script from esphome.types import ConfigType from . import boards @@ -40,27 +41,34 @@ from .const import ( KEY_BOARD, KEY_LWIP_OPTS, KEY_PIO_FILES, - KEY_RP2040, + KEY_RP2, KEY_VARIANT, MCU_TO_VARIANT, STANDARD_BOARDS, VARIANT_FRIENDLY, VARIANTS, - rp2040_ns, + rp2_ns, ) # force import gpio to register pin schema -from .gpio import rp2040_pin_to_code # noqa: F401 +from .gpio import rp2_pin_to_code # noqa: F401 _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jesserockz"] AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True +# Legacy top-level YAML keys that route here. The framework +# (esphome/loader.py + esphome/config.py) handles both the deprecation +# warning and the key-rename pass; this declaration is the only place a +# component needs to opt in. See ComponentManifest.aliases for details. +ALIASES = ["rp2040"] +ALIAS_REMOVAL_VERSION = "2027.7.0" + def get_board() -> str: """Return the configured board name.""" - return CORE.data[KEY_RP2040][KEY_BOARD] + return CORE.data[KEY_RP2][KEY_BOARD] def board_has_wifi() -> bool: @@ -90,22 +98,22 @@ def board_id_has_wifi(board_id: str) -> bool: def set_core_data(config: ConfigType) -> ConfigType: - CORE.data[KEY_RP2040] = {} - CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_RP2040 + CORE.data[KEY_RP2] = {} + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_RP2 CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) - CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD] - CORE.data[KEY_RP2040][KEY_VARIANT] = config[CONF_VARIANT] + CORE.data[KEY_RP2][KEY_BOARD] = config[CONF_BOARD] + CORE.data[KEY_RP2][KEY_VARIANT] = config[CONF_VARIANT] - CORE.data[KEY_RP2040][KEY_PIO_FILES] = {} + CORE.data[KEY_RP2][KEY_PIO_FILES] = {} return config def get_rp2040_variant(core_obj: EsphomeCore | None = None) -> str: - return (core_obj or CORE).data[KEY_RP2040][KEY_VARIANT] + return (core_obj or CORE).data[KEY_RP2][KEY_VARIANT] def only_on_variant( @@ -121,7 +129,7 @@ def only_on_variant( unsupported = [unsupported] def validator_(obj: Any) -> Any: - if not CORE.is_rp2040: + if not CORE.is_rp2: raise cv.Invalid(f"{msg_prefix} is only available on RP2040") variant = get_rp2040_variant() if supported is not None and variant not in supported: @@ -237,8 +245,12 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional( + CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY + ): cv.string_strict, + cv.Optional( + CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY + ): _parse_platform_version, } ), _arduino_check_versions, @@ -306,13 +318,18 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config): - cg.add(rp2040_ns.setup_preferences()) + cg.add(rp2_ns.setup_preferences()) # Allow LDF to properly discover dependency including those in preprocessor # conditionals cg.add_platformio_option("lib_ldf_mode", "chain+") cg.add_platformio_option("lib_compat_mode", "strict") cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_build_flag("-DUSE_RP2") + # USE_RP2040 kept defined as a backwards-compat alias for external + # custom components that may still test for it. Internal code uses + # USE_RP2 (the canonical name for the RP2 chip family — covers + # RP2040, RP2350, and any future RP2-series chips). cg.add_build_flag("-DUSE_RP2040") cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") @@ -322,12 +339,13 @@ async def to_code(config): cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant]) cg.add_define(ThreadModel.SINGLE) - cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) + cg.add_platformio_option("extra_scripts", ["pre:ccache.py", "post:post_build.py"]) conf = config[CONF_FRAMEWORK] cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") - cg.add_build_flag("-DUSE_RP2040_FRAMEWORK_ARDUINO") + cg.add_build_flag("-DUSE_RP2_FRAMEWORK_ARDUINO") + cg.add_build_flag("-DUSE_RP2040_FRAMEWORK_ARDUINO") # back-compat alias # cg.add_build_flag("-DPICO_BOARD=pico_w") cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option( @@ -359,8 +377,12 @@ async def to_code(config): cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"), ) - cg.add_define("USE_RP2040_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT]) - cg.add_define("USE_RP2040_CRASH_HANDLER") + cg.add_define("USE_RP2_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT]) + cg.add_define( + "USE_RP2040_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT] + ) # back-compat alias + cg.add_define("USE_RP2_CRASH_HANDLER") + cg.add_define("USE_RP2040_CRASH_HANDLER") # back-compat alias _configure_lwip() @@ -465,7 +487,7 @@ def _configure_lwip() -> None: } # Store for copy_files() to generate the header - CORE.data[KEY_RP2040][KEY_LWIP_OPTS] = lwip_defines + CORE.data[KEY_RP2][KEY_LWIP_OPTS] = lwip_defines # Add a pre-build extra script that injects our lwip_override directory # into CCFLAGS so our lwipopts.h shadows the framework's version. @@ -500,7 +522,7 @@ def _generate_lwipopts_h() -> None: """ from jinja2 import Environment - lwip_defines = CORE.data[KEY_RP2040].get(KEY_LWIP_OPTS) + lwip_defines = CORE.data[KEY_RP2].get(KEY_LWIP_OPTS) if not lwip_defines: return @@ -527,7 +549,7 @@ def add_pio_file(component: str, key: str, data: str): raise EsphomeError( f"[{component}] Invalid PIO key: {key}. Allowed characters: [{ascii_letters}{digits}_]\nPlease report an issue https://github.com/esphome/esphome/issues" ) from e - CORE.data[KEY_RP2040][KEY_PIO_FILES][key] = data + CORE.data[KEY_RP2][KEY_PIO_FILES][key] = data def generate_pio_files() -> bool: @@ -536,7 +558,7 @@ def generate_pio_files() -> bool: shutil.rmtree(CORE.relative_build_path("src/pio"), ignore_errors=True) includes: list[str] = [] - files = CORE.data[KEY_RP2040][KEY_PIO_FILES] + files = CORE.data[KEY_RP2][KEY_PIO_FILES] if not files: return False for key, data in files.items(): @@ -573,6 +595,7 @@ def copy_files(): inject_lwip_file, CORE.relative_build_path("inject_lwip_include.py"), ) + copy_ccache_script() _generate_lwipopts_h() if generate_pio_files(): path = CORE.relative_src_path("esphome.h") @@ -581,7 +604,7 @@ def copy_files(): # RP2040 crash handler stacktrace decoding -# Matches output from esphome/components/rp2040/crash_handler.cpp +# Matches output from esphome/components/rp2/crash_handler.cpp _CRASH_RE = re.compile(r"CRASH DETECTED ON PREVIOUS BOOT") _CRASH_ADDR_RE = re.compile( r"(?:PC|LR|BT\d):\s+(0x[0-9a-fA-F]{8})\s+\((?:fault location|return address|stack backtrace)\)" diff --git a/esphome/components/rp2040/boards.jinja2 b/esphome/components/rp2/boards.jinja2 similarity index 56% rename from esphome/components/rp2040/boards.jinja2 rename to esphome/components/rp2/boards.jinja2 index 989fb83701..9223009c26 100644 --- a/esphome/components/rp2040/boards.jinja2 +++ b/esphome/components/rp2/boards.jinja2 @@ -1,14 +1,14 @@ # Auto-generated by generate_boards.py — do not edit manually -# To regenerate: python esphome/components/rp2040/generate_boards.py +# To regenerate: python esphome/components/rp2/generate_boards.py # arduino-pico maps pins >= {{ cyw43_gpio_offset }} to CYW43 wireless chip GPIOs CYW43_GPIO_OFFSET = {{ cyw43_gpio_offset }} CYW43_MAX_GPIO = {{ cyw43_max_gpio }} DEFAULT_MAX_PIN = {{ default_max_pin }} -RP2040_BASE_PINS = {} +RP2_BASE_PINS = {} -RP2040_BOARD_PINS = { +RP2_BOARD_PINS = { {%- for name, pins in board_pins %} {{ name | repr }}: {{ pins | format_pins }}, {%- endfor %} @@ -23,3 +23,10 @@ BOARDS = { }, {%- endfor %} } + +# Deprecated: use RP2_BASE_PINS / RP2_BOARD_PINS instead. Kept as back-compat +# aliases so external custom components / tooling that imported the legacy +# names via the ``rp2040`` package alias keep working. +# Scheduled for removal in 2027.7.0. +RP2040_BASE_PINS = RP2_BASE_PINS +RP2040_BOARD_PINS = RP2_BOARD_PINS diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2/boards.py similarity index 99% rename from esphome/components/rp2040/boards.py rename to esphome/components/rp2/boards.py index 0bc5c48d03..94d0ebbb60 100644 --- a/esphome/components/rp2040/boards.py +++ b/esphome/components/rp2/boards.py @@ -1,14 +1,14 @@ # Auto-generated by generate_boards.py — do not edit manually -# To regenerate: python esphome/components/rp2040/generate_boards.py +# To regenerate: python esphome/components/rp2/generate_boards.py # arduino-pico maps pins >= 64 to CYW43 wireless chip GPIOs CYW43_GPIO_OFFSET = 64 CYW43_MAX_GPIO = 66 DEFAULT_MAX_PIN = 29 -RP2040_BASE_PINS = {} +RP2_BASE_PINS = {} -RP2040_BOARD_PINS = { +RP2_BOARD_PINS = { "0xcb_helios": { "LED": 17, "MISO": 20, @@ -2299,3 +2299,10 @@ BOARDS = { "max_pin": 29, }, } + +# Deprecated: use RP2_BASE_PINS / RP2_BOARD_PINS instead. Kept as back-compat +# aliases so external custom components / tooling that imported the legacy +# names via the ``rp2040`` package alias keep working. +# Scheduled for removal in 2027.7.0. +RP2040_BASE_PINS = RP2_BASE_PINS +RP2040_BOARD_PINS = RP2_BOARD_PINS diff --git a/esphome/components/rp2040/build_pio.py.script b/esphome/components/rp2/build_pio.py.script similarity index 100% rename from esphome/components/rp2040/build_pio.py.script rename to esphome/components/rp2/build_pio.py.script diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2/const.py similarity index 91% rename from esphome/components/rp2040/const.py rename to esphome/components/rp2/const.py index 959753d95b..515f9f007c 100644 --- a/esphome/components/rp2040/const.py +++ b/esphome/components/rp2/const.py @@ -2,7 +2,7 @@ import esphome.codegen as cg KEY_BOARD = "board" KEY_LWIP_OPTS = "lwip_opts" -KEY_RP2040 = "rp2040" +KEY_RP2 = "rp2" KEY_PIO_FILES = "pio_files" KEY_VARIANT = "variant" @@ -31,4 +31,4 @@ STANDARD_BOARDS = { VARIANT_RP2350: "rpipico2w", } -rp2040_ns = cg.esphome_ns.namespace("rp2040") +rp2_ns = cg.esphome_ns.namespace("rp2") diff --git a/esphome/components/rp2/core.cpp b/esphome/components/rp2/core.cpp new file mode 100644 index 0000000000..2509f47a86 --- /dev/null +++ b/esphome/components/rp2/core.cpp @@ -0,0 +1,6 @@ +#ifdef USE_RP2 + +// HAL functions live in hal.cpp. core.cpp is intentionally empty for +// rp2 — there is no extra component bootstrap to keep here. + +#endif // USE_RP2 diff --git a/esphome/components/rp2/core.h b/esphome/components/rp2/core.h new file mode 100644 index 0000000000..4ce9151d41 --- /dev/null +++ b/esphome/components/rp2/core.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_RP2 + +#include +#include + +// NOLINTNEXTLINE(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" unsigned long ulMainGetRunTimeCounterValue(); + +namespace esphome::rp2 {} // namespace esphome::rp2 + +#endif // USE_RP2 diff --git a/esphome/components/rp2040/crash_handler.cpp b/esphome/components/rp2/crash_handler.cpp similarity index 97% rename from esphome/components/rp2040/crash_handler.cpp rename to esphome/components/rp2/crash_handler.cpp index f9eb42a0f8..a0fea21637 100644 --- a/esphome/components/rp2040/crash_handler.cpp +++ b/esphome/components/rp2/crash_handler.cpp @@ -1,7 +1,7 @@ -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/defines.h" -#ifdef USE_RP2040_CRASH_HANDLER +#ifdef USE_RP2_CRASH_HANDLER #include "crash_handler.h" #include "esphome/core/log.h" @@ -51,9 +51,9 @@ static inline bool is_code_addr(uint32_t val) { static constexpr size_t MAX_BACKTRACE = 4; -namespace esphome::rp2040 { +namespace esphome::rp2 { -static const char *const TAG = "rp2040.crash"; +static const char *const TAG = "rp2.crash"; // Placed in .noinit so BSS zero-init cannot race with crash_handler_read_and_clear(). // The valid field is explicitly cleared in crash_handler_read_and_clear() instead. @@ -64,7 +64,7 @@ static struct CrashData { uint32_t sp; uint32_t backtrace[MAX_BACKTRACE]; uint8_t backtrace_count; -} s_crash_data __attribute__((section(".noinit"))); +} s_crash_data __attribute__((section(".noinit"))); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) bool crash_handler_has_data() { return s_crash_data.valid; } @@ -117,7 +117,7 @@ void crash_handler_log() { ESP_LOGE(TAG, "%s", hint); } -} // namespace esphome::rp2040 +} // namespace esphome::rp2 // --- HardFault handler --- // Overrides the weak isr_hardfault from arduino-pico's crt0.S. @@ -236,5 +236,5 @@ extern "C" void __attribute__((naked, used)) isr_hardfault() { : "i"(hard_fault_handler_c)); } -#endif // USE_RP2040_CRASH_HANDLER -#endif // USE_RP2040 +#endif // USE_RP2_CRASH_HANDLER +#endif // USE_RP2 diff --git a/esphome/components/rp2040/crash_handler.h b/esphome/components/rp2/crash_handler.h similarity index 66% rename from esphome/components/rp2040/crash_handler.h rename to esphome/components/rp2/crash_handler.h index 78e8ede08c..8c43d9fd3b 100644 --- a/esphome/components/rp2040/crash_handler.h +++ b/esphome/components/rp2/crash_handler.h @@ -1,12 +1,12 @@ #pragma once -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/defines.h" -#ifdef USE_RP2040_CRASH_HANDLER +#ifdef USE_RP2_CRASH_HANDLER -namespace esphome::rp2040 { +namespace esphome::rp2 { /// Read crash data from watchdog scratch registers and clear them. void crash_handler_read_and_clear(); @@ -17,7 +17,7 @@ void crash_handler_log(); /// Returns true if crash data was found this boot. bool crash_handler_has_data(); -} // namespace esphome::rp2040 +} // namespace esphome::rp2 -#endif // USE_RP2040_CRASH_HANDLER -#endif // USE_RP2040 +#endif // USE_RP2_CRASH_HANDLER +#endif // USE_RP2 diff --git a/esphome/components/rp2040/generate_boards.py b/esphome/components/rp2/generate_boards.py similarity index 98% rename from esphome/components/rp2040/generate_boards.py rename to esphome/components/rp2/generate_boards.py index b1a0b17ca3..33eb1b3058 100644 --- a/esphome/components/rp2040/generate_boards.py +++ b/esphome/components/rp2/generate_boards.py @@ -1,6 +1,6 @@ """Generate boards.py from arduino-pico board definitions. -Usage: python esphome/components/rp2040/generate_boards.py +Usage: python esphome/components/rp2/generate_boards.py """ import json diff --git a/esphome/components/rp2040/gpio.cpp b/esphome/components/rp2/gpio.cpp similarity index 81% rename from esphome/components/rp2040/gpio.cpp rename to esphome/components/rp2/gpio.cpp index 4b3c98104c..0dbb124a26 100644 --- a/esphome/components/rp2040/gpio.cpp +++ b/esphome/components/rp2/gpio.cpp @@ -1,12 +1,12 @@ -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "gpio.h" #include "esphome/core/log.h" namespace esphome { -namespace rp2040 { +namespace rp2 { -static const char *const TAG = "rp2040"; +static const char *const TAG = "rp2"; static int flags_to_mode(gpio::Flags flags, uint8_t pin) { if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) @@ -30,7 +30,7 @@ struct ISRPinArg { bool inverted; }; -ISRInternalGPIOPin RP2040GPIOPin::to_isr() const { +ISRInternalGPIOPin RP2GPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) arg->pin = this->pin_; arg->inverted = this->inverted_; @@ -38,7 +38,7 @@ ISRInternalGPIOPin RP2040GPIOPin::to_isr() const { return ISRInternalGPIOPin((void *) arg); } -void RP2040GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { +void RP2GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { PinStatus arduino_mode = LOW; switch (type) { case gpio::INTERRUPT_RISING_EDGE: @@ -60,25 +60,23 @@ void RP2040GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::Inte attachInterrupt(pin_, func, arduino_mode, arg); } -void RP2040GPIOPin::pin_mode(gpio::Flags flags) { +void RP2GPIOPin::pin_mode(gpio::Flags flags) { pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT } -size_t RP2040GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "GPIO%u", this->pin_); -} +size_t RP2GPIOPin::dump_summary(char *buffer, size_t len) const { return snprintf(buffer, len, "GPIO%u", this->pin_); } -bool RP2040GPIOPin::digital_read() { +bool RP2GPIOPin::digital_read() { return bool(digitalRead(pin_)) != inverted_; // NOLINT } -void RP2040GPIOPin::digital_write(bool value) { +void RP2GPIOPin::digital_write(bool value) { digitalWrite(pin_, value != inverted_ ? 1 : 0); // NOLINT } -void RP2040GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } +void RP2GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } -} // namespace rp2040 +} // namespace rp2 -using namespace rp2040; +using namespace rp2; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { auto *arg = reinterpret_cast(this->arg_); @@ -115,4 +113,4 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { } // namespace esphome -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2/gpio.h similarity index 85% rename from esphome/components/rp2040/gpio.h rename to esphome/components/rp2/gpio.h index b9aa497b47..538fef619a 100644 --- a/esphome/components/rp2040/gpio.h +++ b/esphome/components/rp2/gpio.h @@ -1,13 +1,13 @@ #pragma once -#ifdef USE_RP2040 +#ifdef USE_RP2 #include #include "esphome/core/hal.h" -namespace esphome::rp2040 { +namespace esphome::rp2 { -class RP2040GPIOPin final : public InternalGPIOPin { +class RP2GPIOPin final : public InternalGPIOPin { public: void set_pin(uint8_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } @@ -32,6 +32,6 @@ class RP2040GPIOPin final : public InternalGPIOPin { gpio::Flags flags_{}; }; -} // namespace esphome::rp2040 +} // namespace esphome::rp2 -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2/gpio.py similarity index 82% rename from esphome/components/rp2040/gpio.py rename to esphome/components/rp2/gpio.py index 18fb09f76a..e4db6a831c 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2/gpio.py @@ -16,22 +16,22 @@ from esphome.const import ( from esphome.core import CORE from . import boards -from .const import KEY_BOARD, KEY_RP2040, rp2040_ns +from .const import KEY_BOARD, KEY_RP2, rp2_ns -RP2040GPIOPin = rp2040_ns.class_("RP2040GPIOPin", cg.InternalGPIOPin) +RP2GPIOPin = rp2_ns.class_("RP2GPIOPin", cg.InternalGPIOPin) def _lookup_pin(value): - board = CORE.data[KEY_RP2040][KEY_BOARD] - board_pins = boards.RP2040_BOARD_PINS.get(board, {}) + board = CORE.data[KEY_RP2][KEY_BOARD] + board_pins = boards.RP2_BOARD_PINS.get(board, {}) while isinstance(board_pins, str): - board_pins = boards.RP2040_BOARD_PINS[board_pins] + board_pins = boards.RP2_BOARD_PINS[board_pins] if value in board_pins: return board_pins[value] - if value in boards.RP2040_BASE_PINS: - return boards.RP2040_BASE_PINS[value] + if value in boards.RP2_BASE_PINS: + return boards.RP2_BASE_PINS[value] raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.") @@ -61,7 +61,7 @@ def _board_max_virtual_pin(board): def validate_gpio_pin(value): value = _translate_pin(value) - board = CORE.data[KEY_RP2040][KEY_BOARD] + board = CORE.data[KEY_RP2][KEY_BOARD] max_virtual = _board_max_virtual_pin(board) if max_virtual is not None and boards.CYW43_GPIO_OFFSET <= value <= max_virtual: return value @@ -72,7 +72,7 @@ def validate_gpio_pin(value): def validate_supports(value): - board = CORE.data[KEY_RP2040][KEY_BOARD] + board = CORE.data[KEY_RP2][KEY_BOARD] if ( _board_max_virtual_pin(board) is None or value[CONF_NUMBER] < boards.CYW43_GPIO_OFFSET @@ -89,9 +89,9 @@ def validate_supports(value): return value -RP2040_PIN_SCHEMA = cv.All( +RP2_PIN_SCHEMA = cv.All( pins.gpio_base_schema( - RP2040GPIOPin, + RP2GPIOPin, validate_gpio_pin, modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), ), @@ -99,8 +99,8 @@ RP2040_PIN_SCHEMA = cv.All( ) -@pins.PIN_SCHEMA_REGISTRY.register("rp2040", RP2040_PIN_SCHEMA) -async def rp2040_pin_to_code(config): +@pins.PIN_SCHEMA_REGISTRY.register("rp2", RP2_PIN_SCHEMA) +async def rp2_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) diff --git a/esphome/components/rp2040/hal.cpp b/esphome/components/rp2/hal.cpp similarity index 56% rename from esphome/components/rp2040/hal.cpp rename to esphome/components/rp2/hal.cpp index e71d3fd54d..8eb1b469bc 100644 --- a/esphome/components/rp2040/hal.cpp +++ b/esphome/components/rp2/hal.cpp @@ -1,36 +1,35 @@ -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#ifdef USE_RP2040_CRASH_HANDLER +#ifdef USE_RP2_CRASH_HANDLER #include "crash_handler.h" #endif #include "hardware/watchdog.h" -// Empty rp2040 namespace block to satisfy ci-custom's lint_namespace check. +// Empty rp2 namespace block to satisfy ci-custom's lint_namespace check. // HAL functions live in namespace esphome (root) — they are not part of the -// rp2040 component's API. -namespace esphome::rp2040 {} // namespace esphome::rp2040 +// rp2 component's API. +namespace esphome::rp2 {} // namespace esphome::rp2 namespace esphome { // yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(), -// arch_feed_wdt(), arch_get_cpu_cycle_count() inlined in components/rp2040/hal.h. +// arch_feed_wdt(), arch_get_cpu_cycle_count() inlined in components/rp2/hal.h. void arch_restart() { watchdog_reboot(0, 0, 10); - while (1) { - continue; + while (true) { } } void arch_init() { -#ifdef USE_RP2040_CRASH_HANDLER - rp2040::crash_handler_read_and_clear(); +#ifdef USE_RP2_CRASH_HANDLER + rp2::crash_handler_read_and_clear(); #endif -#if USE_RP2040_WATCHDOG_TIMEOUT > 0 - watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false); +#if USE_RP2_WATCHDOG_TIMEOUT > 0 + watchdog_enable(USE_RP2_WATCHDOG_TIMEOUT, false); #endif } @@ -38,4 +37,4 @@ uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } } // namespace esphome -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/rp2040/hal.h b/esphome/components/rp2/hal.h similarity index 84% rename from esphome/components/rp2040/hal.h rename to esphome/components/rp2/hal.h index c9c61c921d..ec46937bab 100644 --- a/esphome/components/rp2040/hal.h +++ b/esphome/components/rp2/hal.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_RP2040 +#ifdef USE_RP2 #include @@ -17,15 +17,15 @@ extern "C" unsigned long micros(void); extern "C" unsigned long millis(void); // NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) -// Forward decl from . +// Forward decls from and the pico-sdk / FreeRTOS port for the +// inline arch_* wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) extern "C" uint64_t time_us_64(void); - -// Forward decls from pico-sdk / FreeRTOS port for the inline arch_* -// wrappers below. extern "C" void watchdog_update(void); extern "C" unsigned long ulMainGetRunTimeCounterValue(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) -namespace esphome::rp2040 {} +namespace esphome::rp2 {} namespace esphome { @@ -58,4 +58,4 @@ uint32_t arch_get_cpu_freq_hz(); } // namespace esphome -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2/helpers.cpp similarity index 98% rename from esphome/components/rp2040/helpers.cpp rename to esphome/components/rp2/helpers.cpp index 6e5ddad236..a54bcf80f7 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2/helpers.cpp @@ -1,7 +1,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/defines.h" -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/hal.h" @@ -89,4 +89,4 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame } // namespace esphome -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/rp2040/inject_lwip_include.py.script b/esphome/components/rp2/inject_lwip_include.py.script similarity index 100% rename from esphome/components/rp2040/inject_lwip_include.py.script rename to esphome/components/rp2/inject_lwip_include.py.script diff --git a/esphome/components/rp2040/lwipopts.h.jinja b/esphome/components/rp2/lwipopts.h.jinja similarity index 100% rename from esphome/components/rp2040/lwipopts.h.jinja rename to esphome/components/rp2/lwipopts.h.jinja diff --git a/esphome/components/rp2040/post_build.py.script b/esphome/components/rp2/post_build.py.script similarity index 100% rename from esphome/components/rp2040/post_build.py.script rename to esphome/components/rp2/post_build.py.script diff --git a/esphome/components/rp2/preference_backend.h b/esphome/components/rp2/preference_backend.h new file mode 100644 index 0000000000..c5e8a757da --- /dev/null +++ b/esphome/components/rp2/preference_backend.h @@ -0,0 +1,27 @@ +#pragma once +#ifdef USE_RP2 + +#include +#include + +namespace esphome::rp2 { + +class RP2PreferenceBackend final { + public: + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + size_t offset = 0; + uint32_t type = 0; +}; + +class RP2Preferences; +RP2Preferences *get_preferences(); + +} // namespace esphome::rp2 + +namespace esphome { +using PreferenceBackend = rp2::RP2PreferenceBackend; +} // namespace esphome + +#endif // USE_RP2 diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2/preferences.cpp similarity index 80% rename from esphome/components/rp2040/preferences.cpp rename to esphome/components/rp2/preferences.cpp index cfc802b28f..d1e0bc555f 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2/preferences.cpp @@ -1,4 +1,4 @@ -#ifdef USE_RP2040 +#ifdef USE_RP2 #include @@ -12,7 +12,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome::rp2040 { +namespace esphome::rp2 { static const char *const TAG = "preferences"; @@ -26,6 +26,7 @@ static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avo // No preference can exceed the total flash storage, so stack buffer covers all cases. static constexpr size_t PREF_MAX_BUFFER_SIZE = RP2040_FLASH_STORAGE_SIZE; +// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp) extern "C" uint8_t _EEPROM_start; template uint8_t calculate_crc(It first, It last, uint32_t type) { @@ -37,10 +38,10 @@ template uint8_t calculate_crc(It first, It last, uint32_t type) { return crc; } -bool RP2040PreferenceBackend::save(const uint8_t *data, size_t len) { - const size_t buffer_size = len + 1; - if (buffer_size > PREF_MAX_BUFFER_SIZE) +bool RP2PreferenceBackend::save(const uint8_t *data, size_t len) { + if (len >= PREF_MAX_BUFFER_SIZE) return false; + const size_t buffer_size = len + 1; uint8_t buffer[PREF_MAX_BUFFER_SIZE]; memcpy(buffer, data, len); buffer[len] = calculate_crc(buffer, buffer + len, this->type); @@ -58,10 +59,10 @@ bool RP2040PreferenceBackend::save(const uint8_t *data, size_t len) { return true; } -bool RP2040PreferenceBackend::load(uint8_t *data, size_t len) { - const size_t buffer_size = len + 1; - if (buffer_size > PREF_MAX_BUFFER_SIZE) +bool RP2PreferenceBackend::load(uint8_t *data, size_t len) { + if (len >= PREF_MAX_BUFFER_SIZE) return false; + const size_t buffer_size = len + 1; uint8_t buffer[PREF_MAX_BUFFER_SIZE]; for (size_t i = 0; i < buffer_size; i++) { @@ -80,27 +81,27 @@ bool RP2040PreferenceBackend::load(uint8_t *data, size_t len) { return true; } -RP2040Preferences::RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {} +RP2Preferences::RP2Preferences() : eeprom_sector_(&_EEPROM_start) {} -void RP2040Preferences::setup() { +void RP2Preferences::setup() { ESP_LOGVV(TAG, "Loading preferences from flash"); memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE); } -ESPPreferenceObject RP2040Preferences::make_preference(size_t length, uint32_t type) { +ESPPreferenceObject RP2Preferences::make_preference(size_t length, uint32_t type) { uint32_t start = this->current_flash_offset; uint32_t end = start + length + 1; if (end > RP2040_FLASH_STORAGE_SIZE) { return {}; } - auto *pref = new RP2040PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + auto *pref = new RP2PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) pref->offset = start; pref->type = type; this->current_flash_offset = end; return ESPPreferenceObject(pref); } -bool RP2040Preferences::sync() { +bool RP2Preferences::sync() { if (!s_flash_dirty) return true; if (s_prevent_write) @@ -121,7 +122,7 @@ bool RP2040Preferences::sync() { return true; } -bool RP2040Preferences::reset() { +bool RP2Preferences::reset() { ESP_LOGD(TAG, "Erasing storage"); { InterruptLock lock; @@ -133,9 +134,9 @@ bool RP2040Preferences::reset() { return true; } -static RP2040Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static RP2Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -RP2040Preferences *get_preferences() { return &s_preferences; } +RP2Preferences *get_preferences() { return &s_preferences; } void setup_preferences() { s_preferences.setup(); @@ -143,10 +144,10 @@ void setup_preferences() { } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } -} // namespace esphome::rp2040 +} // namespace esphome::rp2 namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/rp2040/preferences.h b/esphome/components/rp2/preferences.h similarity index 59% rename from esphome/components/rp2040/preferences.h rename to esphome/components/rp2/preferences.h index eb8c3e5f64..95f7263883 100644 --- a/esphome/components/rp2040/preferences.h +++ b/esphome/components/rp2/preferences.h @@ -1,14 +1,14 @@ #pragma once -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/preference_backend.h" -namespace esphome::rp2040 { +namespace esphome::rp2 { -class RP2040Preferences final : public PreferencesMixin { +class RP2Preferences final : public PreferencesMixin { public: - using PreferencesMixin::make_preference; - RP2040Preferences(); + using PreferencesMixin::make_preference; + RP2Preferences(); void setup(); ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) { return this->make_preference(length, type); @@ -26,8 +26,8 @@ class RP2040Preferences final : public PreferencesMixin { void setup_preferences(); void preferences_prevent_write(bool prevent); -} // namespace esphome::rp2040 +} // namespace esphome::rp2 -DECLARE_PREFERENCE_ALIASES(esphome::rp2040::RP2040Preferences) +DECLARE_PREFERENCE_ALIASES(esphome::rp2::RP2Preferences) -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/rp2040/printf_stubs.cpp b/esphome/components/rp2/printf_stubs.cpp similarity index 90% rename from esphome/components/rp2040/printf_stubs.cpp rename to esphome/components/rp2/printf_stubs.cpp index c2174a1dec..47cf30b263 100644 --- a/esphome/components/rp2040/printf_stubs.cpp +++ b/esphome/components/rp2/printf_stubs.cpp @@ -13,12 +13,12 @@ * Saves ~8.9 KB of flash. */ -#if defined(USE_RP2040) && !defined(USE_FULL_PRINTF) +#if defined(USE_RP2) && !defined(USE_FULL_PRINTF) #include #include #include -namespace esphome::rp2040 {} +namespace esphome::rp2 {} static constexpr size_t PRINTF_BUFFER_SIZE = 512; @@ -33,8 +33,8 @@ static int write_printf_buffer(FILE *stream, char *buf, int len) { if (write_len >= PRINTF_BUFFER_SIZE) { fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); // Use fwrite for the message to avoid recursive __wrap_printf call - static const char msg[] = "\nprintf buffer overflow\n"; - fwrite(msg, 1, sizeof(msg) - 1, stream); + static const char MSG[] = "\nprintf buffer overflow\n"; + fwrite(MSG, 1, sizeof(MSG) - 1, stream); abort(); } if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { @@ -71,4 +71,4 @@ int __wrap_fprintf(FILE *stream, const char *fmt, ...) { } // extern "C" // NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) -#endif // USE_RP2040 && !USE_FULL_PRINTF +#endif // USE_RP2 && !USE_FULL_PRINTF diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp deleted file mode 100644 index 11f23ccfef..0000000000 --- a/esphome/components/rp2040/core.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#ifdef USE_RP2040 - -// HAL functions live in hal.cpp. core.cpp is intentionally empty for -// rp2040 — there is no extra component bootstrap to keep here. - -#endif // USE_RP2040 diff --git a/esphome/components/rp2040/core.h b/esphome/components/rp2040/core.h deleted file mode 100644 index db8937a8a3..0000000000 --- a/esphome/components/rp2040/core.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#ifdef USE_RP2040 - -#include -#include - -extern "C" unsigned long ulMainGetRunTimeCounterValue(); - -namespace esphome::rp2040 {} // namespace esphome::rp2040 - -#endif // USE_RP2040 diff --git a/esphome/components/rp2040/preference_backend.h b/esphome/components/rp2040/preference_backend.h deleted file mode 100644 index 790ee8831d..0000000000 --- a/esphome/components/rp2040/preference_backend.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#ifdef USE_RP2040 - -#include -#include - -namespace esphome::rp2040 { - -class RP2040PreferenceBackend final { - public: - bool save(const uint8_t *data, size_t len); - bool load(uint8_t *data, size_t len); - - size_t offset = 0; - uint32_t type = 0; -}; - -class RP2040Preferences; -RP2040Preferences *get_preferences(); - -} // namespace esphome::rp2040 - -namespace esphome { -using PreferenceBackend = rp2040::RP2040PreferenceBackend; -} // namespace esphome - -#endif // USE_RP2040 diff --git a/esphome/components/rp2040_ble/__init__.py b/esphome/components/rp2040_ble/__init__.py index 648f22691c..ac012b5e85 100644 --- a/esphome/components/rp2040_ble/__init__.py +++ b/esphome/components/rp2040_ble/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID from esphome.types import ConfigType -DEPENDENCIES = ["rp2040"] +DEPENDENCIES = ["rp2"] CODEOWNERS = ["@bdraco"] rp2040_ble_ns = cg.esphome_ns.namespace("rp2040_ble") diff --git a/esphome/components/rp2040_ble/rp2040_ble.cpp b/esphome/components/rp2040_ble/rp2040_ble.cpp index 4125da7ec0..f3896f7b9c 100644 --- a/esphome/components/rp2040_ble/rp2040_ble.cpp +++ b/esphome/components/rp2040_ble/rp2040_ble.cpp @@ -35,10 +35,10 @@ void RP2040BLE::enable() { l2cap_init(); sm_init(); - this->hci_event_callback_registration_.callback = &RP2040BLE::packet_handler_; + this->hci_event_callback_registration_.callback = &RP2040BLE::packet_handler; hci_add_event_handler(&this->hci_event_callback_registration_); - this->sm_event_callback_registration_.callback = &RP2040BLE::packet_handler_; + this->sm_event_callback_registration_.callback = &RP2040BLE::packet_handler; sm_add_event_handler(&this->sm_event_callback_registration_); this->btstack_initialized_ = true; @@ -48,7 +48,7 @@ void RP2040BLE::enable() { } void RP2040BLE::disable() { - if (this->state_ == BLEComponentState::DISABLED || this->state_ == BLEComponentState::OFF) { + if (this->state_ == BLEComponentState::DISABLED || this->state_ == BLEComponentState::STATE_OFF) { return; } @@ -70,7 +70,7 @@ void RP2040BLE::loop() { static const char *state_to_str(BLEComponentState state) { switch (state) { - case BLEComponentState::OFF: + case BLEComponentState::STATE_OFF: return "OFF"; case BLEComponentState::ENABLING: return "ENABLING"; @@ -95,7 +95,7 @@ void RP2040BLE::dump_config() { float RP2040BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } -void RP2040BLE::packet_handler_(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) { +void RP2040BLE::packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) { if (global_ble == nullptr) { return; } diff --git a/esphome/components/rp2040_ble/rp2040_ble.h b/esphome/components/rp2040_ble/rp2040_ble.h index 885e49f690..a77b5fc26c 100644 --- a/esphome/components/rp2040_ble/rp2040_ble.h +++ b/esphome/components/rp2040_ble/rp2040_ble.h @@ -11,7 +11,7 @@ namespace esphome::rp2040_ble { enum class BLEComponentState : uint8_t { - OFF = 0, + STATE_OFF = 0, ENABLING, ACTIVE, DISABLING, @@ -32,12 +32,12 @@ class RP2040BLE final : public Component { void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } protected: - static void packet_handler_(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size); + static void packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size); btstack_packet_callback_registration_t hci_event_callback_registration_{}; btstack_packet_callback_registration_t sm_event_callback_registration_{}; - BLEComponentState state_{BLEComponentState::OFF}; + BLEComponentState state_{BLEComponentState::STATE_OFF}; bool enable_on_boot_{true}; bool btstack_initialized_{false}; bool active_logged_{false}; diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index 8afba6ba1d..cf7041931e 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -1,6 +1,6 @@ #include "led_strip.h" -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -14,26 +14,15 @@ namespace esphome::rp2040_pio_led_strip { -static const char *TAG = "rp2040_pio_led_strip"; - -static uint8_t num_instance_[2] = {0, 0}; -static std::map chipset_offsets_ = { - {CHIPSET_WS2812, 0}, {CHIPSET_WS2812B, 0}, {CHIPSET_SK6812, 0}, {CHIPSET_SM16703, 0}, {CHIPSET_CUSTOM, 0}, -}; -static std::map conf_count_ = { - {CHIPSET_WS2812, false}, {CHIPSET_WS2812B, false}, {CHIPSET_SK6812, false}, - {CHIPSET_SM16703, false}, {CHIPSET_CUSTOM, false}, -}; -static bool dma_chan_active_[12]; -static struct semaphore dma_write_complete_sem_[12]; +static const char *const TAG = "rp2040_pio_led_strip"; // DMA interrupt service routine -void RP2040PIOLEDStripLightOutput::dma_write_complete_handler_() { +void RP2040PIOLEDStripLightOutput::dma_write_complete_handler() { uint32_t channel = dma_hw->ints0; for (uint dma_chan = 0; dma_chan < 12; ++dma_chan) { - if (RP2040PIOLEDStripLightOutput::dma_chan_active_[dma_chan] && (channel & (1u << dma_chan))) { - dma_hw->ints0 = (1u << dma_chan); // Clear the interrupt - sem_release(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[dma_chan]); // Handle the interrupt + if (RP2040PIOLEDStripLightOutput::dma_chan_active[dma_chan] && (channel & (1u << dma_chan))) { + dma_hw->ints0 = (1u << dma_chan); // Clear the interrupt + sem_release(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem[dma_chan]); // Handle the interrupt } } } @@ -69,22 +58,22 @@ void RP2040PIOLEDStripLightOutput::setup() { // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO uint offset = 0; - if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] >= 4) { + if (RP2040PIOLEDStripLightOutput::num_instance[this->pio_ == pio0 ? 0 : 1] >= 4) { ESP_LOGE(TAG, "Too many instances of PIO program"); this->mark_failed(); return; } // keep track of how many instances of the PIO program are running on each PIO - RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1]++; + RP2040PIOLEDStripLightOutput::num_instance[this->pio_ == pio0 ? 0 : 1]++; // if there are multiple strips of the same chipset, we can reuse the same PIO program and save space - if (this->conf_count_[this->chipset_]) { - offset = RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_]; + if (RP2040PIOLEDStripLightOutput::conf_count[this->chipset_]) { + offset = RP2040PIOLEDStripLightOutput::chipset_offsets[this->chipset_]; } else { // Load the assembled program into the PIO and get its location in the PIO's instruction memory and save it offset = pio_add_program(this->pio_, this->program_); - RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_] = offset; - RP2040PIOLEDStripLightOutput::conf_count_[this->chipset_] = true; + RP2040PIOLEDStripLightOutput::chipset_offsets[this->chipset_] = offset; + RP2040PIOLEDStripLightOutput::conf_count[this->chipset_] = true; } // Configure the state machine's PIO, and start it @@ -106,7 +95,7 @@ void RP2040PIOLEDStripLightOutput::setup() { } // Mark the DMA channel as active - RP2040PIOLEDStripLightOutput::dma_chan_active_[this->dma_chan_] = true; + RP2040PIOLEDStripLightOutput::dma_chan_active[this->dma_chan_] = true; this->dma_config_ = dma_channel_get_default_config(this->dma_chan_); channel_config_set_transfer_data_size( @@ -125,11 +114,11 @@ void RP2040PIOLEDStripLightOutput::setup() { ); // Initialize the semaphore for this DMA channel - sem_init(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_], 1, 1); + sem_init(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem[this->dma_chan_], 1, 1); - irq_set_exclusive_handler(DMA_IRQ_0, dma_write_complete_handler_); // after DMA all data, raise an interrupt - dma_channel_set_irq0_enabled(this->dma_chan_, true); // map DMA channel to interrupt - irq_set_enabled(DMA_IRQ_0, true); // enable interrupt + irq_set_exclusive_handler(DMA_IRQ_0, dma_write_complete_handler); // after DMA all data, raise an interrupt + dma_channel_set_irq0_enabled(this->dma_chan_, true); // map DMA channel to interrupt + irq_set_enabled(DMA_IRQ_0, true); // enable interrupt this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_); } @@ -148,12 +137,12 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { } // the bits are already in the correct order for the pio program so we can just copy the buffer using DMA - sem_acquire_blocking(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_]); + sem_acquire_blocking(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem[this->dma_chan_]); dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_()); } light::ESPColorView RP2040PIOLEDStripLightOutput::get_view_internal(int32_t index) const { - int32_t r = 0, g = 0, b = 0, w = 0; + int32_t r = 0, g = 0, b = 0; switch (this->rgb_order_) { case ORDER_RGB: r = 0; diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.h b/esphome/components/rp2040_pio_led_strip/led_strip.h index aaa5b0842d..c499f0a7ca 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.h +++ b/esphome/components/rp2040_pio_led_strip/led_strip.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/color.h" #include "esphome/core/component.h" @@ -95,7 +95,7 @@ class RP2040PIOLEDStripLightOutput final : public light::AddressableLight { size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } - static void dma_write_complete_handler_(); + static void dma_write_complete_handler(); uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; @@ -119,13 +119,13 @@ class RP2040PIOLEDStripLightOutput final : public light::AddressableLight { init_fn init_; private: - inline static int num_instance_[2]; - inline static std::map conf_count_; - inline static std::map chipset_offsets_; - inline static bool dma_chan_active_[12]; - inline static struct semaphore dma_write_complete_sem_[12]; + inline static int num_instance[2]; + inline static std::map conf_count; + inline static std::map chipset_offsets; + inline static bool dma_chan_active[12]; + inline static struct semaphore dma_write_complete_sem[12]; }; } // namespace esphome::rp2040_pio_led_strip -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py index 274f059bd5..b3f816102a 100644 --- a/esphome/components/rp2040_pio_led_strip/light.py +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from esphome import pins import esphome.codegen as cg -from esphome.components import light, rp2040 +from esphome.components import light, rp2 import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -130,9 +130,9 @@ def time_to_cycles(time_us): CONF_PIO = "pio" -AUTO_LOAD = ["rp2040_pio"] +AUTO_LOAD = ["rp2_pio"] CODEOWNERS = ["@Papa-DMan"] -DEPENDENCIES = ["rp2040"] +DEPENDENCIES = ["rp2"] rp2040_pio_led_strip_ns = cg.esphome_ns.namespace("rp2040_pio_led_strip") RP2040PIOLEDStripLightOutput = rp2040_pio_led_strip_ns.class_( @@ -250,7 +250,7 @@ async def to_code(config): if chipset := config.get(CONF_CHIPSET): cg.add(var.set_chipset(chipset)) _LOGGER.info("Generating PIO assembly code") - rp2040.add_pio_file( + rp2.add_pio_file( __name__, key, generate_assembly_code( @@ -265,7 +265,7 @@ async def to_code(config): else: cg.add(var.set_chipset(Chipset.CHIPSET_CUSTOM)) _LOGGER.info("Generating custom PIO assembly code") - rp2040.add_pio_file( + rp2.add_pio_file( __name__, key, generate_assembly_code( diff --git a/esphome/components/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py index ad37926954..a2fda58c9e 100644 --- a/esphome/components/rp2040_pwm/output.py +++ b/esphome/components/rp2040_pwm/output.py @@ -5,7 +5,7 @@ import esphome.config_validation as cv from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_PIN CODEOWNERS = ["@jesserockz"] -DEPENDENCIES = ["rp2040"] +DEPENDENCIES = ["rp2"] rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm") diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.cpp b/esphome/components/rp2040_pwm/rp2040_pwm.cpp index c9b9e6739d..270cc33551 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.cpp +++ b/esphome/components/rp2040_pwm/rp2040_pwm.cpp @@ -1,4 +1,4 @@ -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "rp2040_pwm.h" #include "esphome/core/defines.h" diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.h b/esphome/components/rp2040_pwm/rp2040_pwm.h index 49980a7d76..8263113168 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.h +++ b/esphome/components/rp2040_pwm/rp2040_pwm.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/components/output/float_output.h" #include "esphome/core/automation.h" @@ -54,4 +54,4 @@ template class SetFrequencyAction final : public Action { } // namespace esphome::rp2040_pwm -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/rp2040_pio/__init__.py b/esphome/components/rp2_pio/__init__.py similarity index 98% rename from esphome/components/rp2040_pio/__init__.py rename to esphome/components/rp2_pio/__init__.py index eecfedaa75..9046d2ae6b 100644 --- a/esphome/components/rp2040_pio/__init__.py +++ b/esphome/components/rp2_pio/__init__.py @@ -3,7 +3,7 @@ import platform import esphome.codegen as cg import esphome.config_validation as cv -DEPENDENCIES = ["rp2040"] +DEPENDENCIES = ["rp2"] PIOASM_REPO_VERSION = "1.5.0-b" diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 8db69aa53e..d8517d4493 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -82,7 +82,7 @@ class JPEGFormat(Format): # JPEGDEC uses ESP32-S3 SIMD optimizations (guarded by board-level # ARDUINO_ESP32S3_DEV define) that require esp-dsp headers. # On Arduino this overwrites the stub; on IDF it adds the component. - add_idf_component(name="espressif/esp-dsp", ref="1.7.1") + add_idf_component(name="espressif/esp-dsp", ref="1.8.2") class PNGFormat(Format): diff --git a/esphome/components/runtime_image/image_decoder.cpp b/esphome/components/runtime_image/image_decoder.cpp index 8d3320b5d1..f2c4f5c8cd 100644 --- a/esphome/components/runtime_image/image_decoder.cpp +++ b/esphome/components/runtime_image/image_decoder.cpp @@ -10,12 +10,16 @@ static const char *const TAG = "image_decoder"; bool ImageDecoder::set_size(int width, int height) { bool success = this->image_->resize(width, height) > 0; + this->size_valid_ = success; this->x_scale_ = static_cast(this->image_->get_buffer_width()) / width; this->y_scale_ = static_cast(this->image_->get_buffer_height()) / height; return success; } void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { + if (!this->size_valid_) { + return; + } auto width = std::min(this->image_->get_buffer_width(), static_cast(std::ceil((x + w) * this->x_scale_))); auto height = std::min(this->image_->get_buffer_height(), static_cast(std::ceil((y + h) * this->y_scale_))); for (int i = x * this->x_scale_; i < width; i++) { diff --git a/esphome/components/runtime_image/image_decoder.h b/esphome/components/runtime_image/image_decoder.h index c68ea5720b..6d351a10aa 100644 --- a/esphome/components/runtime_image/image_decoder.h +++ b/esphome/components/runtime_image/image_decoder.h @@ -108,6 +108,7 @@ class ImageDecoder { size_t decoded_bytes_ = 0; // Bytes processed so far double x_scale_ = 1.0; double y_scale_ = 1.0; + bool size_valid_ = true; // Last set_size() result; draw() no-ops while false }; } // namespace esphome::runtime_image diff --git a/esphome/components/runtime_image/png_decoder.cpp b/esphome/components/runtime_image/png_decoder.cpp index 12bce0d284..9501702711 100644 --- a/esphome/components/runtime_image/png_decoder.cpp +++ b/esphome/components/runtime_image/png_decoder.cpp @@ -96,6 +96,8 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { if (fed < 0) { ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_)); return DECODE_ERROR_INTERNAL_DECODER_ERROR; + } else if (!this->size_valid_) { + return DECODE_ERROR_OUT_OF_MEMORY; } else { this->decoded_bytes_ += fed; } diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 4c7f1bfb6f..8fe9be4c8c 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -3,7 +3,9 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include +#include #include +#include #ifdef USE_RUNTIME_IMAGE_BMP #include "bmp_decoder.h" @@ -19,6 +21,13 @@ namespace esphome::runtime_image { static const char *const TAG = "runtime_image"; +// Widest supported format is 4 bytes/pixel, so 32767 * 32767 * 4 still fits a 32-bit size_t +static constexpr int MAX_IMAGE_DIMENSION = 32767; +static constexpr int MAX_IMAGE_BPP = 32; +static_assert((static_cast(MAX_IMAGE_BPP) * MAX_IMAGE_DIMENSION + 7) / 8 * MAX_IMAGE_DIMENSION <= + std::numeric_limits::max(), + "MAX_IMAGE_DIMENSION must keep the worst-case buffer size within size_t"); + inline bool is_color_on(const Color &color) { // This produces the most accurate monochrome conversion, but is slightly slower. // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; @@ -239,9 +248,15 @@ void RuntimeImage::release() { void RuntimeImage::release_buffer_() { if (this->buffer_) { - ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); - RAMAllocator allocator; - allocator.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + if (this->external_buffer_) { + // The caller owns this memory and goes on using it after the image lets go of it. + ESP_LOGV(TAG, "Letting go of the external %dx%d buffer", this->buffer_width_, this->buffer_height_); + this->external_buffer_ = false; + } else { + ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size(this->buffer_width_, this->buffer_height_)); + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->get_buffer_size(this->buffer_width_, this->buffer_height_)); + } this->buffer_ = nullptr; this->data_start_ = nullptr; this->width_ = 0; @@ -254,14 +269,46 @@ void RuntimeImage::release_buffer_() { } } -size_t RuntimeImage::resize_buffer_(int width, int height) { - size_t new_size = this->get_buffer_size_(width, height); +bool RuntimeImage::set_external_buffer(uint8_t *buffer, int width, int height) { + this->release_buffer_(); + if (buffer == nullptr || this->get_buffer_size(width, height) == 0) { + // Keep the released state rather than remembering a buffer that cannot be decoded into: an + // external buffer that is never handed back would otherwise block every later allocation. + ESP_LOGE(TAG, "Refusing an invalid external buffer for %dx%d", width, height); + return false; + } + this->buffer_ = buffer; + this->external_buffer_ = true; + this->buffer_width_ = width; + this->buffer_height_ = height; + return true; +} +size_t RuntimeImage::resize_buffer_(int width, int height) { + size_t new_size = this->get_buffer_size(width, height); + + // A buffer only ever exists with dimensions the image can decode at, so a match here means + // new_size is non-zero. Checking it before the invalid dimension case below lets the external + // buffer be let go of for every decode it cannot serve, not just for valid other dimensions. if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) { // Buffer already allocated with correct size return new_size; } + if (this->external_buffer_) { + ESP_LOGE(TAG, "Image decoded to %dx%d, but the external buffer is %dx%d", width, height, this->buffer_width_, + this->buffer_height_); + // Let the buffer go rather than free memory that belongs to the caller. Dropping it also stops + // a decoder that ignores this failure from publishing a picture it never painted. + this->release_buffer_(); + return 0; + } + + if (new_size == 0) { + ESP_LOGE(TAG, "Refusing to allocate buffer for invalid image dimensions %dx%d", width, height); + return 0; + } + // Release old buffer if dimensions changed if (this->buffer_) { this->release_buffer_(); @@ -286,12 +333,16 @@ size_t RuntimeImage::resize_buffer_(int width, int height) { return new_size; } -size_t RuntimeImage::get_buffer_size_(int width, int height) const { +size_t RuntimeImage::get_buffer_size(int width, int height) const { + // Dimensions come from a remote image header; reject absurd values so the size math cannot overflow + if (width <= 0 || height <= 0 || width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) { + return 0; + } if (this->get_type() == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { // Add extra alpha channel for RGB565 with alpha - return width * height * 3; + return static_cast(width) * height * 3; } - return (this->get_bpp() * width + 7u) / 8u * height; + return (static_cast(this->get_bpp()) * width + 7u) / 8u * height; } int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } diff --git a/esphome/components/runtime_image/runtime_image.h b/esphome/components/runtime_image/runtime_image.h index 4bdcdcac9e..10ce980be2 100644 --- a/esphome/components/runtime_image/runtime_image.h +++ b/esphome/components/runtime_image/runtime_image.h @@ -121,9 +121,48 @@ class RuntimeImage : public image::Image { /** * @brief Release the image buffer and free memory. + * + * An external buffer is let go of rather than freed. */ void release(); + /** + * @brief Decode into a buffer the caller owns, instead of one allocated here. + * + * The image never frees an external buffer and never resizes it: a decode that needs other + * dimensions fails as if the allocation had failed, and the buffer is let go of so a decoder + * that ignores that failure cannot publish a picture it did not paint. The caller keeps the + * buffer alive for as long as anything can draw the image, and calls release() (or hands over + * another buffer) before reusing it. + * + * Hand a buffer over before every decode. The image lets go of one whenever a decode fails and + * whenever release() is called, and it does not remember that it ever had one: a decode that + * starts without a buffer allocates its own, which is the runtime allocation this method exists + * to avoid. + * + * The buffer is decoded into as it is handed over, so the caller owns its initial contents. + * Zero it first if anything can draw the image before a decode has painted every pixel. + * + * Do not hand a buffer over while is_decoding() is true. A running decoder keeps scaling values + * for the buffer it started with. + * + * A null buffer or dimensions the image cannot decode at are refused, leaving the image with + * no buffer at all. + * + * @param buffer Memory for a picture of the given size, at least get_buffer_size() bytes. + * @param width Width of the buffer in pixels. + * @param height Height of the buffer in pixels. + * @return true if the image took the buffer, false if it was refused. + */ + bool set_external_buffer(uint8_t *buffer, int width, int height); + + /** + * @brief Get the buffer size in bytes needed for a picture of the given dimensions. + * + * Returns 0 for dimensions the image cannot decode at. + */ + size_t get_buffer_size(int width, int height) const; + /** * @brief Set whether to allow progressive display during decode. * @@ -149,11 +188,6 @@ class RuntimeImage : public image::Image { */ void release_buffer_(); - /** - * @brief Get the buffer size in bytes for given dimensions. - */ - size_t get_buffer_size_(int width, int height) const; - /** * @brief Get the position in the buffer for a pixel. */ @@ -208,6 +242,8 @@ class RuntimeImage : public image::Image { * This is used to determine how to store 16 bit colors in the buffer. */ bool is_big_endian_{false}; + /** Whether buffer_ belongs to the caller, so it must not be freed or resized here. */ + bool external_buffer_{false}; }; } // namespace esphome::runtime_image diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index 578376258a..70096a56bc 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -1,4 +1,4 @@ -from esphome import automation +from esphome import automation, preferences import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -7,6 +7,7 @@ from esphome.const import ( CONF_NUM_ATTEMPTS, CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, + CONF_STORAGE, KEY_PAST_SAFE_MODE, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -15,6 +16,7 @@ from esphome.cpp_generator import RawExpression CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"] CONF_BOOT_IS_GOOD_AFTER = "boot_is_good_after" +CONF_BOOT_IS_GOOD_ON_SHUTDOWN = "boot_is_good_on_shutdown" CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") @@ -36,12 +38,14 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_BOOT_IS_GOOD_AFTER, default="1min" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_BOOT_IS_GOOD_ON_SHUTDOWN, default=True): cv.boolean, cv.Optional(CONF_DISABLED, default=False): cv.boolean, cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int, cv.Optional( CONF_REBOOT_TIMEOUT, default="5min" ): cv.positive_time_period_milliseconds, cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation({}), + **preferences.storage_schema(), } ).extend(cv.COMPONENT_SCHEMA), _remove_id_if_disabled, @@ -76,6 +80,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + if config[CONF_BOOT_IS_GOOD_ON_SHUTDOWN]: + cg.add_define("USE_SAFE_MODE_BOOT_IS_GOOD_ON_SHUTDOWN") + if on_safe_mode := config.get(CONF_ON_SAFE_MODE): cg.add_define("USE_SAFE_MODE_CALLBACK") cg.add_define("ESPHOME_SAFE_MODE_CALLBACK_COUNT", len(on_safe_mode)) @@ -87,6 +94,7 @@ async def to_code(config): config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT], config[CONF_BOOT_IS_GOOD_AFTER], + preferences.is_in_flash(config[CONF_STORAGE]), ) cg.add(RawExpression(f"if ({condition}) return")) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 5c0047dca0..ce029b4f55 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -117,19 +117,31 @@ void SafeModeComponent::dump_config() { float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } -void SafeModeComponent::mark_successful() { - this->clean_rtc(); - this->boot_successful_ = true; -#if defined(USE_OTA_ROLLBACK) -// Mark OTA partition as valid to prevent rollback +#ifdef USE_OTA_ROLLBACK +void SafeModeComponent::confirm_app_image_() { + // Mark the running app image as valid so the bootloader will not roll back + // to the previously flashed image #if defined(USE_ZEPHYR) if (!boot_is_img_confirmed()) { boot_write_img_confirmed(); } #elif defined(USE_ESP32) - // Mark OTA partition as valid to prevent rollback - esp_ota_mark_app_valid_cancel_rollback(); + // esp_ota_mark_app_valid_cancel_rollback() acts on the partition selected + // for the next boot, not the running one. After an OTA update those differ: + // the new image is already selected, and marking it valid before it has ever + // booted would disable rollback protection for that update. + if (esp_ota_get_running_partition() == esp_ota_get_boot_partition()) { + esp_ota_mark_app_valid_cancel_rollback(); + } #endif +} +#endif + +void SafeModeComponent::mark_successful() { + this->clean_rtc(); + this->boot_successful_ = true; +#ifdef USE_OTA_ROLLBACK + this->confirm_app_image_(); #endif // Disable loop since we no longer need to check this->disable_loop(); @@ -162,13 +174,13 @@ bool SafeModeComponent::get_safe_mode_pending() { return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; } -bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, - uint32_t boot_is_good_after) { +bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after, + bool in_flash) { this->safe_mode_start_time_ = millis(); this->safe_mode_enable_time_ = enable_time; this->safe_mode_boot_is_good_after_ = boot_is_good_after; this->safe_mode_num_attempts_ = num_attempts; - this->rtc_ = global_preferences->make_preference(RTC_KEY, false); + this->rtc_ = global_preferences->make_preference(RTC_KEY, in_flash); #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) // Check partition state to detect if bootloader supports rollback @@ -266,6 +278,15 @@ void SafeModeComponent::clean_rtc() { void SafeModeComponent::on_safe_shutdown() { if (this->read_rtc_() != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) this->clean_rtc(); +#if defined(USE_OTA_ROLLBACK) && defined(USE_SAFE_MODE_BOOT_IS_GOOD_ON_SHUTDOWN) + // An orderly shutdown (deep sleep, restart, power off) means the firmware is + // functional, so confirm the running app image even if boot_is_good_after has + // not elapsed yet. Without this, a device that enters deep sleep shortly + // after waking would have every OTA update rolled back by the bootloader on + // the next wake. Can be turned off with boot_is_good_on_shutdown: false for + // strict rollback semantics. + this->confirm_app_image_(); +#endif } } // namespace esphome::safe_mode diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 94db4357eb..0633c92a78 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -17,7 +17,7 @@ constexpr uint32_t RTC_KEY = 233825507UL; /// SafeModeComponent provides a safe way to recover from repeated boot failures class SafeModeComponent final : public Component { public: - bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after); + bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after, bool in_flash); /// Set to true if the next startup will enter safe mode void set_safe_mode_pending(const bool &pending); @@ -42,6 +42,9 @@ class SafeModeComponent final : public Component { protected: void write_rtc_(uint32_t val); uint32_t read_rtc_(); +#ifdef USE_OTA_ROLLBACK + void confirm_app_image_(); +#endif // Group all 4-byte aligned members together to avoid padding uint32_t safe_mode_boot_is_good_after_{60000}; ///< The amount of time after which the boot is considered successful diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp index a4fe6e7d35..1ebc7fa3d8 100644 --- a/esphome/components/sdm_meter/sdm_meter.cpp +++ b/esphome/components/sdm_meter/sdm_meter.cpp @@ -7,10 +7,10 @@ namespace esphome::sdm_meter { static const char *const TAG = "sdm_meter"; -static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t MODBUS_REGISTER_COUNT = 80; // 74 x 16-bit registers -void SDMMeter::on_modbus_data(const std::vector &data) { +void SDMMeter::on_response(std::span request_pdu, std::span response_pdu) { + auto data = modbus::helpers::server_pdu_payload(response_pdu); if (data.size() < MODBUS_REGISTER_COUNT * 2) { ESP_LOGW(TAG, "Invalid size for SDMMeter!"); return; @@ -82,7 +82,7 @@ void SDMMeter::on_modbus_data(const std::vector &data) { this->export_reactive_energy_sensor_->publish_state(export_reactive_energy); } -void SDMMeter::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void SDMMeter::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT); } void SDMMeter::dump_config() { ESP_LOGCONFIG(TAG, "SDM Meter:\n" diff --git a/esphome/components/sdm_meter/sdm_meter.h b/esphome/components/sdm_meter/sdm_meter.h index aa71fcaa47..e09b74bbc0 100644 --- a/esphome/components/sdm_meter/sdm_meter.h +++ b/esphome/components/sdm_meter/sdm_meter.h @@ -4,7 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" -#include +#include namespace esphome::sdm_meter { @@ -55,7 +55,7 @@ class SDMMeter final : public PollingComponent, public modbus::ModbusClientDevic void update() override; - void on_modbus_data(const std::vector &data) override; + void on_response(std::span request_pdu, std::span response_pdu) override; void dump_config() override; diff --git a/esphome/components/selec_meter/selec_meter.cpp b/esphome/components/selec_meter/selec_meter.cpp index f612b89934..688923d8e6 100644 --- a/esphome/components/selec_meter/selec_meter.cpp +++ b/esphome/components/selec_meter/selec_meter.cpp @@ -7,10 +7,10 @@ namespace esphome::selec_meter { static const char *const TAG = "selec_meter"; -static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers -void SelecMeter::on_modbus_data(const std::vector &data) { +void SelecMeter::on_response(std::span request_pdu, std::span response_pdu) { + auto data = modbus::helpers::server_pdu_payload(response_pdu); if (data.size() < MODBUS_REGISTER_COUNT * 2) { ESP_LOGW(TAG, "Invalid size for SelecMeter!"); return; @@ -81,7 +81,7 @@ void SelecMeter::on_modbus_data(const std::vector &data) { this->maximum_demand_apparent_power_sensor_->publish_state(maximum_demand_apparent_power); } -void SelecMeter::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void SelecMeter::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT); } void SelecMeter::dump_config() { ESP_LOGCONFIG(TAG, "SELEC Meter:\n" diff --git a/esphome/components/selec_meter/selec_meter.h b/esphome/components/selec_meter/selec_meter.h index c367d1d15d..5ae1f9bf99 100644 --- a/esphome/components/selec_meter/selec_meter.h +++ b/esphome/components/selec_meter/selec_meter.h @@ -4,7 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" -#include +#include namespace esphome::selec_meter { @@ -37,7 +37,7 @@ class SelecMeter final : public PollingComponent, public modbus::ModbusClientDev void update() override; - void on_modbus_data(const std::vector &data) override; + void on_response(std::span request_pdu, std::span response_pdu) override; void dump_config() override; }; diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 588650e630..f8df89ee33 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -101,25 +101,31 @@ void SEN5XComponent::setup() { ESP_LOGV(TAG, "Serial number %s", this->serial_number_); uint16_t raw_product_name[16]; - if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { - ESP_LOGE(TAG, "Failed to read product name"); - this->error_code_ = PRODUCT_NAME_FAILED; - this->mark_failed(); - return; + Sen5xType detected_type = Sen5xType::UNKNOWN; + if (this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { + const char *product_name = sensirion_convert_to_string_in_place(raw_product_name, 16); + if (strncmp(product_name, "SEN50", 5) == 0) { + detected_type = Sen5xType::SEN50; + } else if (strncmp(product_name, "SEN54", 5) == 0) { + detected_type = Sen5xType::SEN54; + } else if (strncmp(product_name, "SEN55", 5) == 0) { + detected_type = Sen5xType::SEN55; + } } - const char *product_name = sensirion_convert_to_string_in_place(raw_product_name, 16); - if (strncmp(product_name, "SEN50", 5) == 0) { - this->type_ = Sen5xType::SEN50; - } else if (strncmp(product_name, "SEN54", 5) == 0) { - this->type_ = Sen5xType::SEN54; - } else if (strncmp(product_name, "SEN55", 5) == 0) { - this->type_ = Sen5xType::SEN55; - } else { + + if (this->model_override_.has_value()) { + if (detected_type != this->model_override_.value()) { + ESP_LOGW(TAG, "Detected %s, using %s", LOG_STR_ARG(type_to_string(detected_type)), + LOG_STR_ARG(type_to_string(this->model_override_.value()))); + } + this->type_ = this->model_override_.value(); + } else if (detected_type == Sen5xType::UNKNOWN) { this->type_ = Sen5xType::UNKNOWN; - ESP_LOGE(TAG, "Unknown product name: %.32s", product_name); this->error_code_ = PRODUCT_NAME_FAILED; this->mark_failed(); return; + } else { + this->type_ = detected_type; } ESP_LOGD(TAG, "Type: %s", LOG_STR_ARG(type_to_string(this->type_))); @@ -255,10 +261,12 @@ void SEN5XComponent::dump_config() { } } ESP_LOGCONFIG(TAG, - " Type: %s\n" + " Type: %s%s\n" " Firmware version: %d\n" " Serial number: %s", - LOG_STR_ARG(type_to_string(this->type_)), this->firmware_version_, this->serial_number_); + LOG_STR_ARG(type_to_string(this->type_)), + this->model_override_.has_value() ? LOG_STR_LITERAL(" (overridden)") : LOG_STR_LITERAL(""), + this->firmware_version_, this->serial_number_); if (this->auto_cleaning_interval_.has_value()) { ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value()); } diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 6b5a1f8510..ed7689af52 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -95,6 +95,7 @@ class SEN5XComponent final : public PollingComponent, public sensirion_common::S temp_comp.time_constant = time_constant; this->temperature_compensation_ = temp_comp; } + void set_model(Sen5xType model) { this->model_override_ = model; } bool start_fan_cleaning(); protected: @@ -126,6 +127,7 @@ class SEN5XComponent final : public PollingComponent, public sensirion_common::S optional voc_tuning_params_; optional nox_tuning_params_; optional temperature_compensation_; + optional model_override_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 480654ee1b..761a1885ea 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -2,6 +2,7 @@ from esphome import automation from esphome.automation import maybe_simple_id import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor +from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX import esphome.config_validation as cv from esphome.const import ( CONF_ALGORITHM_TUNING, @@ -12,6 +13,7 @@ from esphome.const import ( CONF_INDEX_OFFSET, CONF_LEARNING_TIME_GAIN_HOURS, CONF_LEARNING_TIME_OFFSET_HOURS, + CONF_MODEL, CONF_NORMALIZED_OFFSET_SLOPE, CONF_NOX, CONF_OFFSET, @@ -39,6 +41,7 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_PERCENT, ) +from esphome.types import ConfigType CODEOWNERS = ["@martgras"] DEPENDENCIES = ["i2c"] @@ -49,6 +52,7 @@ SEN5XComponent = sen5x_ns.class_( "SEN5XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice ) RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode") +Sen5xType = sen5x_ns.enum("Sen5xType", is_class=True) CONF_ACCELERATION_MODE = "acceleration_mode" CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" @@ -63,6 +67,12 @@ ACCELERATION_MODES = { "high": RhtAccelerationMode.HIGH_ACCELERATION, } +MODELS = { + "SEN50": Sen5xType.SEN50, + "SEN54": Sen5xType.SEN54, + "SEN55": Sen5xType.SEN55, +} + def _gas_sensor( *, @@ -113,7 +123,9 @@ def float_previously_pct(value): return value -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( + cv.rename_key(CONF_VOC, CONF_VOC_INDEX, removed_in="2027.2.0", component="sen5x"), + cv.rename_key(CONF_NOX, CONF_NOX_INDEX, removed_in="2027.2.0", component="sen5x"), cv.Schema( { cv.GenerateID(): cv.declare_id(SEN5XComponent), @@ -145,7 +157,7 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval, - cv.Optional(CONF_VOC): _gas_sensor( + cv.Optional(CONF_VOC_INDEX): _gas_sensor( index_offset=100, learning_time_offset=12, learning_time_gain=12, @@ -153,7 +165,7 @@ CONFIG_SCHEMA = ( std_initial=50, gain_factor=230, ), - cv.Optional(CONF_NOX): _gas_sensor( + cv.Optional(CONF_NOX_INDEX): _gas_sensor( index_offset=1, learning_time_offset=12, learning_time_gain=12, @@ -186,10 +198,11 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_ACCELERATION_MODE): cv.enum(ACCELERATION_MODES), + cv.Optional(CONF_MODEL): cv.enum(MODELS, upper=True), } ) .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x69)) + .extend(i2c.i2c_device_schema(0x69)), ) SENSOR_MAP = { @@ -197,8 +210,8 @@ SENSOR_MAP = { CONF_PM_2_5: "set_pm_2_5_sensor", CONF_PM_4_0: "set_pm_4_0_sensor", CONF_PM_10_0: "set_pm_10_0_sensor", - CONF_VOC: "set_voc_sensor", - CONF_NOX: "set_nox_sensor", + CONF_VOC_INDEX: "set_voc_sensor", + CONF_NOX_INDEX: "set_nox_sensor", CONF_TEMPERATURE: "set_temperature_sensor", CONF_HUMIDITY: "set_humidity_sensor", } @@ -210,7 +223,7 @@ SETTING_MAP = { } -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) @@ -219,12 +232,15 @@ async def to_code(config): if cfg := config.get(key): cg.add(getattr(var, funcName)(cfg)) + if (model := config.get(CONF_MODEL)) is not None: + cg.add(var.set_model(model)) + for key, funcName in SENSOR_MAP.items(): if cfg := config.get(key): sens = await sensor.new_sensor(cfg) cg.add(getattr(var, funcName)(sens)) - if cfg := config.get(CONF_VOC, {}).get(CONF_ALGORITHM_TUNING): + if cfg := config.get(CONF_VOC_INDEX, {}).get(CONF_ALGORITHM_TUNING): cg.add( var.set_voc_algorithm_tuning( cfg[CONF_INDEX_OFFSET], @@ -235,7 +251,7 @@ async def to_code(config): cfg[CONF_GAIN_FACTOR], ) ) - if cfg := config.get(CONF_NOX, {}).get(CONF_ALGORITHM_TUNING): + if cfg := config.get(CONF_NOX_INDEX, {}).get(CONF_ALGORITHM_TUNING): cg.add( var.set_nox_algorithm_tuning( cfg[CONF_INDEX_OFFSET], diff --git a/esphome/components/sen6x/sensor.py b/esphome/components/sen6x/sensor.py index 5eb34add65..832a2188ee 100644 --- a/esphome/components/sen6x/sensor.py +++ b/esphome/components/sen6x/sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor +from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX import esphome.config_validation as cv from esphome.const import ( CONF_CO2, @@ -41,7 +42,10 @@ SEN6XComponent = sen6x_ns.class_( "SEN6XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice ) -CONFIG_SCHEMA = ( + +CONFIG_SCHEMA = cv.All( + cv.rename_key(CONF_VOC, CONF_VOC_INDEX, removed_in="2027.2.0", component="sen6x"), + cv.rename_key(CONF_NOX, CONF_NOX_INDEX, removed_in="2027.2.0", component="sen6x"), cv.Schema( { cv.GenerateID(): cv.declare_id(SEN6XComponent), @@ -89,12 +93,12 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_VOC): sensor.sensor_schema( + cv.Optional(CONF_VOC_INDEX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_NOX): sensor.sensor_schema( + cv.Optional(CONF_NOX_INDEX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, @@ -115,7 +119,7 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x6B)) + .extend(i2c.i2c_device_schema(0x6B)), ) SENSOR_MAP = { @@ -125,8 +129,8 @@ SENSOR_MAP = { CONF_PM_10_0: "set_pm_10_0_sensor", CONF_TEMPERATURE: "set_temperature_sensor", CONF_HUMIDITY: "set_humidity_sensor", - CONF_VOC: "set_voc_sensor", - CONF_NOX: "set_nox_sensor", + CONF_VOC_INDEX: "set_voc_sensor", + CONF_NOX_INDEX: "set_nox_sensor", CONF_CO2: "set_co2_sensor", CONF_FORMALDEHYDE: "set_hcho_sensor", } diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index e8c643f9b9..e20925f323 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -129,15 +129,16 @@ def _request_high_performance_networking(config: ConfigType) -> ConfigType: """ network.require_high_performance_networking() # Socket consumption varies by mode: - # - Server mode: 1 listening socket + 2 client connections (for handoff) + # - Server mode: 1 listening socket + 4 client connections (established connection, unproven connections, and a spare) # - Client mode: 1 outbound connection socket.consume_sockets( 1, "sendspin_websocket_server", socket.SocketType.TCP_LISTEN )(config) - socket.consume_sockets(2, "sendspin_websocket_server")(config) + socket.consume_sockets(4, "sendspin_websocket_server")(config) socket.consume_sockets(1, "sendspin_websocket_client")(config) wifi.enable_runtime_power_save_control() + wifi.enable_runtime_roaming_suppression() return config @@ -197,7 +198,7 @@ async def to_code(config: ConfigType) -> None: psram.request_external_task_stack() # sendspin-cpp library - esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.6.1") + esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.0") cg.add_define("USE_SENDSPIN", True) # for MDNS @@ -254,9 +255,6 @@ async def to_code(config: ConfigType) -> None: if psram_stack: psram.request_external_task_stack() - # Library defaults: priority 18 (one above httpd_priority 17 so the decoder is not - # starved by the HTTP server during the initial encoded-audio burst at stream start), - # decode buffer location PREFER_EXTERNAL. player_struct_fields = [ ("audio_formats", audio_format_structs), ("audio_buffer_capacity", player_cfg[CONF_BUFFER_SIZE]), diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 57709306cd..04dbab0080 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -129,6 +129,7 @@ void SendspinHub::on_request_high_performance() { #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) { wifi::global_wifi_component->request_high_performance(); + wifi::global_wifi_component->request_roaming_suppression(); } #endif } @@ -137,6 +138,7 @@ void SendspinHub::on_release_high_performance() { #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) { wifi::global_wifi_component->release_high_performance(); + wifi::global_wifi_component->release_roaming_suppression(); } #endif } @@ -177,7 +179,12 @@ std::optional SendspinHub::load_last_server_hash() { void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional volume, std::optional mute) { if (this->is_ready()) { - this->controller_role_->send_command(command, volume, mute); + sendspin::ClientCommandControllerObject obj = { + .command = command, + .volume = volume, + .muted = mute, + }; + this->controller_role_->send_command(obj); } } diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index da8a540d8d..6ad76046a1 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -321,13 +321,25 @@ _SENSOR_SCHEMA = ( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent), cv.GenerateID(): cv.declare_id(Sensor), - cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, - cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals, - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_STATE_CLASS): validate_state_class, - cv.Optional(CONF_ENTITY_CATEGORY): sensor_entity_category, - cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, - cv.Optional(CONF_EXPIRE_AFTER): cv.All( + cv.Optional( + CONF_UNIT_OF_MEASUREMENT, visibility=cv.Visibility.ADVANCED + ): validate_unit_of_measurement, + cv.Optional( + CONF_ACCURACY_DECIMALS, visibility=cv.Visibility.ADVANCED + ): validate_accuracy_decimals, + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): validate_device_class, + cv.Optional( + CONF_STATE_CLASS, visibility=cv.Visibility.ADVANCED + ): validate_state_class, + cv.Optional( + CONF_ENTITY_CATEGORY, visibility=cv.Visibility.ADVANCED + ): sensor_entity_category, + cv.Optional( + CONF_FORCE_UPDATE, default=False, visibility=cv.Visibility.ADVANCED + ): cv.boolean, + cv.Optional(CONF_EXPIRE_AFTER, visibility=cv.Visibility.ADVANCED): cv.All( cv.requires_component("mqtt"), cv.Any(None, cv.positive_time_period_milliseconds), ), diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index 04c94e9292..4b3a907416 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -89,8 +89,14 @@ void SerialProxy::dump_config() { this->dtr_pin_ != nullptr ? "configured" : "not configured"); } -void SerialProxy::configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, - uint8_t data_size) { +void SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, + uint8_t stop_bits, uint8_t data_size) { +#ifdef USE_API + if (this->port_claimed_by_other_(api_connection)) { + ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_); + return; + } +#endif ESP_LOGD(TAG, "Configuring serial proxy [%" PRIu32 "]: baud=%" PRIu32 ", flow_ctrl=%s, parity=%" PRIu8 ", stop=%" PRIu8 ", data=%" PRIu8, @@ -143,13 +149,27 @@ void SerialProxy::configure(uint32_t baudrate, bool flow_control, uint8_t parity } } -void SerialProxy::write_from_client(const uint8_t *data, size_t len) { +void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) { +#ifdef USE_API + // Bytes from a client other than the live subscriber would interleave with the + // subscriber's traffic on the wire + if (this->port_claimed_by_other_(api_connection)) { + ESP_LOGW(TAG, "Ignoring write from client without port access [%" PRIu32 "]", this->instance_index_); + return; + } +#endif if (data == nullptr || len == 0) return; this->write_array(data, len); } -void SerialProxy::set_modem_pins(uint32_t line_states) { +void SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) { +#ifdef USE_API + if (this->port_claimed_by_other_(api_connection)) { + ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_); + return; + } +#endif const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0; const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0; ESP_LOGV(TAG, "Setting modem pins [%" PRIu32 "]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr)); @@ -175,13 +195,28 @@ uart::UARTFlushResult SerialProxy::flush_port() { } #ifdef USE_API +bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) const { + return this->api_connection_ != nullptr && this->api_connection_ != api_connection && + this->api_connection_->is_connection_setup(); +} + void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type) { switch (type) { case api::enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: - if (this->api_connection_ != nullptr) { - ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + if (this->api_connection_ == api_connection) { + ESP_LOGV(TAG, "API connection is already subscribed to serial proxy [%" PRIu32 "]", this->instance_index_); return; } + if (this->api_connection_ != nullptr) { + // A living subscriber keeps exclusive access. Its connection may be dead without + // loop() having noticed yet (e.g. the client crashed and reconnected quickly); + // in that case let the new client take over instead of locking it out. + if (this->api_connection_->is_connection_setup()) { + ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + return; + } + ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription"); + } this->api_connection_ = api_connection; this->enable_loop(); ESP_LOGV(TAG, "API connection subscribed to serial proxy [%" PRIu32 "]", this->instance_index_); diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index e35fab3d42..268c1b52be 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -67,12 +67,14 @@ class SerialProxy final : public uart::UARTDevice, public Component { api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; } /// Configure UART parameters and apply them + /// @param api_connection The API connection requesting the change /// @param baudrate Baud rate in bits per second /// @param flow_control True to enable hardware flow control /// @param parity Parity setting (0=none, 1=even, 2=odd) /// @param stop_bits Number of stop bits (1 or 2) /// @param data_size Number of data bits (5-8) - void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size); + void configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, + uint8_t stop_bits, uint8_t data_size); /// Get the currently subscribed API connection (nullptr if none) api::APIConnection *get_api_connection() { return this->api_connection_; } @@ -81,12 +83,13 @@ class SerialProxy final : public uart::UARTDevice, public Component { void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type); /// Write data received from an API client to the serial device + /// @param api_connection The API connection sending the data /// @param data Pointer to data buffer /// @param len Number of bytes to write - void write_from_client(const uint8_t *data, size_t len); + void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len); /// Set modem pin states from a bitmask of SerialProxyLineStateFlag values - void set_modem_pins(uint32_t line_states); + void set_modem_pins(api::APIConnection *api_connection, uint32_t line_states); /// Get current modem pin states as a bitmask of SerialProxyLineStateFlag values uint32_t get_modem_pins() const; @@ -104,6 +107,9 @@ class SerialProxy final : public uart::UARTDevice, public Component { #ifdef USE_API /// Read from UART and send to API client (slow path with 256-byte stack buffer) void read_and_send_(size_t available); + + /// True when a live subscriber other than the given connection holds the port + bool port_claimed_by_other_(api::APIConnection *api_connection) const; #endif /// Instance index for identifying this proxy in API messages diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index d407f20a4e..87ef050bc1 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor +from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX import esphome.config_validation as cv from esphome.const import ( CONF_ALGORITHM_TUNING, @@ -35,9 +36,9 @@ CONF_HUMIDITY_SOURCE = "humidity_source" def validate_sensors(config): - if CONF_VOC not in config and CONF_NOX not in config: + if CONF_VOC_INDEX not in config and CONF_NOX_INDEX not in config: raise cv.Invalid( - f"At least one sensor is required. Define {CONF_VOC} and/or {CONF_NOX}" + f"At least one sensor is required. Define {CONF_VOC_INDEX} and/or {CONF_NOX_INDEX}" ) return config @@ -65,15 +66,17 @@ VOC_SENSOR = _gas_sensor_schema(100) NOX_SENSOR = _gas_sensor_schema(1) CONFIG_SCHEMA = cv.All( + cv.rename_key(CONF_VOC, CONF_VOC_INDEX, removed_in="2027.2.0", component="sgp4x"), + cv.rename_key(CONF_NOX, CONF_NOX_INDEX, removed_in="2027.2.0", component="sgp4x"), cv.Schema( { cv.GenerateID(): cv.declare_id(SGP4xComponent), - cv.Optional(CONF_VOC): sensor.sensor_schema( + cv.Optional(CONF_VOC_INDEX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ).extend(VOC_SENSOR), - cv.Optional(CONF_NOX): sensor.sensor_schema( + cv.Optional(CONF_NOX_INDEX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, @@ -107,11 +110,11 @@ async def to_code(config): cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) - if CONF_VOC in config: - sens = await sensor.new_sensor(config[CONF_VOC]) + if CONF_VOC_INDEX in config: + sens = await sensor.new_sensor(config[CONF_VOC_INDEX]) cg.add(var.set_voc_sensor(sens)) - if CONF_ALGORITHM_TUNING in config[CONF_VOC]: - cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING] + if CONF_ALGORITHM_TUNING in config[CONF_VOC_INDEX]: + cfg = config[CONF_VOC_INDEX][CONF_ALGORITHM_TUNING] cg.add( var.set_voc_algorithm_tuning( cfg[CONF_INDEX_OFFSET], @@ -123,11 +126,11 @@ async def to_code(config): ) ) - if CONF_NOX in config: - sens = await sensor.new_sensor(config[CONF_NOX]) + if CONF_NOX_INDEX in config: + sens = await sensor.new_sensor(config[CONF_NOX_INDEX]) cg.add(var.set_nox_sensor(sens)) - if CONF_ALGORITHM_TUNING in config[CONF_NOX]: - cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING] + if CONF_ALGORITHM_TUNING in config[CONF_NOX_INDEX]: + cfg = config[CONF_NOX_INDEX][CONF_ALGORITHM_TUNING] cg.add( var.set_nox_algorithm_tuning( cfg[CONF_INDEX_OFFSET], diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index db56bd13f0..bc6fe794a0 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -18,7 +18,7 @@ void SGP4xComponent::setup() { this->mark_failed(); return; } - this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | + this->serial_number_ = (uint64_t(raw_serial_number[0]) << 32) | (uint64_t(raw_serial_number[1]) << 16) | (uint64_t(raw_serial_number[2])); ESP_LOGD(TAG, "Serial number: %" PRIu64, this->serial_number_); @@ -32,7 +32,6 @@ void SGP4xComponent::setup() { featureset &= 0x1FF; if (featureset == SGP40_FEATURESET) { this->sgp_type_ = SGP40; - this->self_test_time_ = SPG40_SELFTEST_TIME; this->measure_time_ = SGP40_MEASURE_TIME; if (this->nox_sensor_) { ESP_LOGE(TAG, "SGP41 required for NOx, disabling NOx sensor"); @@ -42,7 +41,6 @@ void SGP4xComponent::setup() { } } else if (featureset == SGP41_FEATURESET) { this->sgp_type_ = SGP41; - this->self_test_time_ = SPG41_SELFTEST_TIME; this->measure_time_ = SGP41_MEASURE_TIME; } else { ESP_LOGD(TAG, "Unknown feature set 0x%0X", featureset); @@ -52,29 +50,6 @@ void SGP4xComponent::setup() { ESP_LOGD(TAG, "Version 0x%0X", featureset); - if (this->store_baseline_) { - // Hash with config hash, version, and serial number - // This ensures the baseline storage is cleared after OTA - // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict - uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), this->serial_number_); - this->pref_ = global_preferences->make_preference(hash, true); - - if (this->pref_.load(&this->voc_baselines_storage_)) { - this->voc_state0_ = this->voc_baselines_storage_.state0; - this->voc_state1_ = this->voc_baselines_storage_.state1; - ESP_LOGV(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); - } - - // Initialize storage timestamp - this->seconds_since_last_store_ = 0; - - if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { - ESP_LOGV(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); - voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); - } - } if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) { voc_algorithm_.set_tuning_parameters( voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours, @@ -89,6 +64,33 @@ void SGP4xComponent::setup() { nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor); } + if (this->store_baseline_) { + // Initialize storage timestamp + this->seconds_since_last_store_ = 0; + + // Hash with config hash, version, and serial number + // This ensures the baseline storage is cleared after OTA + // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict + uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), this->serial_number_); + this->pref_ = global_preferences->make_preference(hash, true); + + if (this->pref_.load(&this->voc_baselines_storage_)) { + this->voc_state0_ = this->voc_baselines_storage_.state0; + this->voc_state1_ = this->voc_baselines_storage_.state1; + + ESP_LOGV(TAG, "Loaded VOC baseline state0: %f, state1: %f", this->voc_baselines_storage_.state0, + this->voc_baselines_storage_.state1); + + if (std::isnormal(this->voc_baselines_storage_.state0) && std::isnormal(this->voc_baselines_storage_.state1)) { + ESP_LOGV(TAG, "Setting VOC baseline from save state0: %f, state1: %f", this->voc_baselines_storage_.state0, + this->voc_baselines_storage_.state1); + // Sensirion advises restoring states only after interruptions shorter than 10 minutes; with no way to know + // how long the device was off, restoring a stale state still beats a fresh 12-hour learning phase + voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); + } + } + } + this->self_test_(); /* The official spec for this sensor at @@ -112,11 +114,15 @@ void SGP4xComponent::self_test_() { this->error_code_ = COMMUNICATION_FAILED; ESP_LOGD(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); + return; } - this->set_timeout(this->self_test_time_, [this]() { + this->set_timeout(SGP4X_SELF_TEST_TIME, [this]() { uint16_t reply = 0; - if (!this->read_data(reply) || (reply != 0xD400)) { + // SGP40: MSB is 0xD4 on success, LSB is undefined; SGP41: MSB is undefined, LSB bits 0/1 flag VOC/NOx pixel + // failures + bool passed = this->read_data(reply) && (this->sgp_type_ == SGP41 ? (reply & 0x0003) == 0 : (reply >> 8) == 0xD4); + if (!passed) { this->error_code_ = SELF_TEST_FAILED; ESP_LOGW(TAG, "Self-test failed (0x%X)", reply); this->mark_failed(); @@ -134,19 +140,19 @@ void SGP4xComponent::update_gas_indices_() { if (this->nox_sensor_ != nullptr) this->nox_index_ = this->nox_algorithm_.process(this->nox_sraw_); ESP_LOGV(TAG, "VOC: %" PRId32 ", NOx: %" PRId32, this->voc_index_, this->nox_index_); - // Store baselines after defined interval or if the difference between current and stored baseline becomes too - // much + // Store baselines once the minimum interval has passed and the state has drifted from the stored copy; + // both conditions limit flash wear if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { this->voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_); - if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF || - std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) { + if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF_STATE0 || + std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF_STATE1) { this->seconds_since_last_store_ = 0; this->voc_baselines_storage_.state0 = this->voc_state0_; this->voc_baselines_storage_.state1 = this->voc_state1_; if (this->pref_.save(&this->voc_baselines_storage_)) { - ESP_LOGV(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); + ESP_LOGV(TAG, "Stored VOC baseline state0: %f, state1: %f", this->voc_baselines_storage_.state0, + this->voc_baselines_storage_.state1); } else { ESP_LOGW(TAG, "Storing VOC baselines failed"); } @@ -185,27 +191,28 @@ void SGP4xComponent::measure_raw_() { uint16_t command; uint16_t data[2]; size_t response_words; - // Use SGP40 measure command if we don't care about NOx - if (nox_sensor_ == nullptr) { + if (this->sgp_type_ == SGP40) { command = SGP40_CMD_MEASURE_RAW; response_words = 1; + } else if (this->nox_conditioning_start_.has_value() && millis() - *this->nox_conditioning_start_ < 10000) { + // SGP41 must run the NOx conditioning command for the first 10 seconds + command = SGP41_CMD_NOX_CONDITIONING; + response_words = 1; } else { - // SGP41 sensor must use NOx conditioning command for the first 10 seconds - if (this->nox_conditioning_start_.has_value() && millis() - *this->nox_conditioning_start_ < 10000) { - command = SGP41_CMD_NOX_CONDITIONING; - response_words = 1; - } else { - this->nox_conditioning_start_.reset(); - command = SGP41_CMD_MEASURE_RAW; - response_words = 2; - } + this->nox_conditioning_start_.reset(); + command = SGP41_CMD_MEASURE_RAW; + response_words = 2; + } + if (command == SGP41_CMD_NOX_CONDITIONING) { + // Conditioning requires the default parameters (compensation disabled) + data[0] = 0x8000; + data[1] = 0x6666; + } else { + // first parameter are the relative humidity ticks + data[0] = (uint16_t) std::llround((humidity * 65535) / 100); + // second parameter are the temperature ticks + data[1] = (uint16_t) (((temperature + 45) * 65535) / 175); } - uint16_t rhticks = (uint16_t) std::llround((humidity * 65535) / 100); - uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175); - // first parameter are the relative humidity ticks - data[0] = rhticks; - // secomd parameter are the temperature ticks - data[1] = tempticks; if (!this->write_command(command, data, 2)) { ESP_LOGD(TAG, "write error (%d)", this->last_error_); @@ -232,7 +239,9 @@ void SGP4xComponent::measure_raw_() { void SGP4xComponent::take_sample() { if (!this->self_test_complete_) return; - this->seconds_since_last_store_ += 1; + if (this->store_baseline_) { + this->seconds_since_last_store_ += 1; + } this->measure_raw_(); } @@ -275,7 +284,8 @@ void SGP4xComponent::dump_config() { " Type: %s\n" " Serial number: %" PRIu64 "\n" " Minimum Samples: %f", - sgp_type_ == SGP41 ? "SGP41" : "SPG40", this->serial_number_, GasIndexAlgorithm_INITIAL_BLACKOUT); + this->sgp_type_ == SGP41 ? "SGP41" : "SGP40", this->serial_number_, + GasIndexAlgorithm_INITIAL_BLACKOUT); } LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index a40188e629..2aaf06601b 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -14,9 +14,9 @@ namespace esphome::sgp4x { struct SGP4xBaselines { - int32_t state0; - int32_t state1; -} PACKED; // NOLINT + float state0; + float state1; +}; enum SgpType { SGP40, SGP41 }; @@ -39,17 +39,19 @@ static const uint16_t SGP4X_CMD_SELF_TEST = 0x280e; static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F; static const uint16_t SGP41_CMD_MEASURE_RAW = 0x2619; static const uint16_t SGP41_CMD_NOX_CONDITIONING = 0x2612; -static const uint8_t SGP41_SUBCMD_NOX_CONDITIONING = 0x12; // Shortest time interval of 3H for storing baseline values. // Prevents wear of the flash because of too many write operations const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; -static const uint16_t SPG40_SELFTEST_TIME = 250; // 250 ms for self test -static const uint16_t SPG41_SELFTEST_TIME = 320; // 320 ms for self test +static const uint16_t SGP4X_SELF_TEST_TIME = 320; // maximum self-test duration for both SGP40 and SGP41 static const uint16_t SGP40_MEASURE_TIME = 30; static const uint16_t SGP41_MEASURE_TIME = 55; -// Store anyway if the baseline difference exceeds the max storage diff value -const float MAXIMUM_STORAGE_DIFF = 50.0f; +// Once the store interval has passed, store only if the baseline drifted from the stored copy by more than these +// state0 is mean of variance estimator, hence can have larger absolute values and a larger diff threshold +const float MAXIMUM_STORAGE_DIFF_STATE0 = 50.0f; +// state1 is std of variance estimator, so it typically has smaller absolute values than state0, hence we use a smaller +// diff threshold +const float MAXIMUM_STORAGE_DIFF_STATE1 = 5.0f; class SGP4xComponent; @@ -111,7 +113,6 @@ class SGP4xComponent final : public PollingComponent, uint64_t serial_number_; bool self_test_complete_; - uint16_t self_test_time_; sensor::Sensor *voc_sensor_{nullptr}; VOCGasIndexAlgorithm voc_algorithm_; diff --git a/esphome/components/sha256/sha256.cpp b/esphome/components/sha256/sha256.cpp index 079665c959..136d0f1d58 100644 --- a/esphome/components/sha256/sha256.cpp +++ b/esphome/components/sha256/sha256.cpp @@ -1,7 +1,7 @@ #include "sha256.h" // Only compile SHA256 implementation on platforms that support it -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_HOST) #include "esphome/core/helpers.h" #include @@ -76,7 +76,7 @@ void SHA256::add(const uint8_t *data, size_t len) { mbedtls_sha256_update(&this- void SHA256::calculate() { mbedtls_sha256_finish(&this->ctx_, this->digest_); } -#elif defined(USE_ESP8266) || defined(USE_RP2040) +#elif defined(USE_ESP8266) || defined(USE_RP2) SHA256::~SHA256() = default; diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index d10d418c7a..26afe9e33e 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -3,7 +3,7 @@ #include "esphome/core/defines.h" // Only define SHA256 on platforms that support it -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_HOST) #include #include @@ -25,7 +25,7 @@ #elif defined(USE_LIBRETINY) #define USE_SHA256_MBEDTLS #include "mbedtls/sha256.h" -#elif defined(USE_ESP8266) || defined(USE_RP2040) +#elif defined(USE_ESP8266) || defined(USE_RP2) #include #elif defined(USE_HOST) #include @@ -70,7 +70,7 @@ class SHA256 final : public esphome::HashBase { // The mbedtls context for ESP32-S3 hardware SHA requires proper alignment and stack frame constraints. // See class documentation above for critical requirements. mbedtls_sha256_context ctx_{}; -#elif defined(USE_ESP8266) || defined(USE_RP2040) +#elif defined(USE_ESP8266) || defined(USE_RP2) br_sha256_context ctx_{}; bool calculated_{false}; #elif defined(USE_HOST) diff --git a/esphome/components/sm10bit_base/sm10bit_base.h b/esphome/components/sm10bit_base/sm10bit_base.h index b419b86dbf..a22c4da36e 100644 --- a/esphome/components/sm10bit_base/sm10bit_base.h +++ b/esphome/components/sm10bit_base/sm10bit_base.h @@ -27,7 +27,7 @@ class Sm10BitBase : public Component { void dump_config() override; void loop() override; - class Channel : public output::FloatOutput { + class Channel final : public output::FloatOutput { public: void set_parent(Sm10BitBase *parent) { parent_ = parent; } void set_channel(uint8_t channel) { channel_ = channel; } diff --git a/esphome/components/sm16716/sm16716.h b/esphome/components/sm16716/sm16716.h index 09deb2e8bf..8a76fd86f0 100644 --- a/esphome/components/sm16716/sm16716.h +++ b/esphome/components/sm16716/sm16716.h @@ -7,7 +7,7 @@ namespace esphome::sm16716 { -class SM16716 : public Component { +class SM16716 final : public Component { public: class Channel; @@ -25,7 +25,7 @@ class SM16716 : public Component { /// Send new values if they were updated. void loop() override; - class Channel : public output::FloatOutput { + class Channel final : public output::FloatOutput { public: void set_parent(SM16716 *parent) { parent_ = parent; } void set_channel(uint8_t channel) { channel_ = channel; } diff --git a/esphome/components/sm2135/sm2135.h b/esphome/components/sm2135/sm2135.h index 040ec14b7f..6bf77cf554 100644 --- a/esphome/components/sm2135/sm2135.h +++ b/esphome/components/sm2135/sm2135.h @@ -21,7 +21,7 @@ enum SM2135Current : uint8_t { SM2135_CURRENT_60MA = 0x0A, }; -class SM2135 : public Component { +class SM2135 final : public Component { public: class Channel; @@ -49,7 +49,7 @@ class SM2135 : public Component { /// Send new values if they were updated. void loop() override; - class Channel : public output::FloatOutput { + class Channel final : public output::FloatOutput { public: void set_parent(SM2135 *parent) { parent_ = parent; } void set_channel(uint8_t channel) { channel_ = channel; } diff --git a/esphome/components/sm2235/sm2235.h b/esphome/components/sm2235/sm2235.h index cdb754e298..dbb51945f6 100644 --- a/esphome/components/sm2235/sm2235.h +++ b/esphome/components/sm2235/sm2235.h @@ -6,7 +6,7 @@ namespace esphome::sm2235 { -class SM2235 : public sm10bit_base::Sm10BitBase { +class SM2235 final : public sm10bit_base::Sm10BitBase { public: SM2235() = default; diff --git a/esphome/components/sm2335/sm2335.h b/esphome/components/sm2335/sm2335.h index 44e0e5b03f..7c4f0269aa 100644 --- a/esphome/components/sm2335/sm2335.h +++ b/esphome/components/sm2335/sm2335.h @@ -6,7 +6,7 @@ namespace esphome::sm2335 { -class SM2335 : public sm10bit_base::Sm10BitBase { +class SM2335 final : public sm10bit_base::Sm10BitBase { public: SM2335() = default; diff --git a/esphome/components/sm300d2/sm300d2.h b/esphome/components/sm300d2/sm300d2.h index 629e758e30..87c60e92a1 100644 --- a/esphome/components/sm300d2/sm300d2.h +++ b/esphome/components/sm300d2/sm300d2.h @@ -6,7 +6,7 @@ namespace esphome::sm300d2 { -class SM300D2Sensor : public PollingComponent, public uart::UARTDevice { +class SM300D2Sensor final : public PollingComponent, public uart::UARTDevice { public: void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor) { formaldehyde_sensor_ = formaldehyde_sensor; } diff --git a/esphome/components/sml/sensor/sml_sensor.h b/esphome/components/sml/sensor/sml_sensor.h index d2f8a7743f..a73af28f66 100644 --- a/esphome/components/sml/sensor/sml_sensor.h +++ b/esphome/components/sml/sensor/sml_sensor.h @@ -4,7 +4,7 @@ namespace esphome::sml { -class SmlSensor : public SmlListener, public sensor::Sensor, public Component { +class SmlSensor final : public SmlListener, public sensor::Sensor, public Component { public: SmlSensor(std::string server_id, std::string obis_code); void publish_val(const ObisInfo &obis_info) override; diff --git a/esphome/components/sml/sml.h b/esphome/components/sml/sml.h index 60a80e3ad8..b59526648d 100644 --- a/esphome/components/sml/sml.h +++ b/esphome/components/sml/sml.h @@ -17,7 +17,7 @@ class SmlListener { virtual void publish_val(const ObisInfo &obis_info){}; }; -class Sml : public Component, public uart::UARTDevice { +class Sml final : public Component, public uart::UARTDevice { public: void register_sml_listener(SmlListener *listener); void loop() override; diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.h b/esphome/components/sml/text_sensor/sml_text_sensor.h index 6194f22349..d445d514e9 100644 --- a/esphome/components/sml/text_sensor/sml_text_sensor.h +++ b/esphome/components/sml/text_sensor/sml_text_sensor.h @@ -6,7 +6,7 @@ namespace esphome::sml { -class SmlTextSensor : public SmlListener, public text_sensor::TextSensor, public Component { +class SmlTextSensor final : public SmlListener, public text_sensor::TextSensor, public Component { public: SmlTextSensor(std::string server_id, std::string obis_code, SmlType format); void publish_val(const ObisInfo &obis_info) override; diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index b68151eeb4..55977a5caf 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -6,7 +6,7 @@ namespace esphome::smt100 { -class SMT100Component : public PollingComponent, public uart::UARTDevice { +class SMT100Component final : public PollingComponent, public uart::UARTDevice { static const uint16_t MAX_LINE_LENGTH = 31; public: diff --git a/esphome/components/sn74hc165/sn74hc165.h b/esphome/components/sn74hc165/sn74hc165.h index 596f2eb4f5..9e80aa67bf 100644 --- a/esphome/components/sn74hc165/sn74hc165.h +++ b/esphome/components/sn74hc165/sn74hc165.h @@ -8,7 +8,7 @@ namespace esphome::sn74hc165 { -class SN74HC165Component : public Component { +class SN74HC165Component final : public Component { public: SN74HC165Component() = default; @@ -40,7 +40,7 @@ class SN74HC165Component : public Component { }; /// Helper class to expose a SC74HC165 pin as an internal input GPIO pin. -class SN74HC165GPIOPin : public GPIOPin, public Parented { +class SN74HC165GPIOPin final : public GPIOPin, public Parented { public: void setup() override {} void pin_mode(gpio::Flags flags) override {} diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h index 23977e3d04..0b291b9ee5 100644 --- a/esphome/components/sn74hc595/sn74hc595.h +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -47,7 +47,7 @@ class SN74HC595Component : public Component { }; /// Helper class to expose a SC74HC595 pin as an internal output GPIO pin. -class SN74HC595GPIOPin : public GPIOPin, public Parented { +class SN74HC595GPIOPin final : public GPIOPin, public Parented { public: void setup() override {} void pin_mode(gpio::Flags flags) override {} @@ -66,7 +66,7 @@ class SN74HC595GPIOPin : public GPIOPin, public Parented { bool inverted_; }; -class SN74HC595GPIOComponent : public SN74HC595Component { +class SN74HC595GPIOComponent final : public SN74HC595Component { public: void setup() override; void set_data_pin(GPIOPin *pin) { data_pin_ = pin; } @@ -80,9 +80,9 @@ class SN74HC595GPIOComponent : public SN74HC595Component { }; #ifdef USE_SPI -class SN74HC595SPIComponent : public SN74HC595Component, - public spi::SPIDevice { +class SN74HC595SPIComponent final : public SN74HC595Component, + public spi::SPIDevice { public: void setup() override; diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index ef737c1978..686fb30d25 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -15,7 +15,7 @@ namespace esphome::sntp { /// The C library (newlib) available on ESPs only supports TZ strings that specify an offset and DST info; /// you cannot specify zone names or paths to zoneinfo files. /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html -class SNTPComponent : public time::RealTimeClock { +class SNTPComponent final : public time::RealTimeClock { public: SNTPComponent(const std::array &servers) : servers_(servers) {} diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 69a2436d3d..7d592f8ef8 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -13,7 +13,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_RTL87XX, ) from esphome.core import CORE @@ -98,7 +98,7 @@ CONFIG_SCHEMA = cv.All( [ PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX, diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 38d787c20a..cd002d9eb0 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -144,7 +144,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_IMPLEMENTATION, esp8266=IMPLEMENTATION_LWIP_TCP, esp32=IMPLEMENTATION_BSD_SOCKETS, - rp2040=IMPLEMENTATION_LWIP_TCP, + rp2=IMPLEMENTATION_LWIP_TCP, bk72xx=IMPLEMENTATION_LWIP_SOCKETS, ln882x=IMPLEMENTATION_LWIP_SOCKETS, rtl87xx=IMPLEMENTATION_LWIP_SOCKETS, diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index f9b652f14a..528d201799 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -104,7 +104,7 @@ struct iovec { size_t iov_len; }; -#if defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_RP2) // arduino-esp8266 declares a global vars called INADDR_NONE/ANY which are invalid with the define #ifdef INADDR_ANY #undef INADDR_ANY diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index c6692b0165..4fcec553fa 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -17,7 +17,7 @@ extern "C" void esphome_wake_ota_component_any_context(); #ifdef USE_ESP8266 #include // For esp_schedule() -#elif defined(USE_RP2040) +#elif defined(USE_RP2) #include // For __sev(), __wfe() #include // For add_alarm_in_ms(), cancel_alarm() #endif diff --git a/esphome/components/sonoff_d1/sonoff_d1.h b/esphome/components/sonoff_d1/sonoff_d1.h index a92877e6c8..b7fcb1efa7 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.h +++ b/esphome/components/sonoff_d1/sonoff_d1.h @@ -41,7 +41,7 @@ namespace esphome::sonoff_d1 { -class SonoffD1Output : public light::LightOutput, public uart::UARTDevice, public Component { +class SonoffD1Output final : public light::LightOutput, public uart::UARTDevice, public Component { public: // LightOutput methods light::LightTraits get_traits() override; diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index aabea62ca4..94c18421ba 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -12,7 +12,7 @@ namespace esphome::sound_level { -class SoundLevelComponent : public Component { +class SoundLevelComponent final : public Component { public: void dump_config() override; void setup() override; @@ -59,12 +59,12 @@ class SoundLevelComponent : public Component { uint32_t measurement_duration_ms_; }; -template class StartAction : public Action, public Parented { +template class StartAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->start(); } }; -template class StopAction : public Action, public Parented { +template class StopAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->stop(); } }; diff --git a/esphome/components/spa06_i2c/spa06_i2c.h b/esphome/components/spa06_i2c/spa06_i2c.h index 6b4bce3a4e..05e60cbb5d 100644 --- a/esphome/components/spa06_i2c/spa06_i2c.h +++ b/esphome/components/spa06_i2c/spa06_i2c.h @@ -4,7 +4,7 @@ namespace esphome::spa06_i2c { -class SPA06I2CComponent : public spa06_base::SPA06Component, public i2c::I2CDevice { +class SPA06I2CComponent final : public spa06_base::SPA06Component, public i2c::I2CDevice { public: bool spa_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); } bool spa_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); } diff --git a/esphome/components/spa06_spi/spa06_spi.h b/esphome/components/spa06_spi/spa06_spi.h index ffbc162d6f..56d72df620 100644 --- a/esphome/components/spa06_spi/spa06_spi.h +++ b/esphome/components/spa06_spi/spa06_spi.h @@ -5,9 +5,9 @@ namespace esphome::spa06_spi { -class SPA06SPIComponent : public spa06_base::SPA06Component, - public spi::SPIDevice { +class SPA06SPIComponent final : public spa06_base::SPA06Component, + public spi::SPIDevice { void setup() override; bool spa_read_byte(uint8_t a_register, uint8_t *data) override; bool spa_write_byte(uint8_t a_register, uint8_t data) override; diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index 9997b064d5..443588a04c 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -7,7 +7,7 @@ namespace esphome::speaker { -template class PlayAction : public Action, public Parented { +template class PlayAction final : public Action, public Parented { public: void set_data_template(std::vector (*func)(Ts...)) { this->data_.func = func; @@ -38,12 +38,12 @@ template class PlayAction : public Action, public Parente } data_; }; -template class VolumeSetAction : public Action, public Parented { +template class VolumeSetAction final : public Action, public Parented { TEMPLATABLE_VALUE(float, volume) void play(const Ts &...x) override { this->parent_->set_volume(this->volume_.value(x...)); } }; -template class MuteOnAction : public Action { +template class MuteOnAction final : public Action { public: explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {} @@ -53,7 +53,7 @@ template class MuteOnAction : public Action { Speaker *speaker_; }; -template class MuteOffAction : public Action { +template class MuteOffAction final : public Action { public: explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {} @@ -63,22 +63,22 @@ template class MuteOffAction : public Action { Speaker *speaker_; }; -template class StopAction : public Action, public Parented { +template class StopAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->stop(); } }; -template class FinishAction : public Action, public Parented { +template class FinishAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->finish(); } }; -template class IsPlayingCondition : public Condition, public Parented { +template class IsPlayingCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_running(); } }; -template class IsStoppedCondition : public Condition, public Parented { +template class IsStoppedCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_stopped(); } }; diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index 89f4707ab3..02dad15de9 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -56,7 +56,7 @@ struct InfoErrorEvent { optional decoding_err; }; -class AudioPipeline { +class AudioPipeline final { public: /// @param speaker ESPHome speaker component for pipeline's audio output /// @param buffer_size Size of the buffer in bytes between the reader and decoder diff --git a/esphome/components/speaker/media_player/automation.h b/esphome/components/speaker/media_player/automation.h index 7843399866..f9e2127993 100644 --- a/esphome/components/speaker/media_player/automation.h +++ b/esphome/components/speaker/media_player/automation.h @@ -9,7 +9,8 @@ namespace esphome::speaker { -template class PlayOnDeviceMediaAction : public Action, public Parented { +template +class PlayOnDeviceMediaAction final : public Action, public Parented { TEMPLATABLE_VALUE(audio::AudioFile *, audio_file) TEMPLATABLE_VALUE(bool, announcement) TEMPLATABLE_VALUE(bool, enqueue) diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 2d80377312..6470fb925c 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -42,11 +42,11 @@ struct VolumeRestoreState { bool is_muted; }; -class SpeakerMediaPlayer : public Component, - public media_player::MediaPlayer +class SpeakerMediaPlayer final : public Component, + public media_player::MediaPlayer #ifdef USE_OTA_STATE_LISTENER , - public ota::OTAGlobalStateListener + public ota::OTAGlobalStateListener #endif { public: diff --git a/esphome/components/speaker_source/automation.h b/esphome/components/speaker_source/automation.h index b436149a03..a03fa42477 100644 --- a/esphome/components/speaker_source/automation.h +++ b/esphome/components/speaker_source/automation.h @@ -9,7 +9,7 @@ namespace esphome::speaker_source { -template class SetPlaylistDelayAction : public Action { +template class SetPlaylistDelayAction final : public Action { public: explicit SetPlaylistDelayAction(SpeakerSourceMediaPlayer *parent) : parent_(parent) {} diff --git a/esphome/components/speaker_source/speaker_source_media_player.h b/esphome/components/speaker_source/speaker_source_media_player.h index 652390edd2..ab1f8edfed 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.h +++ b/esphome/components/speaker_source/speaker_source_media_player.h @@ -146,7 +146,7 @@ struct VolumeRestoreState { bool is_muted; }; -class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPlayer { +class SpeakerSourceMediaPlayer final : public Component, public media_player::MediaPlayer { friend struct SourceBinding; public: diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index c618d6bc5f..510b3e9621 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -7,7 +7,7 @@ namespace esphome::speed { -class SpeedFan : public Component, public fan::Fan { +class SpeedFan final : public Component, public fan::Fan { public: SpeedFan(int speed_count) : speed_count_(speed_count) {} void setup() override; diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index d1961cec59..608adc7514 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -35,7 +35,7 @@ from esphome.const import ( KEY_VARIANT, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_RP2040, + PLATFORM_RP2, PlatformFramework, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -54,7 +54,7 @@ SPIMode = spi_ns.enum("SPIMode") PLATFORM_SPI_CLOCKS = { PLATFORM_ESP8266: 40e6, PLATFORM_ESP32: 80e6, - PLATFORM_RP2040: 62.5e6, + PLATFORM_RP2: 62.5e6, } MAX_DATA_RATE_ERROR = 0.05 # Max allowable actual data rate difference from requested @@ -179,7 +179,7 @@ def get_hw_interface_list(): ]: return [["spi", "spi2"]] return [["spi", "spi2"], ["spi3"]] - if target_platform == PLATFORM_RP2040: + if target_platform == PLATFORM_RP2: return [["spi"], ["spi1"]] return [] @@ -247,7 +247,7 @@ def validate_hw_pins(spi, index=-1): if target_platform == PLATFORM_ESP32: return clk_pin_no >= 0 - if target_platform == PLATFORM_RP2040: + if target_platform == PLATFORM_RP2: if index == -1: matches = list( filter(lambda s: clk_pin_no in s[CONF_CLK_PIN], RP_SPI_PINSETS) @@ -323,7 +323,7 @@ def get_spi_interface(index): # ESP32 uses ESP-IDF SPI driver for both Arduino and IDF frameworks return ["SPI2_HOST", "SPI3_HOST"][index] # Arduino code follows - if platform == PLATFORM_RP2040: + if platform == PLATFORM_RP2: return ["&SPI", "&SPI1"][index] if index == 0: return "&SPI" @@ -349,7 +349,7 @@ SPI_SINGLE_SCHEMA = cv.All( } ), cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2]), ) @@ -500,7 +500,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( { "spi_arduino.cpp": { PlatformFramework.ESP8266_ARDUINO, - PlatformFramework.RP2040_ARDUINO, + PlatformFramework.RP2_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index e6f592c6e4..0358ed278f 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -13,11 +13,11 @@ using SPIInterface = spi_host_device_t; -#elif defined(USE_ARDUINO) +#elif defined(USE_ARDUINO) && !defined(USE_LIBRETINY) #include -#ifdef USE_RP2040 +#ifdef USE_RP2 using SPIInterface = SPIClassRP2040 *; #else using SPIInterface = SPIClass *; @@ -334,7 +334,7 @@ class SPIBus { class SPIClient; -class SPIComponent : public Component { +class SPIComponent final : public Component { public: SPIDelegate *register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, GPIOPin *cs_pin, bool release_device, bool write_only); diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index 4267fe63ce..a3e09d2800 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -11,7 +11,7 @@ class SPIDelegateHw : public SPIDelegate { : SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel) {} void begin_transaction() override { -#ifdef USE_RP2040 +#ifdef USE_RP2 SPISettings const settings(this->data_rate_, static_cast(this->bit_order_), this->mode_); #elif defined(ESP8266) // Arduino ESP8266 library has mangled values for SPI modes :-( @@ -41,7 +41,7 @@ class SPIDelegateHw : public SPIDelegate { this->channel_->transfer(*ptr); return; } -#ifdef USE_RP2040 +#ifdef USE_RP2 this->channel_->transfer(ptr, nullptr, length); #elif defined(USE_ESP8266) // ESP8266 SPI library requires the pointer to be word aligned, but the data may not be @@ -75,7 +75,7 @@ class SPIBusHw : public SPIBus { #ifdef USE_ESP32 channel->begin(Utility::get_pin_no(clk), Utility::get_pin_no(sdi), Utility::get_pin_no(sdo), -1); #endif -#ifdef USE_RP2040 +#ifdef USE_RP2 if (Utility::get_pin_no(sdi) != -1) channel->setRX(Utility::get_pin_no(sdi)); if (Utility::get_pin_no(sdo) != -1) diff --git a/esphome/components/spi_device/spi_device.h b/esphome/components/spi_device/spi_device.h index 3a2523fbab..506090fc58 100644 --- a/esphome/components/spi_device/spi_device.h +++ b/esphome/components/spi_device/spi_device.h @@ -5,9 +5,9 @@ namespace esphome::spi_device { -class SPIDeviceComponent : public Component, - public spi::SPIDevice { +class SPIDeviceComponent final : public Component, + public spi::SPIDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/spi_led_strip/spi_led_strip.h b/esphome/components/spi_led_strip/spi_led_strip.h index e2bcd5af63..20b9c25c2e 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.h +++ b/esphome/components/spi_led_strip/spi_led_strip.h @@ -8,9 +8,9 @@ namespace esphome::spi_led_strip { static const char *const TAG = "spi_led_strip"; -class SpiLedStrip : public light::AddressableLight, - public spi::SPIDevice { +class SpiLedStrip final : public light::AddressableLight, + public spi::SPIDevice { public: SpiLedStrip(uint16_t num_leds); void setup() override; diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index c6fe2e4e02..beeec96b98 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -6,7 +6,7 @@ namespace esphome::sprinkler { -template class SetDividerAction : public Action { +template class SetDividerAction final : public Action { public: explicit SetDividerAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -18,7 +18,7 @@ template class SetDividerAction : public Action { Sprinkler *sprinkler_; }; -template class SetMultiplierAction : public Action { +template class SetMultiplierAction final : public Action { public: explicit SetMultiplierAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -30,7 +30,7 @@ template class SetMultiplierAction : public Action { Sprinkler *sprinkler_; }; -template class QueueValveAction : public Action { +template class QueueValveAction final : public Action { public: explicit QueueValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -46,7 +46,7 @@ template class QueueValveAction : public Action { Sprinkler *sprinkler_; }; -template class ClearQueuedValvesAction : public Action { +template class ClearQueuedValvesAction final : public Action { public: explicit ClearQueuedValvesAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -56,7 +56,7 @@ template class ClearQueuedValvesAction : public Action { Sprinkler *sprinkler_; }; -template class SetRepeatAction : public Action { +template class SetRepeatAction final : public Action { public: explicit SetRepeatAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -68,7 +68,7 @@ template class SetRepeatAction : public Action { Sprinkler *sprinkler_; }; -template class SetRunDurationAction : public Action { +template class SetRunDurationAction final : public Action { public: explicit SetRunDurationAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -84,7 +84,7 @@ template class SetRunDurationAction : public Action { Sprinkler *sprinkler_; }; -template class StartFromQueueAction : public Action { +template class StartFromQueueAction final : public Action { public: explicit StartFromQueueAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -94,7 +94,7 @@ template class StartFromQueueAction : public Action { Sprinkler *sprinkler_; }; -template class StartFullCycleAction : public Action { +template class StartFullCycleAction final : public Action { public: explicit StartFullCycleAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -104,7 +104,7 @@ template class StartFullCycleAction : public Action { Sprinkler *sprinkler_; }; -template class StartSingleValveAction : public Action { +template class StartSingleValveAction final : public Action { public: explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -122,7 +122,7 @@ template class StartSingleValveAction : public Action { TemplatableValue valve_to_start_{}; }; -template class ShutdownAction : public Action { +template class ShutdownAction final : public Action { public: explicit ShutdownAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -132,7 +132,7 @@ template class ShutdownAction : public Action { Sprinkler *sprinkler_; }; -template class NextValveAction : public Action { +template class NextValveAction final : public Action { public: explicit NextValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -142,7 +142,7 @@ template class NextValveAction : public Action { Sprinkler *sprinkler_; }; -template class PreviousValveAction : public Action { +template class PreviousValveAction final : public Action { public: explicit PreviousValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -152,7 +152,7 @@ template class PreviousValveAction : public Action { Sprinkler *sprinkler_; }; -template class PauseAction : public Action { +template class PauseAction final : public Action { public: explicit PauseAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -162,7 +162,7 @@ template class PauseAction : public Action { Sprinkler *sprinkler_; }; -template class ResumeAction : public Action { +template class ResumeAction final : public Action { public: explicit ResumeAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -172,7 +172,7 @@ template class ResumeAction : public Action { Sprinkler *sprinkler_; }; -template class ResumeOrStartAction : public Action { +template class ResumeOrStartAction final : public Action { public: explicit ResumeOrStartAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 2598a5606a..bd610f7ad3 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -70,7 +70,7 @@ struct SprinklerValve { std::unique_ptr> valve_turn_on_automation; }; -class SprinklerControllerNumber : public number::Number, public Component { +class SprinklerControllerNumber final : public number::Number, public Component { public: void setup() override; void dump_config() override; @@ -89,7 +89,7 @@ class SprinklerControllerNumber : public number::Number, public Component { ESPPreferenceObject pref_; }; -class SprinklerControllerSwitch : public switch_::Switch, public Component { +class SprinklerControllerSwitch final : public switch_::Switch, public Component { public: SprinklerControllerSwitch(); @@ -173,7 +173,7 @@ class SprinklerValveRunRequest { SprinklerValveRunRequestOrigin origin_{USER}; }; -class Sprinkler : public Component { +class Sprinkler final : public Component { public: Sprinkler(); Sprinkler(const char *name); diff --git a/esphome/components/sps30/automation.h b/esphome/components/sps30/automation.h index e58f857eb3..ba978e7770 100644 --- a/esphome/components/sps30/automation.h +++ b/esphome/components/sps30/automation.h @@ -6,17 +6,17 @@ namespace esphome::sps30 { -template class StartFanAction : public Action, public Parented { +template class StartFanAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->start_fan_cleaning(); } }; -template class StartMeasurementAction : public Action, public Parented { +template class StartMeasurementAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->start_measurement(); } }; -template class StopMeasurementAction : public Action, public Parented { +template class StopMeasurementAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->stop_measurement(); } }; diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index ccb3e8ff41..10b89c844b 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -8,7 +8,7 @@ namespace esphome::sps30 { /// This class implements support for the Sensirion SPS30 i2c/UART Particulate Matter /// PM1.0, PM2.5, PM4, PM10 Air Quality sensors. -class SPS30Component : public PollingComponent, public sensirion_common::SensirionI2CDevice { +class SPS30Component final : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 8ff908fe7a..00c864a217 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -41,17 +41,17 @@ void I2CSSD1306::command(uint8_t value) { this->write_byte(0x00, value); } void HOT I2CSSD1306::write_display_data() { if (this->is_sh1106_() || this->is_sh1107_()) { uint32_t i = 0; + // Some panels wire their visible columns to a window of the controller RAM + // that does not start at column 0 (e.g. SH1107 M5Stack Unit OLED needs offset_x: 32). + // SH1106 keeps its historical 0x02 base column on top of any offset. + uint8_t start_column = this->offset_x_; + if (this->is_sh1106_()) { + start_column += 0x02; + } for (uint8_t page = 0; page < (uint8_t) this->get_height_internal() / 8; page++) { - this->command(0xB0 + page); // row - if (this->is_sh1106_()) { - this->command(0x02); // lower column - 0x02 is historical SH1106 value - } else { - // Other SH1107 drivers use 0x00 - // Column values dont change and it seems they can be set only once, - // but we follow SH1106 implementation and resend them - this->command(0x00); - } - this->command(0x10); // higher column + this->command(0xB0 + page); // row + this->command(start_column & 0x0F); // lower column + this->command(0x10 | (start_column >> 4)); // higher column for (uint8_t x = 0; x < (uint8_t) this->get_width_internal() / 16; x++) { uint8_t data[16]; for (uint8_t &j : data) diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.h b/esphome/components/ssd1306_i2c/ssd1306_i2c.h index 0316da0e77..54c7d86287 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.h +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.h @@ -6,7 +6,7 @@ namespace esphome::ssd1306_i2c { -class I2CSSD1306 : public ssd1306_base::SSD1306, public i2c::I2CDevice { +class I2CSSD1306 final : public ssd1306_base::SSD1306, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index 5c9369f1a2..0534deeb03 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -38,14 +38,17 @@ void SPISSD1306::command(uint8_t value) { } void HOT SPISSD1306::write_display_data() { if (this->is_sh1106_() || this->is_sh1107_()) { + // Some panels wire their visible columns to a window of the controller RAM + // that does not start at column 0 (e.g. SH1107 M5Stack Unit OLED needs offset_x: 32). + // SH1106 keeps its historical 0x02 base column on top of any offset. + uint8_t start_column = this->offset_x_; + if (this->is_sh1106_()) { + start_column += 0x02; + } for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { this->command(0xB0 + y); - if (this->is_sh1106_()) { - this->command(0x02); - } else { - this->command(0x00); - } - this->command(0x10); + this->command(start_column & 0x0F); // lower column + this->command(0x10 | (start_column >> 4)); // higher column this->dc_pin_->digital_write(true); for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x++) { this->enable(); diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.h b/esphome/components/ssd1306_spi/ssd1306_spi.h index f8346033b3..948d099d0f 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.h +++ b/esphome/components/ssd1306_spi/ssd1306_spi.h @@ -6,9 +6,9 @@ namespace esphome::ssd1306_spi { -class SPISSD1306 : public ssd1306_base::SSD1306, - public spi::SPIDevice { +class SPISSD1306 final : public ssd1306_base::SSD1306, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.h b/esphome/components/ssd1322_spi/ssd1322_spi.h index 31d17d0ef1..1ac9654109 100644 --- a/esphome/components/ssd1322_spi/ssd1322_spi.h +++ b/esphome/components/ssd1322_spi/ssd1322_spi.h @@ -6,9 +6,9 @@ namespace esphome::ssd1322_spi { -class SPISSD1322 : public ssd1322_base::SSD1322, - public spi::SPIDevice { +class SPISSD1322 final : public ssd1322_base::SSD1322, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.h b/esphome/components/ssd1325_spi/ssd1325_spi.h index 32cbb28fd8..3202eabec5 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.h +++ b/esphome/components/ssd1325_spi/ssd1325_spi.h @@ -6,9 +6,9 @@ namespace esphome::ssd1325_spi { -class SPISSD1325 : public ssd1325_base::SSD1325, - public spi::SPIDevice { +class SPISSD1325 final : public ssd1325_base::SSD1325, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.h b/esphome/components/ssd1327_i2c/ssd1327_i2c.h index f08ef94fef..75f854d3da 100644 --- a/esphome/components/ssd1327_i2c/ssd1327_i2c.h +++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.h @@ -6,7 +6,7 @@ namespace esphome::ssd1327_i2c { -class I2CSSD1327 : public ssd1327_base::SSD1327, public i2c::I2CDevice { +class I2CSSD1327 final : public ssd1327_base::SSD1327, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.h b/esphome/components/ssd1327_spi/ssd1327_spi.h index fd1ed0357f..cb7d5e2181 100644 --- a/esphome/components/ssd1327_spi/ssd1327_spi.h +++ b/esphome/components/ssd1327_spi/ssd1327_spi.h @@ -6,9 +6,9 @@ namespace esphome::ssd1327_spi { -class SPISSD1327 : public ssd1327_base::SSD1327, - public spi::SPIDevice { +class SPISSD1327 final : public ssd1327_base::SSD1327, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } diff --git a/esphome/components/ssd1331_spi/ssd1331_spi.h b/esphome/components/ssd1331_spi/ssd1331_spi.h index acdc004b26..add010712c 100644 --- a/esphome/components/ssd1331_spi/ssd1331_spi.h +++ b/esphome/components/ssd1331_spi/ssd1331_spi.h @@ -6,9 +6,9 @@ namespace esphome::ssd1331_spi { -class SPISSD1331 : public ssd1331_base::SSD1331, - public spi::SPIDevice { +class SPISSD1331 final : public ssd1331_base::SSD1331, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.h b/esphome/components/ssd1351_spi/ssd1351_spi.h index 5ce41c1f9e..307807d19f 100644 --- a/esphome/components/ssd1351_spi/ssd1351_spi.h +++ b/esphome/components/ssd1351_spi/ssd1351_spi.h @@ -6,9 +6,9 @@ namespace esphome::ssd1351_spi { -class SPISSD1351 : public ssd1351_base::SSD1351, - public spi::SPIDevice { +class SPISSD1351 final : public ssd1351_base::SSD1351, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } diff --git a/esphome/components/st7567_i2c/st7567_i2c.h b/esphome/components/st7567_i2c/st7567_i2c.h index 49489d79e6..eea3068e03 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.h +++ b/esphome/components/st7567_i2c/st7567_i2c.h @@ -6,7 +6,7 @@ namespace esphome::st7567_i2c { -class I2CST7567 : public st7567_base::ST7567, public i2c::I2CDevice { +class I2CST7567 final : public st7567_base::ST7567, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/st7567_spi/st7567_spi.h b/esphome/components/st7567_spi/st7567_spi.h index fb6f9501a9..e4699437ad 100644 --- a/esphome/components/st7567_spi/st7567_spi.h +++ b/esphome/components/st7567_spi/st7567_spi.h @@ -6,9 +6,9 @@ namespace esphome::st7567_spi { -class SPIST7567 : public st7567_base::ST7567, - public spi::SPIDevice { +class SPIST7567 final : public st7567_base::ST7567, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } diff --git a/esphome/components/st7701s/st7701s.h b/esphome/components/st7701s/st7701s.h index c65a213929..d44f8c6859 100644 --- a/esphome/components/st7701s/st7701s.h +++ b/esphome/components/st7701s/st7701s.h @@ -26,9 +26,9 @@ const uint8_t CMD2_BKSEL = 0xFF; const uint8_t CMD2_BK0[5] = {0x77, 0x01, 0x00, 0x00, 0x10}; const uint8_t ST7701S_DELAY_FLAG = 0xFF; -class ST7701S : public display::Display, - public spi::SPIDevice { +class ST7701S final : public display::Display, + public spi::SPIDevice { public: void update() override { this->do_update_(); } void setup() override; diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index 7fa0ad7335..28bc0916f9 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -31,9 +31,9 @@ enum ST7735Model { ST7735_INITR_18REDTAB = INITR_18REDTAB }; -class ST7735 : public display::DisplayBuffer, - public spi::SPIDevice { +class ST7735 final : public display::DisplayBuffer, + public spi::SPIDevice { public: ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr, bool invert_colors); diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 3f9942b117..1b7ba318a6 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -106,9 +106,9 @@ static const uint8_t ST7789_MADCTL_GS = 0x01; static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR; -class ST7789V : public display::DisplayBuffer, - public spi::SPIDevice { +class ST7789V final : public display::DisplayBuffer, + public spi::SPIDevice { public: void set_model_str(const char *model_str); void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index 71fe7aa89c..0160c5270f 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -10,9 +10,9 @@ class ST7920; using st7920_writer_t = display::DisplayWriter; -class ST7920 : public display::DisplayBuffer, - public spi::SPIDevice { +class ST7920 final : public display::DisplayBuffer, + public spi::SPIDevice { public: void set_writer(st7920_writer_t &&writer) { this->writer_local_ = writer; } void set_height(uint16_t height) { this->height_ = height; } diff --git a/esphome/components/statsd/statsd.h b/esphome/components/statsd/statsd.h index 77f3d797c5..7cbde6d743 100644 --- a/esphome/components/statsd/statsd.h +++ b/esphome/components/statsd/statsd.h @@ -27,7 +27,7 @@ namespace esphome::statsd { -class StatsdComponent : public PollingComponent { +class StatsdComponent final : public PollingComponent { public: ~StatsdComponent(); diff --git a/esphome/components/status/status_binary_sensor.h b/esphome/components/status/status_binary_sensor.h index 7e8c31d741..28cf4cd083 100644 --- a/esphome/components/status/status_binary_sensor.h +++ b/esphome/components/status/status_binary_sensor.h @@ -5,7 +5,7 @@ namespace esphome::status { -class StatusBinarySensor : public binary_sensor::BinarySensor, public PollingComponent { +class StatusBinarySensor final : public binary_sensor::BinarySensor, public PollingComponent { public: void update() override; diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index 0483669d0a..5eb0d3c085 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -7,7 +7,7 @@ namespace esphome::status_led { -class StatusLEDLightOutput : public light::LightOutput, public Component { +class StatusLEDLightOutput final : public light::LightOutput, public Component { public: void set_pin(GPIOPin *pin) { pin_ = pin; } void set_output(output::BinaryOutput *output) { output_ = output; } diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index bda144d2cd..3688dba8d6 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -5,7 +5,7 @@ namespace esphome::status_led { -class StatusLED : public Component { +class StatusLED final : public Component { public: explicit StatusLED(GPIOPin *pin); diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 9fbd0d92e6..06ef3bab37 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -37,7 +37,7 @@ class Stepper { uint32_t last_step_{0}; }; -template class SetTargetAction : public Action { +template class SetTargetAction final : public Action { public: explicit SetTargetAction(Stepper *parent) : parent_(parent) {} @@ -49,7 +49,7 @@ template class SetTargetAction : public Action { Stepper *parent_; }; -template class ReportPositionAction : public Action { +template class ReportPositionAction final : public Action { public: explicit ReportPositionAction(Stepper *parent) : parent_(parent) {} @@ -61,7 +61,7 @@ template class ReportPositionAction : public Action { Stepper *parent_; }; -template class SetSpeedAction : public Action { +template class SetSpeedAction final : public Action { public: explicit SetSpeedAction(Stepper *parent) : parent_(parent) {} @@ -77,7 +77,7 @@ template class SetSpeedAction : public Action { Stepper *parent_; }; -template class SetAccelerationAction : public Action { +template class SetAccelerationAction final : public Action { public: explicit SetAccelerationAction(Stepper *parent) : parent_(parent) {} @@ -92,7 +92,7 @@ template class SetAccelerationAction : public Action { Stepper *parent_; }; -template class SetDecelerationAction : public Action { +template class SetDecelerationAction final : public Action { public: explicit SetDecelerationAction(Stepper *parent) : parent_(parent) {} diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h index 038fa0dd80..6752cf689b 100644 --- a/esphome/components/sts3x/sts3x.h +++ b/esphome/components/sts3x/sts3x.h @@ -9,7 +9,9 @@ namespace esphome::sts3x { /// This class implements support for the ST3x-DIS family of temperature i2c sensors. -class STS3XComponent : public sensor::Sensor, public PollingComponent, public sensirion_common::SensirionI2CDevice { +class STS3XComponent final : public sensor::Sensor, + public PollingComponent, + public sensirion_common::SensirionI2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/stts22h/stts22h.h b/esphome/components/stts22h/stts22h.h index 442a263e49..d8d7a485cf 100644 --- a/esphome/components/stts22h/stts22h.h +++ b/esphome/components/stts22h/stts22h.h @@ -6,7 +6,7 @@ namespace esphome::stts22h { -class STTS22HComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { +class STTS22HComponent final : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: void setup() override; void update() override; diff --git a/esphome/components/sun/sensor/sun_sensor.h b/esphome/components/sun/sensor/sun_sensor.h index 148e5297d9..bec1a1af67 100644 --- a/esphome/components/sun/sensor/sun_sensor.h +++ b/esphome/components/sun/sensor/sun_sensor.h @@ -11,7 +11,7 @@ enum SensorType { SUN_SENSOR_AZIMUTH, }; -class SunSensor : public sensor::Sensor, public PollingComponent { +class SunSensor final : public sensor::Sensor, public PollingComponent { public: void set_parent(Sun *parent) { parent_ = parent; } void set_type(SensorType type) { type_ = type; } diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 2999c93c71..ea9e05042d 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -51,7 +51,7 @@ struct HorizontalCoordinate { } // namespace internal -class Sun { +class Sun final { public: void set_time(time::RealTimeClock *time) { time_ = time; } time::RealTimeClock *get_time() const { return time_; } @@ -78,7 +78,7 @@ class Sun { internal::GeoLocation location_; }; -class SunTrigger : public Trigger<>, public PollingComponent, public Parented { +class SunTrigger final : public Trigger<>, public PollingComponent, public Parented { public: SunTrigger() : PollingComponent(60000) {} @@ -109,7 +109,7 @@ class SunTrigger : public Trigger<>, public PollingComponent, public Parented class SunCondition : public Condition, public Parented { +template class SunCondition final : public Condition, public Parented { public: TEMPLATABLE_VALUE(double, elevation); void set_above(bool above) { above_ = above; } diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.h b/esphome/components/sun/text_sensor/sun_text_sensor.h index 65b0e358d0..a247a95e06 100644 --- a/esphome/components/sun/text_sensor/sun_text_sensor.h +++ b/esphome/components/sun/text_sensor/sun_text_sensor.h @@ -8,7 +8,7 @@ namespace esphome::sun { -class SunTextSensor : public text_sensor::TextSensor, public PollingComponent { +class SunTextSensor final : public text_sensor::TextSensor, public PollingComponent { public: void set_parent(Sun *parent) { parent_ = parent; } void set_elevation(double elevation) { elevation_ = elevation; } diff --git a/esphome/components/sun_gtil2/sun_gtil2.h b/esphome/components/sun_gtil2/sun_gtil2.h index e774fefcf8..dc3516f2b5 100644 --- a/esphome/components/sun_gtil2/sun_gtil2.h +++ b/esphome/components/sun_gtil2/sun_gtil2.h @@ -15,7 +15,7 @@ namespace esphome::sun_gtil2 { -class SunGTIL2 : public Component, public uart::UARTDevice { +class SunGTIL2 final : public Component, public uart::UARTDevice { public: float get_setup_priority() const override { return setup_priority::LATE; } void setup() override; diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 1108652e99..18b95113cc 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -78,7 +78,9 @@ _SWITCH_SCHEMA = ( cv.Optional(CONF_ON_STATE): automation.validate_automation({}), cv.Optional(CONF_ON_TURN_ON): automation.validate_automation({}), cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation({}), - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): validate_device_class, } ) ) diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index ed1f056c8b..158fb08baf 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -6,7 +6,7 @@ namespace esphome::switch_ { -template class TurnOnAction : public Action { +template class TurnOnAction final : public Action { public: explicit TurnOnAction(Switch *a_switch) : switch_(a_switch) {} @@ -16,7 +16,7 @@ template class TurnOnAction : public Action { Switch *switch_; }; -template class TurnOffAction : public Action { +template class TurnOffAction final : public Action { public: explicit TurnOffAction(Switch *a_switch) : switch_(a_switch) {} @@ -26,7 +26,7 @@ template class TurnOffAction : public Action { Switch *switch_; }; -template class ToggleAction : public Action { +template class ToggleAction final : public Action { public: explicit ToggleAction(Switch *a_switch) : switch_(a_switch) {} @@ -36,7 +36,7 @@ template class ToggleAction : public Action { Switch *switch_; }; -template class ControlAction : public Action { +template class ControlAction final : public Action { public: explicit ControlAction(Switch *a_switch) : switch_(a_switch) {} @@ -53,7 +53,7 @@ template class ControlAction : public Action { Switch *switch_; }; -template class SwitchCondition : public Condition { +template class SwitchCondition final : public Condition { public: SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {} bool check(const Ts &...x) override { return this->parent_->state == this->state_; } @@ -63,14 +63,14 @@ template class SwitchCondition : public Condition { bool state_; }; -class SwitchStateTrigger : public Trigger { +class SwitchStateTrigger final : public Trigger { public: SwitchStateTrigger(Switch *a_switch) { a_switch->add_on_state_callback([this](bool state) { this->trigger(state); }); } }; -class SwitchTurnOnTrigger : public Trigger<> { +class SwitchTurnOnTrigger final : public Trigger<> { public: SwitchTurnOnTrigger(Switch *a_switch) { a_switch->add_on_state_callback([this](bool state) { @@ -81,7 +81,7 @@ class SwitchTurnOnTrigger : public Trigger<> { } }; -class SwitchTurnOffTrigger : public Trigger<> { +class SwitchTurnOffTrigger final : public Trigger<> { public: SwitchTurnOffTrigger(Switch *a_switch) { a_switch->add_on_state_callback([this](bool state) { @@ -92,7 +92,7 @@ class SwitchTurnOffTrigger : public Trigger<> { } }; -template class SwitchPublishAction : public Action { +template class SwitchPublishAction final : public Action { public: SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.h b/esphome/components/switch/binary_sensor/switch_binary_sensor.h index 0b77cdd920..5c4184ecfa 100644 --- a/esphome/components/switch/binary_sensor/switch_binary_sensor.h +++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.h @@ -6,7 +6,7 @@ namespace esphome::switch_ { -class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component { +class SwitchBinarySensor final : public binary_sensor::BinarySensor, public Component { public: void set_source(Switch *source) { source_ = source; } void setup() override; diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h index 2721cbfbbf..4eb33abaa1 100644 --- a/esphome/components/sx126x/automation.h +++ b/esphome/components/sx126x/automation.h @@ -6,12 +6,12 @@ namespace esphome::sx126x { -template class RunImageCalAction : public Action, public Parented { +template class RunImageCalAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->run_image_cal(); } }; -template class SendPacketAction : public Action, public Parented { +template class SendPacketAction final : public Action, public Parented { public: void set_data_template(std::vector (*func)(Ts...)) { this->data_.func = func; @@ -43,23 +43,23 @@ template class SendPacketAction : public Action, public P } data_; }; -template class SetModeTxAction : public Action, public Parented { +template class SetModeTxAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->set_mode_tx(); } }; -template class SetModeRxAction : public Action, public Parented { +template class SetModeRxAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->set_mode_rx(); } }; -template class SetModeSleepAction : public Action, public Parented { +template class SetModeSleepAction final : public Action, public Parented { public: TEMPLATABLE_VALUE(bool, cold) void play(const Ts &...x) override { this->parent_->set_mode_sleep(this->cold_.value(x...)); } }; -template class SetModeStandbyAction : public Action, public Parented { +template class SetModeStandbyAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->set_mode_standby(STDBY_XOSC); } }; diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.h b/esphome/components/sx126x/packet_transport/sx126x_transport.h index 7590e35c28..ccd20755e5 100644 --- a/esphome/components/sx126x/packet_transport/sx126x_transport.h +++ b/esphome/components/sx126x/packet_transport/sx126x_transport.h @@ -7,7 +7,7 @@ namespace esphome::sx126x { -class SX126xTransport : public packet_transport::PacketTransport, public Parented, public SX126xListener { +class SX126xTransport final : public packet_transport::PacketTransport, public Parented, public SX126xListener { public: void setup() override; void on_packet(const std::vector &packet, float rssi, float snr) override; diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index 6816084df0..b3dfe6590a 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -53,9 +53,9 @@ class SX126xListener { virtual void on_packet(const std::vector &packet, float rssi, float snr) = 0; }; -class SX126x : public Component, - public spi::SPIDevice { +class SX126x final : public Component, + public spi::SPIDevice { public: size_t get_max_packet_size(); float get_setup_priority() const override { return setup_priority::PROCESSOR; } diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h index 7a2eb7ee8d..f6a4537e23 100644 --- a/esphome/components/sx127x/automation.h +++ b/esphome/components/sx127x/automation.h @@ -6,12 +6,12 @@ namespace esphome::sx127x { -template class RunImageCalAction : public Action, public Parented { +template class RunImageCalAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->run_image_cal(); } }; -template class SendPacketAction : public Action, public Parented { +template class SendPacketAction final : public Action, public Parented { public: void set_data_template(std::vector (*func)(Ts...)) { this->data_.func = func; @@ -43,22 +43,22 @@ template class SendPacketAction : public Action, public P } data_; }; -template class SetModeTxAction : public Action, public Parented { +template class SetModeTxAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->set_mode_tx(); } }; -template class SetModeRxAction : public Action, public Parented { +template class SetModeRxAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->set_mode_rx(); } }; -template class SetModeSleepAction : public Action, public Parented { +template class SetModeSleepAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->set_mode_sleep(); } }; -template class SetModeStandbyAction : public Action, public Parented { +template class SetModeStandbyAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->set_mode_standby(); } }; diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.h b/esphome/components/sx127x/packet_transport/sx127x_transport.h index 5dcfe02c33..fb38fc15bc 100644 --- a/esphome/components/sx127x/packet_transport/sx127x_transport.h +++ b/esphome/components/sx127x/packet_transport/sx127x_transport.h @@ -7,7 +7,7 @@ namespace esphome::sx127x { -class SX127xTransport : public packet_transport::PacketTransport, public Parented, public SX127xListener { +class SX127xTransport final : public packet_transport::PacketTransport, public Parented, public SX127xListener { public: void setup() override; void on_packet(const std::vector &packet, float rssi, float snr) override; diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h index 376c987ed1..070a6eeb96 100644 --- a/esphome/components/sx127x/sx127x.h +++ b/esphome/components/sx127x/sx127x.h @@ -41,9 +41,9 @@ class SX127xListener { virtual void on_packet(const std::vector &packet, float rssi, float snr) = 0; }; -class SX127x : public Component, - public spi::SPIDevice { +class SX127x final : public Component, + public spi::SPIDevice { public: size_t get_max_packet_size(); float get_setup_priority() const override { return setup_priority::PROCESSOR; } diff --git a/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h index bcd8901530..5d26a37283 100644 --- a/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h +++ b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h @@ -5,7 +5,7 @@ namespace esphome::sx1509 { -class SX1509BinarySensor : public sx1509::SX1509Processor, public binary_sensor::BinarySensor { +class SX1509BinarySensor final : public sx1509::SX1509Processor, public binary_sensor::BinarySensor { public: void set_row_col(uint8_t row, uint8_t col) { this->key_ = (1 << (col + 8)) | (1 << row); } void process(uint16_t data) override { this->publish_state(static_cast(data == key_)); } diff --git a/esphome/components/sx1509/output/sx1509_float_output.h b/esphome/components/sx1509/output/sx1509_float_output.h index ee53cef637..8790b2fcd7 100644 --- a/esphome/components/sx1509/output/sx1509_float_output.h +++ b/esphome/components/sx1509/output/sx1509_float_output.h @@ -7,7 +7,7 @@ namespace esphome::sx1509 { class SX1509Component; -class SX1509FloatOutputChannel : public output::FloatOutput, public Component { +class SX1509FloatOutputChannel final : public output::FloatOutput, public Component { public: void set_parent(SX1509Component *parent) { this->parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index 35883eed5b..c7aed2cddd 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -28,12 +28,12 @@ class SX1509Processor { virtual void process(uint16_t data){}; }; -class SX1509KeyTrigger : public Trigger {}; +class SX1509KeyTrigger final : public Trigger {}; -class SX1509Component : public Component, - public i2c::I2CDevice, - public gpio_expander::CachedGpioExpander, - public key_provider::KeyProvider { +class SX1509Component final : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander, + public key_provider::KeyProvider { public: SX1509Component() = default; diff --git a/esphome/components/sx1509/sx1509_gpio_pin.h b/esphome/components/sx1509/sx1509_gpio_pin.h index 9dcad37b27..3bd3d90bd9 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.h +++ b/esphome/components/sx1509/sx1509_gpio_pin.h @@ -6,7 +6,7 @@ namespace esphome::sx1509 { class SX1509Component; -class SX1509GPIOPin : public GPIOPin { +class SX1509GPIOPin final : public GPIOPin { public: void setup() override; void pin_mode(gpio::Flags flags) override; diff --git a/esphome/components/sy6970/binary_sensor/sy6970_binary_sensor.h b/esphome/components/sy6970/binary_sensor/sy6970_binary_sensor.h index 4a374d7e3d..b94c89d123 100644 --- a/esphome/components/sy6970/binary_sensor/sy6970_binary_sensor.h +++ b/esphome/components/sy6970/binary_sensor/sy6970_binary_sensor.h @@ -6,7 +6,7 @@ namespace esphome::sy6970 { template -class StatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor { +class StatusBinarySensor final : public SY6970Listener, public binary_sensor::BinarySensor { public: void on_data(const SY6970Data &data) override { uint8_t value = (data.registers[REG] >> SHIFT) & MASK; @@ -24,7 +24,7 @@ class InverseStatusBinarySensor : public SY6970Listener, public binary_sensor::B }; // Custom binary sensor for charging (true when pre-charge or fast charge) -class SY6970ChargingBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor { +class SY6970ChargingBinarySensor final : public SY6970Listener, public binary_sensor::BinarySensor { public: void on_data(const SY6970Data &data) override { uint8_t chrg_stat = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03; diff --git a/esphome/components/sy6970/sensor/sy6970_sensor.h b/esphome/components/sy6970/sensor/sy6970_sensor.h index f912d726b2..61abbc3e36 100644 --- a/esphome/components/sy6970/sensor/sy6970_sensor.h +++ b/esphome/components/sy6970/sensor/sy6970_sensor.h @@ -34,7 +34,7 @@ using SY6970SystemVoltageSensor = VoltageSensor; // Precharge current sensor needs special handling (bit shift) -class SY6970PrechargeCurrentSensor : public SY6970Listener, public sensor::Sensor { +class SY6970PrechargeCurrentSensor final : public SY6970Listener, public sensor::Sensor { public: void on_data(const SY6970Data &data) override { uint8_t iprechg = (data.registers[SY6970_REG_PRECHARGE_CURRENT] >> 4) & 0x0F; diff --git a/esphome/components/sy6970/sy6970.h b/esphome/components/sy6970/sy6970.h index 2225dd781b..06f0615ab4 100644 --- a/esphome/components/sy6970/sy6970.h +++ b/esphome/components/sy6970/sy6970.h @@ -73,7 +73,7 @@ class SY6970Listener { virtual void on_data(const SY6970Data &data) = 0; }; -class SY6970Component : public PollingComponent, public i2c::I2CDevice { +class SY6970Component final : public PollingComponent, public i2c::I2CDevice { public: SY6970Component(bool led_enabled, uint16_t input_current_limit, uint16_t charge_voltage, uint16_t charge_current, uint16_t precharge_current, bool charge_enabled, bool enable_adc) diff --git a/esphome/components/sy6970/text_sensor/sy6970_text_sensor.h b/esphome/components/sy6970/text_sensor/sy6970_text_sensor.h index 665c5eca64..e569bd0b90 100644 --- a/esphome/components/sy6970/text_sensor/sy6970_text_sensor.h +++ b/esphome/components/sy6970/text_sensor/sy6970_text_sensor.h @@ -6,7 +6,7 @@ namespace esphome::sy6970 { // Bus status text sensor -class SY6970BusStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor { +class SY6970BusStatusTextSensor final : public SY6970Listener, public text_sensor::TextSensor { public: void on_data(const SY6970Data &data) override { uint8_t status = (data.registers[SY6970_REG_STATUS] >> 5) & 0x07; @@ -40,7 +40,7 @@ class SY6970BusStatusTextSensor : public SY6970Listener, public text_sensor::Tex }; // Charge status text sensor -class SY6970ChargeStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor { +class SY6970ChargeStatusTextSensor final : public SY6970Listener, public text_sensor::TextSensor { public: void on_data(const SY6970Data &data) override { uint8_t status = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03; @@ -66,7 +66,7 @@ class SY6970ChargeStatusTextSensor : public SY6970Listener, public text_sensor:: }; // NTC status text sensor -class SY6970NtcStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor { +class SY6970NtcStatusTextSensor final : public SY6970Listener, public text_sensor::TextSensor { public: void on_data(const SY6970Data &data) override { uint8_t status = data.registers[SY6970_REG_FAULT] & 0x07; diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index be4fa91436..4a76f9ac62 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -7,7 +7,7 @@ #ifdef USE_NETWORK namespace esphome::syslog { -class Syslog : public Component, public Parented { +class Syslog final : public Component, public Parented { public: Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {} void setup() override; diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h index 0c2088f7b0..7ad2ae23c7 100644 --- a/esphome/components/t6615/t6615.h +++ b/esphome/components/t6615/t6615.h @@ -19,7 +19,7 @@ enum class T6615Command : uint8_t { SET_ELEVATION, }; -class T6615Component : public PollingComponent, public uart::UARTDevice { +class T6615Component final : public PollingComponent, public uart::UARTDevice { public: void loop() override; void update() override; diff --git a/esphome/components/tc74/tc74.h b/esphome/components/tc74/tc74.h index 4a53f39bc1..c48303c009 100644 --- a/esphome/components/tc74/tc74.h +++ b/esphome/components/tc74/tc74.h @@ -6,7 +6,7 @@ namespace esphome::tc74 { -class TC74Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { +class TC74Component final : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { public: /// Setup the sensor and check connection. void setup() override; diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index f0417ac7f7..a98c226d32 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -8,7 +8,7 @@ namespace esphome::tca9548a { static const uint8_t TCA9548A_DISABLE_CHANNELS_COMMAND = 0x00; class TCA9548AComponent; -class TCA9548AChannel : public i2c::I2CBus { +class TCA9548AChannel final : public i2c::I2CBus { public: void set_channel(uint8_t channel) { channel_ = channel; } void set_parent(TCA9548AComponent *parent) { parent_ = parent; } @@ -21,7 +21,7 @@ class TCA9548AChannel : public i2c::I2CBus { TCA9548AComponent *parent_; }; -class TCA9548AComponent : public Component, public i2c::I2CDevice { +class TCA9548AComponent final : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/tca9555/tca9555.h b/esphome/components/tca9555/tca9555.h index 19773a0e93..50037cbe92 100644 --- a/esphome/components/tca9555/tca9555.h +++ b/esphome/components/tca9555/tca9555.h @@ -7,9 +7,9 @@ namespace esphome::tca9555 { -class TCA9555Component : public Component, - public i2c::I2CDevice, - public gpio_expander::CachedGpioExpander { +class TCA9555Component final : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: TCA9555Component() = default; @@ -47,7 +47,7 @@ class TCA9555Component : public Component, }; /// Helper class to expose a TCA9555 pin as an internal input GPIO pin. -class TCA9555GPIOPin : public GPIOPin, public Parented { +class TCA9555GPIOPin final : public GPIOPin, public Parented { public: void setup() override; void pin_mode(gpio::Flags flags) override; diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h index 0aef2decc8..21eb618947 100644 --- a/esphome/components/tcl112/tcl112.h +++ b/esphome/components/tcl112/tcl112.h @@ -8,7 +8,7 @@ namespace esphome::tcl112 { const float TCL112_TEMP_MAX = 31.0; const float TCL112_TEMP_MIN = 16.0; -class Tcl112Climate : public climate_ir::ClimateIR { +class Tcl112Climate final : public climate_ir::ClimateIR { public: Tcl112Climate() : climate_ir::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f, true, true, diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index 15e4fae52f..79b49bc810 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -35,7 +35,7 @@ enum TCS34725Gain { TCS34725_GAIN_60X = 0x03, }; -class TCS34725Component : public PollingComponent, public i2c::I2CDevice { +class TCS34725Component final : public PollingComponent, public i2c::I2CDevice { public: void set_integration_time(TCS34725IntegrationTime integration_time); void set_gain(TCS34725Gain gain); diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h index 4a08291318..bbd63a4e2b 100644 --- a/esphome/components/tee501/tee501.h +++ b/esphome/components/tee501/tee501.h @@ -7,7 +7,7 @@ namespace esphome::tee501 { /// This class implements support for the tee501 of temperature i2c sensors. -class TEE501Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { +class TEE501Component final : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; diff --git a/esphome/components/teleinfo/sensor/teleinfo_sensor.h b/esphome/components/teleinfo/sensor/teleinfo_sensor.h index 37736c4e73..f4a27fa08b 100644 --- a/esphome/components/teleinfo/sensor/teleinfo_sensor.h +++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.h @@ -4,7 +4,7 @@ namespace esphome::teleinfo { -class TeleInfoSensor : public TeleInfoListener, public sensor::Sensor, public Component { +class TeleInfoSensor final : public TeleInfoListener, public sensor::Sensor, public Component { public: TeleInfoSensor(const char *tag); void publish_val(const std::string &val) override; diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp index cd2ddbbb38..e00895d162 100644 --- a/esphome/components/teleinfo/teleinfo.cpp +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -57,7 +57,7 @@ bool TeleInfo::read_chars_until_(bool drop, uint8_t c) { */ if (buf_index_ >= (MAX_BUF_SIZE - 1)) { ESP_LOGW(TAG, "Internal buffer full"); - state_ = OFF; + state_ = STATE_OFF; return false; } buf_[buf_index_++] = received; @@ -65,18 +65,18 @@ bool TeleInfo::read_chars_until_(bool drop, uint8_t c) { return false; } -void TeleInfo::setup() { state_ = OFF; } +void TeleInfo::setup() { state_ = STATE_OFF; } void TeleInfo::update() { - if (state_ == OFF) { + if (state_ == STATE_OFF) { buf_index_ = 0; - state_ = ON; + state_ = STATE_ON; } } void TeleInfo::loop() { switch (state_) { - case OFF: + case STATE_OFF: break; - case ON: + case STATE_ON: /* Dequeue chars until start frame (0x2) */ if (read_chars_until_(true, 0x2)) state_ = START_FRAME_RECEIVED; @@ -173,7 +173,7 @@ void TeleInfo::loop() { publish_value_(std::string(tag_), std::string(val_)); } - state_ = OFF; + state_ = STATE_OFF; break; } } diff --git a/esphome/components/teleinfo/teleinfo.h b/esphome/components/teleinfo/teleinfo.h index eeab3b5103..4aab3bf2cd 100644 --- a/esphome/components/teleinfo/teleinfo.h +++ b/esphome/components/teleinfo/teleinfo.h @@ -20,7 +20,7 @@ class TeleInfoListener { std::string tag; virtual void publish_val(const std::string &val){}; }; -class TeleInfo : public PollingComponent, public uart::UARTDevice { +class TeleInfo final : public PollingComponent, public uart::UARTDevice { public: TeleInfo(bool historical_mode); void register_teleinfo_listener(TeleInfoListener *listener); @@ -40,11 +40,11 @@ class TeleInfo : public PollingComponent, public uart::UARTDevice { char val_[MAX_VAL_SIZE]; char timestamp_[MAX_TIMESTAMP_SIZE]; enum State { - OFF, - ON, + STATE_OFF, + STATE_ON, START_FRAME_RECEIVED, END_FRAME_RECEIVED, - } state_{OFF}; + } state_{STATE_OFF}; bool read_chars_until_(bool drop, uint8_t c); bool check_crc_(const char *grp, const char *grp_end); void publish_value_(const std::string &tag, const std::string &val); diff --git a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h index f4c04a03a0..24ec00e671 100644 --- a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h +++ b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h @@ -3,7 +3,7 @@ #include "esphome/components/text_sensor/text_sensor.h" namespace esphome::teleinfo { -class TeleInfoTextSensor : public TeleInfoListener, public text_sensor::TextSensor, public Component { +class TeleInfoTextSensor final : public TeleInfoListener, public text_sensor::TextSensor, public Component { public: TeleInfoTextSensor(const char *tag); void publish_val(const std::string &val) override; diff --git a/esphome/components/tem3200/tem3200.h b/esphome/components/tem3200/tem3200.h index 5c73a25fbb..ad8d0154f3 100644 --- a/esphome/components/tem3200/tem3200.h +++ b/esphome/components/tem3200/tem3200.h @@ -7,7 +7,7 @@ namespace esphome::tem3200 { /// This class implements support for the tem3200 pressure and temperature i2c sensors. -class TEM3200Component : public PollingComponent, public i2c::I2CDevice { +class TEM3200Component final : public PollingComponent, public i2c::I2CDevice { public: void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_raw_pressure_sensor(sensor::Sensor *raw_pressure_sensor) { diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 01a57cbaa1..a3f4999a8f 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -144,7 +144,9 @@ _TEXT_SENSOR_SCHEMA = ( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor), cv.GenerateID(): cv.declare_id(TextSensor), - cv.Optional(CONF_DEVICE_CLASS): validate_device_class, + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}), diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 4e623942ac..6a52348ae9 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -10,7 +10,7 @@ #ifdef USE_ESP8266 #include "sys/time.h" #endif -#if defined(USE_RP2040) || defined(USE_ZEPHYR) +#if defined(USE_RP2) || defined(USE_ZEPHYR) #include #endif #include diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index 4a5c03ace4..cf0c5fca19 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -60,40 +60,79 @@ def validate_calibration(calibration_config): return calibration_config -CALIBRATION_SCHEMA = cv.All( - cv.Schema( - { - cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095), - cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095), - cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=4095), - cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=4095), - } - ), - validate_calibration, -) +def option_with_default(option: str, defaults: dict, required: bool = False): + if option in defaults or not required: + return cv.Optional(option, default=defaults.get(option, cv.UNDEFINED)) + return cv.Required(option) -def touchscreen_schema(default_touch_timeout=cv.UNDEFINED, calibration_required=False): - calibration = ( - cv.Required(CONF_CALIBRATION) - if calibration_required - else cv.Optional(CONF_CALIBRATION) - ) +_CALIBRATION_KEYS = {CONF_X_MIN, CONF_X_MAX, CONF_Y_MIN, CONF_Y_MAX} +_TRANSFORM_KEYS = {CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y} + + +def _calibration_schema(defaults: dict, required: bool) -> dict: + """ + Generate Calibration schema. If defaults are provided for all suboptions, + the entire calibration config is optional with a populated default value. + Otherwise, it's optional or required as specified. + """ + if _CALIBRATION_KEYS.issubset(defaults): + key = cv.Optional( + CONF_CALIBRATION, + default={k: v for k, v in defaults.items() if k in _CALIBRATION_KEYS}, + ) + elif required: + key = cv.Required(CONF_CALIBRATION) + else: + key = cv.Optional(CONF_CALIBRATION) + return { + key: cv.All( + cv.Schema( + { + option_with_default(x, defaults, True): cv.int_range( + min=0, max=4095 + ) + for x in _CALIBRATION_KEYS + } + ), + validate_calibration, + ) + } + + +def _transform_schema(defaults: dict) -> dict: + if _TRANSFORM_KEYS.issubset(defaults): + key = cv.Optional( + CONF_TRANSFORM, + default={k: v for k, v in defaults.items() if k in _TRANSFORM_KEYS}, + ) + else: + key = cv.Optional(CONF_TRANSFORM) + return { + key: cv.Schema( + { + cv.Optional(x, default=defaults.get(x, False)): cv.boolean + for x in _TRANSFORM_KEYS + } + ) + } + + +def touchscreen_schema( + default_touch_timeout=cv.UNDEFINED, + calibration_required=False, + defaults: dict = None, +) -> cv.Schema: + defaults = defaults or {} return cv.Schema( { cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display), - cv.Optional(CONF_TRANSFORM): cv.Schema( - { - cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, - cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, - cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, - } - ), cv.Optional(CONF_TOUCH_TIMEOUT, default=default_touch_timeout): cv.All( cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=65535)), ), - calibration: CALIBRATION_SCHEMA, + **_transform_schema(defaults), + **_calibration_schema(defaults, calibration_required), cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True), cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True), diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 4ea32e26a3..7e3701bb07 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -49,7 +49,7 @@ IDFUARTComponent = uart_ns.class_("IDFUARTComponent", UARTComponent, cg.Componen ESP8266UartComponent = uart_ns.class_( "ESP8266UartComponent", UARTComponent, cg.Component ) -RP2040UartComponent = uart_ns.class_("RP2040UartComponent", UARTComponent, cg.Component) +RP2UartComponent = uart_ns.class_("RP2UartComponent", UARTComponent, cg.Component) LibreTinyUARTComponent = uart_ns.class_( "LibreTinyUARTComponent", UARTComponent, cg.Component ) @@ -59,7 +59,7 @@ HostUartComponent = uart_ns.class_("HostUartComponent", UARTComponent, cg.Compon NATIVE_UART_CLASSES = ( str(IDFUARTComponent), str(ESP8266UartComponent), - str(RP2040UartComponent), + str(RP2UartComponent), str(LibreTinyUARTComponent), ) @@ -157,8 +157,8 @@ def _uart_declare_type(value): return cv.declare_id(ESP8266UartComponent)(value) if CORE.is_esp32: return cv.declare_id(IDFUARTComponent)(value) - if CORE.is_rp2040: - return cv.declare_id(RP2040UartComponent)(value) + if CORE.is_rp2: + return cv.declare_id(RP2UartComponent)(value) if CORE.is_libretiny: return cv.declare_id(LibreTinyUARTComponent)(value) if CORE.is_host: @@ -529,7 +529,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( }, "uart_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "uart_component_host.cpp": {PlatformFramework.HOST_NATIVE}, - "uart_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "uart_component_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, "uart_component_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index c99caac97b..e5a9fa7c7b 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -7,7 +7,7 @@ namespace esphome::uart { -template class UARTWriteAction : public Action, public Parented { +template class UARTWriteAction final : public Action, public Parented { public: void set_data_template(std::vector (*func)(Ts...)) { // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers diff --git a/esphome/components/uart/button/uart_button.h b/esphome/components/uart/button/uart_button.h index 2b530d3c4b..47f45d4899 100644 --- a/esphome/components/uart/button/uart_button.h +++ b/esphome/components/uart/button/uart_button.h @@ -8,7 +8,7 @@ namespace esphome::uart { -class UARTButton : public button::Button, public UARTDevice, public Component { +class UARTButton final : public button::Button, public UARTDevice, public Component { public: void set_data(std::vector &&data) { this->data_ = std::move(data); } void set_data(std::initializer_list data) { this->data_ = std::vector(data); } diff --git a/esphome/components/uart/event/uart_event.h b/esphome/components/uart/event/uart_event.h index 8a00b5894b..3960ffd5bb 100644 --- a/esphome/components/uart/event/uart_event.h +++ b/esphome/components/uart/event/uart_event.h @@ -7,7 +7,7 @@ namespace esphome::uart { -class UARTEvent : public event::Event, public UARTDevice, public Component { +class UARTEvent final : public event::Event, public UARTDevice, public Component { public: void setup() override; void loop() override; diff --git a/esphome/components/uart/packet_transport/uart_transport.h b/esphome/components/uart/packet_transport/uart_transport.h index 1c92af536e..b1ce8ac590 100644 --- a/esphome/components/uart/packet_transport/uart_transport.h +++ b/esphome/components/uart/packet_transport/uart_transport.h @@ -20,7 +20,7 @@ static const uint16_t MAX_PACKET_SIZE = 508; static const uint8_t FLAG_BYTE = 0x7E; static const uint8_t CONTROL_BYTE = 0x7D; -class UARTTransport : public packet_transport::PacketTransport, public UARTDevice { +class UARTTransport final : public packet_transport::PacketTransport, public UARTDevice { public: void loop() override; float get_setup_priority() const override { return setup_priority::PROCESSOR; } diff --git a/esphome/components/uart/switch/uart_switch.h b/esphome/components/uart/switch/uart_switch.h index 5730fc9b4b..c924c7d4e5 100644 --- a/esphome/components/uart/switch/uart_switch.h +++ b/esphome/components/uart/switch/uart_switch.h @@ -9,7 +9,7 @@ namespace esphome::uart { -class UARTSwitch : public switch_::Switch, public UARTDevice, public Component { +class UARTSwitch final : public switch_::Switch, public UARTDevice, public Component { public: void loop() override; diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index afd3ad5777..3e52531791 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -178,7 +178,7 @@ class UARTComponent { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - virtual void load_settings(bool dump_config){}; + virtual void load_settings(bool dump_config) = 0; /** * Load the UART settings. @@ -190,7 +190,7 @@ class UARTComponent { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - virtual void load_settings(){}; + void load_settings() { this->load_settings(true); } #endif // USE_ESP8266 || USE_ESP32 #ifdef USE_UART_DEBUGGER diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index 7f844d9b65..469885b6b6 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -46,7 +46,7 @@ class ESP8266SoftwareSerial { ISRInternalGPIOPin rx_pin_; }; -class ESP8266UartComponent : public UARTComponent, public Component { +class ESP8266UartComponent final : public UARTComponent, public Component { public: void setup() override; void dump_config() override; @@ -75,7 +75,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ void load_settings(bool dump_config) override; - void load_settings() override { this->load_settings(true); } + using UARTComponent::load_settings; // also bring in the no-arg overload for convenience protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index ec4f2884b2..649dd3aa46 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -16,7 +16,7 @@ namespace esphome::uart { /// Thread safety: All public methods must only be called from the main loop. /// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's /// peek byte state (has_peek_/peek_byte_) is not synchronized. -class IDFUARTComponent : public UARTComponent, public Component { +class IDFUARTComponent final : public UARTComponent, public Component { public: void setup() override; void dump_config() override; @@ -50,7 +50,7 @@ class IDFUARTComponent : public UARTComponent, public Component { * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ void load_settings(bool dump_config) override; - void load_settings() override { this->load_settings(true); } + using UARTComponent::load_settings; // also bring in the no-arg overload for convenience protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index a47e5649be..bca62debf1 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -8,7 +8,7 @@ namespace esphome::uart { -class HostUartComponent : public UARTComponent, public Component { +class HostUartComponent final : public UARTComponent, public Component { public: virtual ~HostUartComponent(); void setup() override; diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 4172e7c164..fbf0c20ded 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -18,7 +18,7 @@ namespace esphome::uart { static const char *const TAG = "uart.lt"; -static const char *UART_TYPE[] = { +static const char *const UART_TYPE[] = { "hardware", "software", }; @@ -45,19 +45,19 @@ uint16_t LibreTinyUARTComponent::get_config() { } void LibreTinyUARTComponent::setup() { - int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); - int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); - bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); - bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted(); + int16_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); + int16_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); - auto shouldFallbackToSoftwareSerial = [&]() -> bool { - auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool { + auto should_fallback_to_software_serial = [&]() -> bool { + auto has_flags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool { return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE; }; - if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) || - hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) { + if (has_flags(this->tx_pin_, + gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) || + has_flags(this->rx_pin_, + gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) { #if LT_ARD_HAS_SOFTSERIAL - ESP_LOGI(TAG, "Pins has flags set. Using Software Serial"); + ESP_LOGI(TAG, "Pins have flags set. Using Software Serial"); return true; #else ESP_LOGW(TAG, "Pin flags are set but not supported for hardware serial. Ignoring"); @@ -66,25 +66,26 @@ void LibreTinyUARTComponent::setup() { return false; }; - if (false) + if (false) { // NOLINT(readability-simplify-boolean-expr) return; + } #if LT_HW_UART0 else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX) && - !shouldFallbackToSoftwareSerial()) { + !should_fallback_to_software_serial()) { this->serial_ = &Serial0; this->hardware_idx_ = 0; } #endif #if LT_HW_UART1 else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX) && - !shouldFallbackToSoftwareSerial()) { + !should_fallback_to_software_serial()) { this->serial_ = &Serial1; this->hardware_idx_ = 1; } #endif #if LT_HW_UART2 else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX) && - !shouldFallbackToSoftwareSerial()) { + !should_fallback_to_software_serial()) { this->serial_ = &Serial2; this->hardware_idx_ = 2; } @@ -97,6 +98,8 @@ void LibreTinyUARTComponent::setup() { if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { this->tx_pin_->setup(); } + bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); + bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted(); this->serial_ = new SoftwareSerial(rx_pin, tx_pin, rx_inverted || tx_inverted); #else this->serial_ = &Serial; @@ -133,7 +136,7 @@ void LibreTinyUARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); } ESP_LOGCONFIG(TAG, - " Baud Rate: %u baud\n" + " Baud Rate: %" PRIu32 " baud\n" " Data Bits: %u\n" " Parity: %s\n" " Stop bits: %u", diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 872ea86601..aa13a01392 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -10,7 +10,7 @@ namespace esphome::uart { -class LibreTinyUARTComponent : public UARTComponent, public Component { +class LibreTinyUARTComponent final : public UARTComponent, public Component { public: void setup() override; void dump_config() override; diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2.cpp similarity index 91% rename from esphome/components/uart/uart_component_rp2040.cpp rename to esphome/components/uart/uart_component_rp2.cpp index 1aaf98dc84..9cc3009a22 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2.cpp @@ -1,5 +1,5 @@ -#ifdef USE_RP2040 -#include "uart_component_rp2040.h" +#ifdef USE_RP2 +#include "uart_component_rp2.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -13,9 +13,9 @@ namespace esphome::uart { -static const char *const TAG = "uart.arduino_rp2040"; +static const char *const TAG = "uart.arduino_rp2"; -uint16_t RP2040UartComponent::get_config() { +uint16_t RP2UartComponent::get_config() { uint16_t config = 0; if (this->parity_ == UART_CONFIG_PARITY_NONE) { @@ -50,7 +50,7 @@ uint16_t RP2040UartComponent::get_config() { return config; } -void RP2040UartComponent::setup() { +void RP2UartComponent::setup() { auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -162,7 +162,7 @@ void RP2040UartComponent::setup() { } } -void RP2040UartComponent::dump_config() { +void RP2UartComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus:"); LOG_PIN(" TX Pin: ", tx_pin_); LOG_PIN(" RX Pin: ", rx_pin_); @@ -182,7 +182,7 @@ void RP2040UartComponent::dump_config() { } } -void RP2040UartComponent::write_array(const uint8_t *data, size_t len) { +void RP2UartComponent::write_array(const uint8_t *data, size_t len) { this->serial_->write(data, len); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { @@ -190,13 +190,13 @@ void RP2040UartComponent::write_array(const uint8_t *data, size_t len) { } #endif } -bool RP2040UartComponent::peek_byte(uint8_t *data) { +bool RP2UartComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; *data = this->serial_->peek(); return true; } -bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { +bool RP2UartComponent::read_array(uint8_t *data, size_t len) { if (!this->check_read_timeout_(len)) return false; this->serial_->readBytes(data, len); @@ -207,12 +207,12 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { #endif return true; } -size_t RP2040UartComponent::available() { return this->serial_->available(); } -UARTFlushResult RP2040UartComponent::flush() { +size_t RP2UartComponent::available() { return this->serial_->available(); } +UARTFlushResult RP2UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } } // namespace esphome::uart -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2.h similarity index 89% rename from esphome/components/uart/uart_component_rp2040.h rename to esphome/components/uart/uart_component_rp2.h index 198c698af9..734bc6022e 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_RP2040 +#ifdef USE_RP2 #include #include @@ -13,7 +13,7 @@ namespace esphome::uart { -class RP2040UartComponent : public UARTComponent, public Component { +class RP2UartComponent final : public UARTComponent, public Component { public: void setup() override; void dump_config() override; @@ -40,4 +40,4 @@ class RP2040UartComponent : public UARTComponent, public Component { }; } // namespace esphome::uart -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h index da33bea70c..b69dcf0676 100644 --- a/esphome/components/uart/uart_debugger.h +++ b/esphome/components/uart/uart_debugger.h @@ -18,7 +18,7 @@ namespace esphome::uart { /// 'appropriate time' means exactly, is determined by a number of /// configurable constraints. E.g. when a given number of bytes is gathered /// and/or when no more data has been seen for a given time interval. -class UARTDebugger : public Component, public Trigger, StringRef> { +class UARTDebugger final : public Component, public Trigger, StringRef> { public: explicit UARTDebugger(UARTComponent *parent); void loop() override; @@ -73,7 +73,7 @@ class UARTDebugger : public Component, public Trigger class UDPWriteAction : public Action, public Parented { +template class UDPWriteAction final : public Action, public Parented { public: void set_data_template(std::vector (*func)(Ts...)) { this->data_.func = func; diff --git a/esphome/components/udp/packet_transport/udp_transport.h b/esphome/components/udp/packet_transport/udp_transport.h index 8621ddca48..e91a3e2a5a 100644 --- a/esphome/components/udp/packet_transport/udp_transport.h +++ b/esphome/components/udp/packet_transport/udp_transport.h @@ -8,7 +8,7 @@ namespace esphome::udp { -class UDPTransport : public packet_transport::PacketTransport, public Parented { +class UDPTransport final : public packet_transport::PacketTransport, public Parented { public: void setup() override; diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index fb0edf2ebd..274e0119ee 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -18,7 +18,7 @@ namespace esphome::udp { static const size_t MAX_PACKET_SIZE = 508; -class UDPComponent : public Component { +class UDPComponent final : public Component { public: void set_addresses(std::initializer_list addresses) { this->addresses_ = addresses; } /// Prevent accidental use of std::string which would dangle diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h index fce6258632..0928fda9ee 100644 --- a/esphome/components/ufire_ec/ufire_ec.h +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -24,7 +24,7 @@ static const uint8_t COMMAND_CALIBRATE_PROBE = 20; static const uint8_t COMMAND_MEASURE_TEMP = 40; static const uint8_t COMMAND_MEASURE_EC = 80; -class UFireECComponent : public PollingComponent, public i2c::I2CDevice { +class UFireECComponent final : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void update() override; @@ -58,7 +58,7 @@ class UFireECComponent : public PollingComponent, public i2c::I2CDevice { float temperature_coefficient_{0.0}; }; -template class UFireECCalibrateProbeAction : public Action { +template class UFireECCalibrateProbeAction final : public Action { public: UFireECCalibrateProbeAction(UFireECComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, solution) @@ -72,7 +72,7 @@ template class UFireECCalibrateProbeAction : public Action class UFireECResetAction : public Action { +template class UFireECResetAction final : public Action { public: UFireECResetAction(UFireECComponent *parent) : parent_(parent) {} diff --git a/esphome/components/ufire_ise/ufire_ise.h b/esphome/components/ufire_ise/ufire_ise.h index bff8eeff9d..85916f227e 100644 --- a/esphome/components/ufire_ise/ufire_ise.h +++ b/esphome/components/ufire_ise/ufire_ise.h @@ -29,7 +29,7 @@ static const uint8_t COMMAND_CALIBRATE_LOW = 10; static const uint8_t COMMAND_MEASURE_TEMP = 40; static const uint8_t COMMAND_MEASURE_MV = 80; -class UFireISEComponent : public PollingComponent, public i2c::I2CDevice { +class UFireISEComponent final : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void update() override; @@ -58,7 +58,7 @@ class UFireISEComponent : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *ph_sensor_{nullptr}; }; -template class UFireISECalibrateProbeLowAction : public Action { +template class UFireISECalibrateProbeLowAction final : public Action { public: UFireISECalibrateProbeLowAction(UFireISEComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, solution) @@ -69,7 +69,7 @@ template class UFireISECalibrateProbeLowAction : public Action class UFireISECalibrateProbeHighAction : public Action { +template class UFireISECalibrateProbeHighAction final : public Action { public: UFireISECalibrateProbeHighAction(UFireISEComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, solution) @@ -80,7 +80,7 @@ template class UFireISECalibrateProbeHighAction : public Action< UFireISEComponent *parent_; }; -template class UFireISEResetAction : public Action { +template class UFireISEResetAction final : public Action { public: UFireISEResetAction(UFireISEComponent *parent) : parent_(parent) {} diff --git a/esphome/components/uln2003/uln2003.h b/esphome/components/uln2003/uln2003.h index 70f55f72bf..1b1a16f95e 100644 --- a/esphome/components/uln2003/uln2003.h +++ b/esphome/components/uln2003/uln2003.h @@ -12,7 +12,7 @@ enum ULN2003StepMode { ULN2003_STEP_MODE_WAVE_DRIVE, }; -class ULN2003 : public stepper::Stepper, public Component { +class ULN2003 final : public stepper::Stepper, public Component { public: void set_pin_a(GPIOPin *pin_a) { pin_a_ = pin_a; } void set_pin_b(GPIOPin *pin_b) { pin_b_ = pin_b; } diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index 7d333a1b24..ea8fcbf72e 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -18,7 +18,7 @@ struct UltrasonicSensorStore { volatile bool echo_end{false}; }; -class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent { +class UltrasonicSensorComponent final : public sensor::Sensor, public PollingComponent { public: void set_trigger_pin(InternalGPIOPin *trigger_pin) { this->trigger_pin_ = trigger_pin; } void set_echo_pin(InternalGPIOPin *echo_pin) { this->echo_pin_ = echo_pin; } diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index ddb471be18..18d333a5ef 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -54,7 +54,9 @@ _UPDATE_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTUpdateComponent), - cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_ON_UPDATE_AVAILABLE): automation.validate_automation( single=True ), @@ -136,7 +138,9 @@ async def to_code(config): automation.maybe_simple_id( { cv.GenerateID(): cv.use_id(UpdateEntity), - cv.Optional(CONF_FORCE_UPDATE, default=False): cv.templatable(cv.boolean), + cv.Optional( + CONF_FORCE_UPDATE, default=False, visibility=cv.Visibility.ADVANCED + ): cv.templatable(cv.boolean), } ), synchronous=True, diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h index 821151f67c..8ba7b71a9c 100644 --- a/esphome/components/update/automation.h +++ b/esphome/components/update/automation.h @@ -6,19 +6,19 @@ namespace esphome::update { -template class PerformAction : public Action, public Parented { +template class PerformAction final : public Action, public Parented { TEMPLATABLE_VALUE(bool, force) public: void play(const Ts &...x) override { this->parent_->perform(this->force_.value(x...)); } }; -template class CheckAction : public Action, public Parented { +template class CheckAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->check(); } }; -template class IsAvailableCondition : public Condition, public Parented { +template class IsAvailableCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } }; diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h index 4cc5a4a3bc..4755655747 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h @@ -6,7 +6,7 @@ namespace esphome::uponor_smatrix { -class UponorSmatrixClimate : public climate::Climate, public Component, public UponorSmatrixDevice { +class UponorSmatrixClimate final : public climate::Climate, public Component, public UponorSmatrixDevice { public: void dump_config() override; void loop() override; diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h index 346fe1e3d6..b507642fce 100644 --- a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h @@ -6,7 +6,7 @@ namespace esphome::uponor_smatrix { -class UponorSmatrixSensor : public sensor::Sensor, public Component, public UponorSmatrixDevice { +class UponorSmatrixSensor final : public sensor::Sensor, public Component, public UponorSmatrixDevice { SUB_SENSOR(temperature) SUB_SENSOR(external_temperature) SUB_SENSOR(humidity) diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.h b/esphome/components/uponor_smatrix/uponor_smatrix.h index e9e772feab..8476c6bac2 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.h +++ b/esphome/components/uponor_smatrix/uponor_smatrix.h @@ -62,7 +62,7 @@ struct UponorSmatrixData { class UponorSmatrixDevice; -class UponorSmatrixComponent : public uart::UARTDevice, public Component { +class UponorSmatrixComponent final : public uart::UARTDevice, public Component { public: UponorSmatrixComponent() = default; diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h index 1b80a4480a..b0b12954b2 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.h +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.h @@ -5,7 +5,7 @@ namespace esphome::uptime { -class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { +class UptimeSecondsSensor final : public sensor::Sensor, public PollingComponent { public: void update() override; void dump_config() override; diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h index 912c0b7655..5b837cbce1 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h @@ -10,7 +10,7 @@ namespace esphome::uptime { -class UptimeTimestampSensor : public sensor::Sensor, public Component { +class UptimeTimestampSensor final : public sensor::Sensor, public Component { public: void setup() override; void dump_config() override; diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h index a97ba332bb..0bdc7fe404 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.h +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -7,7 +7,7 @@ namespace esphome::uptime { -class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent { +class UptimeTextSensor final : public text_sensor::TextSensor, public PollingComponent { public: UptimeTextSensor(const char *days_text, const char *hours_text, const char *minutes_text, const char *seconds_text, const char *separator, bool expand) diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index 10692fd436..8e71fc61b2 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -51,7 +51,7 @@ struct CDCEvent { class USBCDCACMComponent; /// Represents a single CDC ACM interface instance -class USBCDCACMInstance : public uart::UARTComponent, public Parented { +class USBCDCACMInstance final : public uart::UARTComponent, public Parented { public: void setup(); void loop(); @@ -84,6 +84,12 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parenteddefer([this, error_code = status.error_code]() { - ESP_LOGE(TAG, "CH34x chip detection failed: %s", esp_err_to_name(error_code)); - this->apply_line_settings_(); - }); - return; - } - CH34xChipType chiptype = CHIP_UNKNOWN; - uint8_t num_ports = 1; - for (const auto &e : CH34X_TABLE) { - if (e.pid != this->pid_) - continue; - if (e.match != 0xFF && (status.data[e.byte_idx] & e.mask) != e.match) - continue; - chiptype = e.chiptype; - num_ports = e.num_ports; +bool USBUartTypeCH34X::config_device_step(uint8_t step, bool ok, const uint8_t *response) { + if (step == 0) { + // Vendor-specific GET_CHIP_VERSION request (bRequest=0x5F): returns chip ID bytes + // used to distinguish CH34x variants sharing the same PID. + this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_IN, 0x5F, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0}); + return true; + } + // step 1: parse the chip-version response (falling back to "unknown" on failure). + if (!ok) { + ESP_LOGE(TAG, "CH34x chip detection failed"); + return false; + } + CH34xChipType chiptype = CHIP_UNKNOWN; + uint8_t num_ports = 1; + for (const auto &e : CH34X_TABLE) { + if (e.pid != this->pid_) + continue; + if (e.match != 0xFF && (response[e.byte_idx] & e.mask) != e.match) + continue; + chiptype = e.chiptype; + num_ports = e.num_ports; + break; + } + // CH344L vs CH344L_V2 requires chipver (data[0]) in addition to chiptype (data[1]) + if (chiptype == CHIP_CH344L && (response[0] & 0xF0) != 0x40) + chiptype = CHIP_CH344L_V2; + const char *name = "unknown"; + for (const auto &e : CH34X_TABLE) { + if (e.chiptype == chiptype) { + name = e.name; break; } - // CH344L vs CH344L_V2 requires chipver (data[0]) in addition to chiptype (data[1]) - if (chiptype == CHIP_CH344L && (status.data[0] & 0xF0) != 0x40) - chiptype = CHIP_CH344L_V2; - const char *name = "unknown"; - for (const auto &e : CH34X_TABLE) { - if (e.chiptype == chiptype) { - name = e.name; - break; - } - } - this->defer([this, chiptype, num_ports, name]() { - this->chiptype_ = chiptype; - this->chip_name_ = name; - this->num_ports_ = num_ports; - ESP_LOGD(TAG, "CH34x chip: %s, ports: %u", name, this->num_ports_); - this->apply_line_settings_(); - }); - }; - // Vendor-specific GET_CHIP_VERSION request (bRequest=0x5F): returns chip ID bytes - // used to distinguish CH34x variants sharing the same PID. - this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_IN, 0x5F, 0, 0, cb, {0, 0, 0, 0, 0, 0, 0, 0}); + } + this->chiptype_ = chiptype; + this->chip_name_ = name; + this->num_ports_ = num_ports; + ESP_LOGD(TAG, "CH34x chip: %s, ports: %u", name, this->num_ports_); + return false; } void USBUartTypeCH34X::dump_config() { @@ -98,67 +95,64 @@ void USBUartTypeCH34X::dump_config() { ESP_LOGCONFIG(TAG, " CH34x chip: %s", this->chip_name_); } -void USBUartTypeCH34X::apply_line_settings_() { - for (auto *channel : this->channels_) { - if (!channel->initialised_.load()) - continue; - usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) { - if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - channel->initialised_.store(false); +bool USBUartTypeCH34X::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, + const uint8_t *response) { + uint8_t cmd = 0xA1 + channel->index_; + if (channel->index_ >= 2) + cmd += 0xE; + switch (step) { + case 0: { + uint8_t divisor = 7; + uint32_t clk = 12000000; + + auto baud_rate = channel->baud_rate_; + if (baud_rate < 256000) { + if (baud_rate > 6000000 / 255) { + divisor = 3; + clk = 6000000; + } else if (baud_rate > 750000 / 255) { + divisor = 2; + clk = 750000; + } else if (baud_rate > 93750 / 255) { + divisor = 1; + clk = 93750; + } else { + divisor = 0; + clk = 11719; + } } - }; - - uint8_t divisor = 7; - uint32_t clk = 12000000; - - auto baud_rate = channel->baud_rate_; - if (baud_rate < 256000) { - if (baud_rate > 6000000 / 255) { - divisor = 3; - clk = 6000000; - } else if (baud_rate > 750000 / 255) { - divisor = 2; - clk = 750000; - } else if (baud_rate > 93750 / 255) { - divisor = 1; - clk = 93750; - } else { - divisor = 0; - clk = 11719; + ESP_LOGV(TAG, "baud_rate: %" PRIu32 ", divisor: %d, clk: %" PRIu32, baud_rate, divisor, clk); + auto factor = static_cast(clk / baud_rate); + if (factor == 0 || factor == 0xFF) { + ESP_LOGE(TAG, "Invalid baud rate %" PRIu32, baud_rate); + return false; } - } - ESP_LOGV(TAG, "baud_rate: %" PRIu32 ", divisor: %d, clk: %" PRIu32, baud_rate, divisor, clk); - auto factor = static_cast(clk / baud_rate); - if (factor == 0 || factor == 0xFF) { - ESP_LOGE(TAG, "Invalid baud rate %" PRIu32, baud_rate); - channel->initialised_.store(false); - continue; - } - if ((clk / factor - baud_rate) > (baud_rate - clk / (factor + 1))) - factor++; - factor = 256 - factor; + if ((clk / factor - baud_rate) > (baud_rate - clk / (factor + 1))) + factor++; + factor = 256 - factor; - uint16_t value = 0xC0; - if (channel->stop_bits_ == UART_CONFIG_STOP_BITS_2) - value |= 4; - switch (channel->parity_) { - case UART_CONFIG_PARITY_NONE: - break; - default: - value |= 8 | ((channel->parity_ - 1) << 4); - break; + uint16_t value = 0xC0; + if (channel->stop_bits_ == UART_CONFIG_STOP_BITS_2) + value |= 4; + switch (channel->parity_) { + case UART_CONFIG_PARITY_NONE: + break; + default: + value |= 8 | ((channel->parity_ - 1) << 4); + break; + } + value |= channel->data_bits_ - 5; + value <<= 8; + value |= 0x8C; + this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor); + return true; } - value |= channel->data_bits_ - 5; - value <<= 8; - value |= 0x8C; - uint8_t cmd = 0xA1 + channel->index_; - if (channel->index_ >= 2) - cmd += 0xE; - this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback); - this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd + 3, 0x80, 0, callback); + case 1: + this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd + 3, 0x80, 0); + return true; + default: + return false; } - this->start_channels_(); } std::vector USBUartTypeCH34X::parse_descriptors(usb_device_handle_t dev_hdl) { diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index c4edaed038..2722ec8555 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -97,29 +97,31 @@ std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev return cdc_devs; } -void USBUartTypeCP210X::enable_channels() { - // enable the channels - for (auto *channel : this->channels_) { - if (!channel->initialised_.load()) - continue; - usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) { - if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - channel->initialised_.store(false); - } - }; - this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_, callback); - uint16_t line_control = channel->stop_bits_; - line_control |= static_cast(channel->parity_) << 4; - line_control |= channel->data_bits_ << 8; - ESP_LOGD(TAG, "Line control value 0x%X", line_control); - this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_LINE_CTL, line_control, channel->index_, - callback); - auto baud = ByteBuffer::wrap(channel->baud_rate_, LITTLE); - this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_BAUDRATE, 0, channel->index_, callback, - baud.get_data()); +bool USBUartTypeCP210X::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, + const uint8_t *response) { + // On reload, skip the one-time IFC_ENABLE step (the interface is already enabled). + if (reload) + step++; + switch (step) { + case 0: + this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_); + return true; + case 1: { + uint16_t line_control = channel->stop_bits_; + line_control |= static_cast(channel->parity_) << 4; + line_control |= channel->data_bits_ << 8; + ESP_LOGD(TAG, "Line control value 0x%X", line_control); + this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_LINE_CTL, line_control, channel->index_); + return true; + } + case 2: { + auto baud = ByteBuffer::wrap(channel->baud_rate_, LITTLE); + this->config_transfer_(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_BAUDRATE, 0, channel->index_, baud.get_data()); + return true; + } + default: + return false; } - this->start_channels_(); } } // namespace esphome::usb_uart diff --git a/esphome/components/usb_uart/ft23xx.cpp b/esphome/components/usb_uart/ft23xx.cpp index 2e8ff8bcb5..79aa107d72 100644 --- a/esphome/components/usb_uart/ft23xx.cpp +++ b/esphome/components/usb_uart/ft23xx.cpp @@ -3,6 +3,7 @@ #include "usb_uart.h" #include "usb/usb_host.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include "esphome/components/uart/uart_debugger.h" #include "esphome/components/bytebuffer/bytebuffer.h" @@ -111,40 +112,46 @@ static int ftdi_to_clkbits(int baudrate, unsigned int clk, int clk_div, uint32_t return best_baud; } -static int ftdi_convert_baudrate(int baudrate, uint8_t chip_type, uint8_t channel_index, uint16_t *value, - uint16_t *index) { +struct FtdiConfig { + uint16_t value; + uint16_t ftdi_index; int best_baud; +}; + +static FtdiConfig ftdi_convert_baudrate(int baudrate, uint8_t chip_type, uint8_t channel_index) { uint32_t encoded_divisor; + FtdiConfig config{}; + if (baudrate <= 0) { - return -1; + return config; } static constexpr uint32_t H_CLK = 120000000; static constexpr uint32_t C_CLK = 48000000; if ((chip_type == TYPE_2232H) || (chip_type == TYPE_4232H) || (chip_type == TYPE_232H)) { if (baudrate * 10 > H_CLK / 0x3fff) { - best_baud = ftdi_to_clkbits(baudrate, H_CLK, 10, &encoded_divisor); + config.best_baud = ftdi_to_clkbits(baudrate, H_CLK, 10, &encoded_divisor); encoded_divisor |= 0x20000; /* switch on CLK/10*/ } else { - best_baud = ftdi_to_clkbits(baudrate, C_CLK, 16, &encoded_divisor); + config.best_baud = ftdi_to_clkbits(baudrate, C_CLK, 16, &encoded_divisor); } } else if ((chip_type == TYPE_BM) || (chip_type == TYPE_2232C) || (chip_type == TYPE_R) || (chip_type == TYPE_230X)) { - best_baud = ftdi_to_clkbits(baudrate, C_CLK, 16, &encoded_divisor); + config.best_baud = ftdi_to_clkbits(baudrate, C_CLK, 16, &encoded_divisor); } else { - best_baud = ftdi_to_clkbits_am(baudrate, &encoded_divisor); + config.best_baud = ftdi_to_clkbits_am(baudrate, &encoded_divisor); } - *value = (uint16_t) (encoded_divisor & 0xFFFF); + config.value = (uint16_t) (encoded_divisor & 0xFFFF); if (chip_type == TYPE_2232H || chip_type == TYPE_4232H || chip_type == TYPE_232H) { - *index = (uint16_t) (encoded_divisor >> 8); - *index &= 0xFF00; - *index |= (channel_index + 1); + config.ftdi_index = (uint16_t) (encoded_divisor >> 8); + config.ftdi_index &= 0xFF00; + config.ftdi_index |= (channel_index + 1); } else { - *index = (uint16_t) (encoded_divisor >> 16); + config.ftdi_index = (uint16_t) (encoded_divisor >> 16); } - return best_baud; + return config; } static optional get_uart(const usb_config_desc_t *config_desc, uint8_t intf_idx) { @@ -263,140 +270,15 @@ std::vector USBUartTypeFT23XX::parse_descriptors(usb_device_handle_t dev return cdc_devs; } -int USBUartTypeFT23XX::reset_(USBUartChannel *channel) { - usb_host::transfer_cb_t callback = [channel, this](const usb_host::TransferStatus &status) { - if (!status.success) { - ESP_LOGE(TAG, "Reset failed, status=%s", esp_err_to_name(status.error_code)); - channel->initialised_.store(false); - } else { - ESP_LOGD(TAG, "Reset successful, setting baudrate..."); - this->set_baudrate_(channel); - } - }; - bool ok = this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x00, 0x00, - channel->cdc_dev_.bulk_interface_number + 1, callback); - if (!ok) { - ESP_LOGE(TAG, "Reset control_transfer submit failed"); - channel->initialised_.store(false); - return -1; - } - return 0; -} - -int USBUartTypeFT23XX::set_baudrate_(USBUartChannel *channel, uint32_t baudrate) { - usb_host::transfer_cb_t callback = [channel, this](const usb_host::TransferStatus &status) { - if (!status.success) { - ESP_LOGE(TAG, "Set baudrate failed, status=%s", esp_err_to_name(status.error_code)); - channel->initialised_.store(false); - } else { - ESP_LOGD(TAG, "Baudrate %" PRIu32 " set, setting line properties...", channel->baud_rate_); - this->set_line_properties_(channel); - } - }; - if (baudrate == 0) { - baudrate = channel->baud_rate_; - } - uint16_t value = 0, ftdi_index = 0; - ftdi_convert_baudrate(baudrate, this->chip_type_, channel->index_, &value, &ftdi_index); - ESP_LOGD(TAG, "Baudrate: %" PRIu32 ", value=0x%04X, ftdi_index=0x%04X", baudrate, value, ftdi_index); - uint16_t usb_index = (ftdi_index & 0xFF00) | (channel->cdc_dev_.bulk_interface_number + 1); - bool ok = this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x03, value, usb_index, callback); - if (!ok) { - ESP_LOGE(TAG, "Set baudrate control_transfer submit failed"); - channel->initialised_.store(false); - return -1; - } - return 0; -} - -int USBUartTypeFT23XX::set_line_properties_(USBUartChannel *channel) { - usb_host::transfer_cb_t callback = [channel, this](const usb_host::TransferStatus &status) { - if (!status.success) { - ESP_LOGE(TAG, "Set line properties failed, status=%s", esp_err_to_name(status.error_code)); - channel->initialised_.store(false); - return; - } - ESP_LOGD(TAG, "Line properties set, setting modem control..."); - this->set_dtr_rts_(channel); - }; - - uint16_t value = channel->data_bits_; - - switch (channel->parity_) { - case UART_CONFIG_PARITY_NONE: - value |= (0x00 << 8); - break; - case UART_CONFIG_PARITY_ODD: - value |= (0x01 << 8); - break; - case UART_CONFIG_PARITY_EVEN: - value |= (0x02 << 8); - break; - case UART_CONFIG_PARITY_MARK: - value |= (0x03 << 8); - break; - case UART_CONFIG_PARITY_SPACE: - value |= (0x04 << 8); - break; - } - - switch (channel->stop_bits_) { - case UART_CONFIG_STOP_BITS_1: - value |= (0x00 << 11); - break; - case UART_CONFIG_STOP_BITS_1_5: - value |= (0x01 << 11); - break; - case UART_CONFIG_STOP_BITS_2: - value |= (0x02 << 11); - break; - } - - value |= (0x00 << 14); - - bool ok = this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x04, value, - channel->cdc_dev_.bulk_interface_number + 1, callback); - if (!ok) { - ESP_LOGE(TAG, "Set line properties control_transfer submit failed"); - channel->initialised_.store(false); - return -1; - } - return 0; -} - -int USBUartTypeFT23XX::set_dtr_rts_(USBUartChannel *channel) { - usb_host::transfer_cb_t callback = [channel, this](const usb_host::TransferStatus &status) { - if (!status.success) { - ESP_LOGE(TAG, "Set modem control failed, status=%s", esp_err_to_name(status.error_code)); - channel->initialised_.store(false); - return; - } - ESP_LOGD(TAG, "Modem control set for channel %d, starting input...", channel->index_); - channel->initialised_.store(true); - this->start_input(channel); - uint8_t next_index = channel->index_ + 1; - if (next_index < this->channels_.size()) { - USBUartChannel *next_channel = this->channels_[next_index]; - ESP_LOGD(TAG, "Configuring next channel %d", next_channel->index_); - this->reset_(next_channel); - return; - } else { - ESP_LOGI(TAG, "All channels configured"); - } - }; - - bool ok = this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x01, 0x0000, - channel->cdc_dev_.bulk_interface_number + 1, callback); - if (!ok) { - ESP_LOGE(TAG, "Set modem control control_transfer submit failed"); - channel->initialised_.store(false); - return -1; - } - return 0; -} - void USBUartTypeFT23XX::start_input(USBUartChannel *channel) { - if (!channel->initialised_.load() || channel->input_started_.load()) + if (!channel->initialised_.load()) + return; + + // Use compare_exchange_strong to avoid a check-then-act race: start_input() is called + // from both the USB task (self-restart on success) and the main loop (backpressure + // restart), so a plain load()/store() pair can let both threads submit a transfer. + auto started = false; + if (!channel->input_started_.compare_exchange_strong(started, true)) return; const auto *ep = channel->cdc_dev_.in_ep; @@ -408,51 +290,119 @@ void USBUartTypeFT23XX::start_input(USBUartChannel *channel) { return; } + // FTDI prepends a 2-byte modem/line status header to every bulk IN packet. size_t uart_data_len = (status.data_len > 2) ? (status.data_len - 2) : 0; if (uart_data_len > 0) { ESP_LOGV(TAG, "RX callback: Received %zu bytes, channel=%d", uart_data_len, channel->index_); if (!channel->dummy_receiver_) { - // Copy the entire received UART payload into the ring buffer in one - // operation to avoid per-byte overhead and reduce the chance of - // heap activity in hot paths. - channel->input_buffer_.push(status.data + 2, uart_data_len); + UsbDataChunk *chunk = this->chunk_pool_.allocate(); + if (chunk == nullptr) { + this->usb_data_queue_.increment_dropped_count(); + channel->input_started_.store(false); + // Queue is full — wake the main loop to drain it, then let read_array() + // retrigger start_input() rather than spinning here in the USB task. + this->enable_loop_soon_any_context(); + App.wake_loop_threadsafe(); + return; + } + // Strip the 2-byte FTDI header before queuing. + memcpy(chunk->data, status.data + 2, uart_data_len); + chunk->length = static_cast(uart_data_len); + chunk->channel = channel; + this->usb_data_queue_.push(chunk); #ifdef USE_UART_DEBUGGER if (channel->debug_) { - // Debug path creates a temporary vector for logging only; this is - // acceptable because debug mode is opt-in and not used in release. uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, std::vector(status.data + 2, status.data + 2 + uart_data_len), ',', channel->debug_prefix_); } #endif + this->enable_loop_soon_any_context(); + App.wake_loop_threadsafe(); } - } else { + } else if (status.data_len >= 2) { ESP_LOGVV(TAG, "RX: Status packet, modem=0x%02X line=0x%02X, ch=%d", status.data[0], status.data[1], channel->index_); } channel->input_started_.store(false); - if (channel->dummy_receiver_ || - channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) { - this->start_input(channel); - } + this->start_input(channel); }; - channel->input_started_.store(true); - this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { + ESP_LOGE(TAG, "RX transfer submission failed for ep=0x%02X", ep->bEndpointAddress); + channel->input_started_.store(false); + } } -void USBUartTypeFT23XX::enable_channels() { - if (!this->channels_.empty() && this->channels_[0]->initialised_.load()) { - this->reset_(this->channels_[0]); - } +void USBUartTypeFT23XX::on_rx_overflow(USBUartChannel *channel) { + ESP_LOGW(TAG, "RX buffer overflow on channel %d, clearing to resync", channel->index_); + channel->input_buffer_.clear(); +} - for (auto *channel : this->channels_) { - if (!channel->initialised_.load()) - continue; - channel->input_started_.store(false); - channel->output_started_.store(false); +bool USBUartTypeFT23XX::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, + const uint8_t *response) { + // On reload (settings change on an open channel) skip the SIO reset; the FTDI set_termios + // path only re-applies baud + line properties and does not re-assert DTR/RTS. + if (reload) + step++; + switch (step) { + case 0: // SIO reset (init only) + this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x00, 0x00, + channel->cdc_dev_.bulk_interface_number + 1); + return true; + case 1: { // set baudrate + auto config = ftdi_convert_baudrate(channel->baud_rate_, this->chip_type_, channel->index_); + uint16_t usb_index = (config.ftdi_index & 0xFF00) | (channel->cdc_dev_.bulk_interface_number + 1); + ESP_LOGD(TAG, "Baudrate: %u, value=0x%04X, ftdi_index=0x%04X", (unsigned) channel->baud_rate_, config.value, + config.ftdi_index); + this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x03, config.value, usb_index); + return true; + } + case 2: { // set line properties (data bits / parity / stop bits) + uint16_t value = channel->data_bits_; + switch (channel->parity_) { + case UART_CONFIG_PARITY_NONE: + value |= (0x00 << 8); + break; + case UART_CONFIG_PARITY_ODD: + value |= (0x01 << 8); + break; + case UART_CONFIG_PARITY_EVEN: + value |= (0x02 << 8); + break; + case UART_CONFIG_PARITY_MARK: + value |= (0x03 << 8); + break; + case UART_CONFIG_PARITY_SPACE: + value |= (0x04 << 8); + break; + } + switch (channel->stop_bits_) { + default: // 1 bit + value |= (0x00 << 11); + break; + case UART_CONFIG_STOP_BITS_1_5: + value |= (0x01 << 11); + break; + case UART_CONFIG_STOP_BITS_2: + value |= (0x02 << 11); + break; + } + value |= (0x00 << 14); + this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x04, value, + channel->cdc_dev_.bulk_interface_number + 1); + return true; + } + case 3: // set modem control DTR+RTS (init only) + if (reload) + return false; + this->config_transfer_(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, 0x01, 0x0000, + channel->cdc_dev_.bulk_interface_number + 1); + return true; + default: + return false; } } diff --git a/esphome/components/usb_uart/pl2303.cpp b/esphome/components/usb_uart/pl2303.cpp index 134c51198d..3c7ecd9a83 100644 --- a/esphome/components/usb_uart/pl2303.cpp +++ b/esphome/components/usb_uart/pl2303.cpp @@ -200,100 +200,114 @@ std::vector USBUartTypePL2303::parse_descriptors(usb_device_handle_t dev return cdc_devs; } -void USBUartTypePL2303::enable_channels() { - if (this->channels_.empty()) - return; +// Vendor init sequence for non-HXN chips (mirrors pl2303_startup in the Linux driver): +// read 0x8484, write 0x0404=0, read 0x8484, read 0x8383, read 0x8484, write 0x0404=1, +// read 0x8484, read 0x8383, write 0=1, write 1=0, write 2=0x24 (legacy) or 0x44 (HX+). +// The final entry's wIndex is patched at runtime depending on the chip type. +struct Pl2303InitStep { + uint8_t type; + uint8_t request; + uint16_t value; + uint16_t index; + bool read; // reads need a 1-byte buffer to set wLength=1 so the IN data stage runs +}; +static const Pl2303InitStep PL2303_INIT[] = { + {VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8484, 0, true}, + {VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 0x0404, 0, false}, + {VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8484, 0, true}, + {VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8383, 0, true}, + {VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8484, 0, true}, + {VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 0x0404, 1, false}, + {VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8484, 0, true}, + {VENDOR_READ_REQUEST_TYPE, VENDOR_READ_REQUEST, 0x8383, 0, true}, + {VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 0, 1, false}, + {VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 1, 0, false}, + {VENDOR_WRITE_REQUEST_TYPE, VENDOR_WRITE_REQUEST, 2, 0, false}, +}; +static constexpr uint8_t PL2303_INIT_COUNT = sizeof(PL2303_INIT) / sizeof(PL2303_INIT[0]); - auto *channel = this->channels_[0]; +bool USBUartTypePL2303::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, + const uint8_t *response) { bool is_legacy = (this->chip_type_ == PL2303_TYPE_H); bool is_hxn = (this->chip_type_ == PL2303_TYPE_HXN); - usb_host::transfer_cb_t nop_cb = [](const usb_host::TransferStatus &status) { - if (!status.success) - ESP_LOGW(TAG, "PL2303: vendor init transfer failed"); - }; - - // Init sequence for non-HXN chips (mirrors pl2303_startup in Linux driver): - // Read 0x8484, write 0x0404=0, read 0x8484, read 0x8383, read 0x8484, - // write 0x0404=1, read 0x8484, read 0x8383, - // write 0=1, write 1=0, write 2=0x24 (legacy) or 0x44 (HX+) - if (!is_hxn) { - uint8_t req = VENDOR_READ_REQUEST; - uint8_t wreq = VENDOR_WRITE_REQUEST; - - // Fire-and-forget vendor reads: result discarded, chip requires this sequence. - // Pass a 1-byte buffer to set wLength=1 so the IN data stage is performed. - this->control_transfer(VENDOR_READ_REQUEST_TYPE, req, 0x8484, 0, nop_cb, {0}); - this->control_transfer(VENDOR_WRITE_REQUEST_TYPE, wreq, 0x0404, 0, nop_cb); - this->control_transfer(VENDOR_READ_REQUEST_TYPE, req, 0x8484, 0, nop_cb, {0}); - this->control_transfer(VENDOR_READ_REQUEST_TYPE, req, 0x8383, 0, nop_cb, {0}); - this->control_transfer(VENDOR_READ_REQUEST_TYPE, req, 0x8484, 0, nop_cb, {0}); - this->control_transfer(VENDOR_WRITE_REQUEST_TYPE, wreq, 0x0404, 1, nop_cb); - this->control_transfer(VENDOR_READ_REQUEST_TYPE, req, 0x8484, 0, nop_cb, {0}); - this->control_transfer(VENDOR_READ_REQUEST_TYPE, req, 0x8383, 0, nop_cb, {0}); - this->control_transfer(VENDOR_WRITE_REQUEST_TYPE, wreq, 0, 1, nop_cb); - this->control_transfer(VENDOR_WRITE_REQUEST_TYPE, wreq, 1, 0, nop_cb); - this->control_transfer(VENDOR_WRITE_REQUEST_TYPE, wreq, 2, is_legacy ? 0x24 : 0x44, nop_cb); + // Vendor init burst runs only on full init for non-HXN chips. + uint8_t init_count = (!reload && !is_hxn) ? PL2303_INIT_COUNT : 0; + if (step < init_count) { + const auto &e = PL2303_INIT[step]; + uint16_t index = (step == PL2303_INIT_COUNT - 1) ? (is_legacy ? 0x24 : 0x44) : e.index; + this->config_transfer_(e.type, e.request, e.value, index, + e.read ? std::vector{0} : std::vector{}); + return true; } + step -= init_count; - // Build 7-byte line coding structure: - // [0-3] baud rate (LE32), [4] stop bits, [5] parity, [6] data bits - uint8_t line_coding[7] = {}; - uint32_t baud = channel->get_baud_rate(); - - // Choose baud encoding based on chip type - uint32_t nearest = nearest_supported_baud(baud); - if (baud == nearest || this->chip_type_ == PL2303_TYPE_HXN) { - encode_baud_direct(line_coding, baud); - } else if (this->chip_type_ == PL2303_TYPE_TA || this->chip_type_ == PL2303_TYPE_TB) { - encode_baud_divisor_alt(line_coding, baud); - } else { - encode_baud_divisor(line_coding, baud); - } - - // Stop bits: 0=1, 1=1.5, 2=2 - switch (channel->get_stop_bits()) { - case 2: - line_coding[4] = 2; - break; - default: - line_coding[4] = 0; - break; - } - - // Parity: 0=none, 1=odd, 2=even, 3=mark, 4=space - switch (channel->parity_) { - case UART_CONFIG_PARITY_ODD: - line_coding[5] = 1; - break; - case UART_CONFIG_PARITY_EVEN: - line_coding[5] = 2; - break; - case UART_CONFIG_PARITY_MARK: - line_coding[5] = 3; - break; - case UART_CONFIG_PARITY_SPACE: - line_coding[5] = 4; - break; - default: - line_coding[5] = 0; - break; - } - - // Data bits - line_coding[6] = channel->get_data_bits(); - - ESP_LOGD(TAG, "PL2303: SET_LINE_REQUEST baud=%" PRIu32 " stop=%u parity=%u data=%u", baud, line_coding[4], - line_coding[5], line_coding[6]); - - std::vector lc_vec(line_coding, line_coding + 7); uint16_t iface = channel->cdc_dev_.bulk_interface_number; - this->control_transfer(SET_LINE_REQUEST_TYPE, SET_LINE_REQUEST, 0, iface, nop_cb, lc_vec); + switch (step) { + case 0: { + // Build 7-byte line coding structure: + // [0-3] baud rate (LE32), [4] stop bits, [5] parity, [6] data bits + uint8_t line_coding[7] = {}; + uint32_t baud = channel->get_baud_rate(); - // Assert DTR + RTS - this->control_transfer(SET_CONTROL_REQUEST_TYPE, SET_CONTROL_REQUEST, CONTROL_DTR | CONTROL_RTS, iface, nop_cb); + // Choose baud encoding based on chip type + uint32_t nearest = nearest_supported_baud(baud); + if (baud == nearest || this->chip_type_ == PL2303_TYPE_HXN) { + encode_baud_direct(line_coding, baud); + } else if (this->chip_type_ == PL2303_TYPE_TA || this->chip_type_ == PL2303_TYPE_TB) { + encode_baud_divisor_alt(line_coding, baud); + } else { + encode_baud_divisor(line_coding, baud); + } - this->start_channels_(); + // Stop bits: 0=1, 1=1.5, 2=2 + switch (channel->get_stop_bits()) { + case 2: + line_coding[4] = 2; + break; + default: + line_coding[4] = 0; + break; + } + + // Parity: 0=none, 1=odd, 2=even, 3=mark, 4=space + switch (channel->parity_) { + case UART_CONFIG_PARITY_ODD: + line_coding[5] = 1; + break; + case UART_CONFIG_PARITY_EVEN: + line_coding[5] = 2; + break; + case UART_CONFIG_PARITY_MARK: + line_coding[5] = 3; + break; + case UART_CONFIG_PARITY_SPACE: + line_coding[5] = 4; + break; + default: + line_coding[5] = 0; + break; + } + + // Data bits + line_coding[6] = channel->get_data_bits(); + + ESP_LOGD(TAG, "PL2303: SET_LINE_REQUEST baud=%u stop=%u parity=%u data=%u", baud, line_coding[4], line_coding[5], + line_coding[6]); + + std::vector lc_vec(line_coding, line_coding + 7); + this->config_transfer_(SET_LINE_REQUEST_TYPE, SET_LINE_REQUEST, 0, iface, lc_vec); + return true; + } + case 1: + // Assert DTR + RTS (init only) + if (reload) + return false; + this->config_transfer_(SET_CONTROL_REQUEST_TYPE, SET_CONTROL_REQUEST, CONTROL_DTR | CONTROL_RTS, iface); + return true; + default: + return false; + } } } // namespace esphome::usb_uart diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index b8749b6a76..c289625f1a 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -6,6 +6,7 @@ #include "esphome/core/application.h" #include +#include namespace esphome::usb_uart { @@ -159,7 +160,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { } uint16_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE); memcpy(chunk->data, data, chunk_len); - chunk->length = static_cast(chunk_len); + chunk->length = chunk_len; // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. this->output_queue_.push(chunk); @@ -213,6 +214,7 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { void USBUartComponent::setup() { USBClient::setup(); } void USBUartComponent::loop() { bool had_work = this->process_usb_events_(); + had_work |= this->run_config_machine_(); // Process USB data from the lock-free queue UsbDataChunk *chunk; @@ -228,6 +230,11 @@ void USBUartComponent::loop() { } #endif + // If there is not enough space for the full chunk, let the device subclass + // handle it (e.g. FTDI clears the buffer to prevent mid-telegram corruption). + if (channel->input_buffer_.get_free_space() < chunk->length) { + this->on_rx_overflow(channel); + } // Push data to ring buffer (now safe in main loop) channel->input_buffer_.push(chunk->data, chunk->length); @@ -484,60 +491,182 @@ void USBUartTypeCdcAcm::on_disconnected() { USBClient::on_disconnected(); } -void USBUartTypeCdcAcm::enable_channels() { +bool USBUartTypeCdcAcm::config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, + const uint8_t *response) { static constexpr uint8_t CDC_REQUEST_TYPE = usb_host::USB_TYPE_CLASS | usb_host::USB_RECIP_INTERFACE; static constexpr uint8_t CDC_SET_LINE_CODING = 0x20; static constexpr uint8_t CDC_SET_CONTROL_LINE_STATE = 0x22; static constexpr uint16_t CDC_DTR_RTS = 0x0003; // D0=DTR, D1=RTS - for (auto *channel : this->channels_) { - if (!channel->initialised_.load()) - continue; - // Configure the bridge's UART parameters. A USB-UART bridge will not forward data - // at the correct speed until SET_LINE_CODING is sent; without it the UART may run - // at an indeterminate default rate so the NCP receives garbled bytes and never - // sends RSTACK. - uint32_t baud = channel->baud_rate_; - std::vector line_coding = { - static_cast(baud & 0xFF), static_cast((baud >> 8) & 0xFF), - static_cast((baud >> 16) & 0xFF), static_cast((baud >> 24) & 0xFF), - static_cast(channel->stop_bits_), // bCharFormat: 0=1stop, 1=1.5stop, 2=2stop - static_cast(channel->parity_), // bParityType: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space - static_cast(channel->data_bits_), // bDataBits - }; - ESP_LOGD(TAG, "SET_LINE_CODING: baud=%u stop=%u parity=%u data=%u", (unsigned) baud, channel->stop_bits_, - (unsigned) channel->parity_, channel->data_bits_); - this->control_transfer( - CDC_REQUEST_TYPE, CDC_SET_LINE_CODING, 0, channel->cdc_dev_.interrupt_interface_number, - [](const usb_host::TransferStatus &status) { - if (!status.success) { - ESP_LOGW(TAG, "SET_LINE_CODING failed: %X", status.error_code); - } else { - ESP_LOGD(TAG, "SET_LINE_CODING OK"); - } - }, - line_coding); - // Assert DTR+RTS to signal DTE is present. - this->control_transfer(CDC_REQUEST_TYPE, CDC_SET_CONTROL_LINE_STATE, CDC_DTR_RTS, - channel->cdc_dev_.interrupt_interface_number, [](const usb_host::TransferStatus &status) { - if (!status.success) { - ESP_LOGW(TAG, "SET_CONTROL_LINE_STATE failed: %X", status.error_code); - } else { - ESP_LOGD(TAG, "SET_CONTROL_LINE_STATE (DTR+RTS) OK"); - } - }); + switch (step) { + case 0: { + // Configure the bridge's UART parameters. A USB-UART bridge will not forward data + // at the correct speed until SET_LINE_CODING is sent; without it the UART may run + // at an indeterminate default rate so the NCP receives garbled bytes and never + // sends RSTACK. + uint32_t baud = channel->baud_rate_; + std::vector line_coding = { + static_cast(baud & 0xFF), static_cast((baud >> 8) & 0xFF), + static_cast((baud >> 16) & 0xFF), static_cast((baud >> 24) & 0xFF), + static_cast(channel->stop_bits_), // bCharFormat: 0=1stop, 1=1.5stop, 2=2stop + static_cast(channel->parity_), // bParityType: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space + static_cast(channel->data_bits_), // bDataBits + }; + ESP_LOGD(TAG, "SET_LINE_CODING: baud=%u stop=%u parity=%u data=%u", (unsigned) baud, channel->stop_bits_, + (unsigned) channel->parity_, channel->data_bits_); + this->config_transfer_(CDC_REQUEST_TYPE, CDC_SET_LINE_CODING, 0, channel->cdc_dev_.interrupt_interface_number, + line_coding); + return true; + } + case 1: + // Assert DTR+RTS to signal DTE is present (init only). + if (reload) + return false; + this->config_transfer_(CDC_REQUEST_TYPE, CDC_SET_CONTROL_LINE_STATE, CDC_DTR_RTS, + channel->cdc_dev_.interrupt_interface_number); + return true; + default: + return false; } - this->start_channels_(); } -void USBUartTypeCdcAcm::start_channels_() { - for (auto *channel : this->channels_) { - if (!channel->initialised_.load()) - continue; +void USBUartComponent::enable_channels() { + this->cfg_single_ = nullptr; + this->cfg_pending_reload_ = nullptr; + this->cfg_channel_idx_ = 0; + this->start_config_(false); +} + +void USBUartComponent::apply_channel_settings(USBUartChannel *channel) { + if (this->cfg_active_) { + // A config sequence is already running. Defer this reload until it finishes to preserve + // the one-control-transfer-at-a-time guarantee (restarting mid-flight would let an + // in-flight callback complete against fresh state). The pending slot coalesces multiple + // requests; the channel's live settings are read when the reload eventually runs. + // Note: multiple channel reloads are not queued; only one pending reload is supported at a time. + this->cfg_pending_reload_ = channel; + return; + } + this->cfg_single_ = channel; + this->start_config_(true); +} + +void USBUartComponent::start_config_(bool reload) { + this->cfg_reload_ = reload; + this->cfg_device_phase_ = !reload; + this->cfg_step_ = 0; + this->cfg_ok_ = true; + this->cfg_in_flight_ = false; + this->cfg_done_.store(false); + this->cfg_active_ = true; + this->enable_loop(); +} + +void USBUartComponent::config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index, + const std::vector &data) { + this->cfg_done_.store(false); + // The completion callback runs in the USB-task context: it only records the result and + // wakes the loop. The next transfer is issued from run_config_machine_() on the loop thread. + bool submitted = this->control_transfer( + type, request, value, index, + [this](const usb_host::TransferStatus &status) { + this->cfg_ok_ = status.success; + if (!status.success) { + ESP_LOGW(TAG, "Config control transfer failed: %s", esp_err_to_name(status.error_code)); + } else if (status.data_len > 0) { + memcpy(this->cfg_response_, status.data, std::min(status.data_len, sizeof(this->cfg_response_))); + } + // Release: publishes cfg_ok_/cfg_response_ before the loop observes cfg_done_. + this->cfg_done_.store(true, std::memory_order_release); + this->enable_loop_soon_any_context(); + App.wake_loop_threadsafe(); + }, + data); + if (!submitted) { + // Submission failed (e.g. no free transfer request). No callback will fire, so synthesize + // a failed completion here so the state machine advances/aborts instead of hanging. + ESP_LOGW(TAG, "Config control transfer submit failed"); + this->cfg_ok_ = false; + this->cfg_done_.store(true, std::memory_order_release); + } +} + +bool USBUartComponent::run_config_machine_() { + if (!this->cfg_active_) + return false; + + if (this->cfg_in_flight_) { + // Acquire: pairs with the release in config_transfer_'s callback. + if (!this->cfg_done_.load(std::memory_order_acquire)) + return false; // still waiting; the callback will re-wake the loop (no busy spin) + this->cfg_in_flight_ = false; + this->cfg_done_.store(false); + this->cfg_step_++; + } + + // cfg_ok_ is now synchronized (we only get here on the initial entry or after observing + // cfg_done_ with acquire ordering), so it is safe to read. + ESP_LOGV(TAG, "Config machine: device_phase=%d channel_idx=%d step=%d reload=%d ok=%d", this->cfg_device_phase_, + this->cfg_channel_idx_, this->cfg_step_, this->cfg_reload_, this->cfg_ok_); + + // One-time device-level phase (init only). config_device_step() inspects cfg_ok_ itself. + if (this->cfg_device_phase_) { + if (this->config_device_step(this->cfg_step_, this->cfg_ok_, this->cfg_response_)) { + this->cfg_in_flight_ = true; + return true; + } + this->cfg_device_phase_ = false; + this->cfg_step_ = 0; + this->cfg_ok_ = true; + } + + USBUartChannel *channel = + this->cfg_single_ != nullptr + ? this->cfg_single_ + : (this->cfg_channel_idx_ < this->channels_.size() ? this->channels_[this->cfg_channel_idx_] : nullptr); + + if (channel != nullptr && channel->initialised_.load()) { + if (!this->cfg_ok_) { + // A previous step in this channel's sequence failed. Abort the rest. On a full init, + // mark the channel uninitialised so data flow isn't started on a misconfigured channel; + // on a reload, leave the already-working channel as it was. + if (!this->cfg_reload_) + channel->initialised_.store(false); + } else if (this->config_step(channel, this->cfg_step_, this->cfg_reload_, this->cfg_ok_, this->cfg_response_)) { + this->cfg_in_flight_ = true; + return true; + } + } + + // Channel finished (or aborted). On full init, kick off data flow if still initialised. + if (channel != nullptr && !this->cfg_reload_ && channel->initialised_.load()) { channel->input_started_.store(false); channel->output_started_.store(false); this->start_input(channel); } + + // Advance to the next channel (or finish). + this->cfg_step_ = 0; + this->cfg_ok_ = true; + if (this->cfg_single_ != nullptr) { + this->cfg_active_ = false; + this->cfg_single_ = nullptr; + } else if (++this->cfg_channel_idx_ >= this->channels_.size()) { + this->cfg_active_ = false; + } + + // If the machine just went idle and a reload was requested while it was busy, start it now. + if (!this->cfg_active_ && this->cfg_pending_reload_ != nullptr) { + this->cfg_single_ = this->cfg_pending_reload_; + this->cfg_pending_reload_ = nullptr; + this->start_config_(true); + } + return true; +} + +void USBUartChannel::load_settings(bool /*dump_config*/) { + // The per-channel control transfers already log their values at debug level. + this->parent_->apply_channel_settings(this); } } // namespace esphome::usb_uart diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index c4fb77bdb2..5bb4c97796 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -126,7 +126,7 @@ struct UsbOutputChunk { void release() {} }; -class USBUartChannel : public uart::UARTComponent, public Parented { +class USBUartChannel final : public uart::UARTComponent, public Parented { friend class USBUartComponent; friend class USBUartTypeCdcAcm; friend class USBUartTypeCP210X; @@ -146,7 +146,9 @@ class USBUartChannel : public uart::UARTComponent, public Parentedinput_buffer_.get_available(); } bool is_connected() override { return this->initialised_.load(); } uart::UARTFlushResult flush() override; - void check_logger_conflict() override {} + // Re-apply the current line settings (baud, parity, etc) to this already-open channel. + void load_settings(bool dump_config) override; + using UARTComponent::load_settings; // also bring in the no-arg overload for convenience void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } @@ -160,6 +162,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented cb) { this->rx_callback_ = std::move(cb); } protected: + void check_logger_conflict() override {} // Larger structures first (8+ bytes) RingBuffer input_buffer_; LockFreeQueue output_queue_; @@ -192,9 +195,19 @@ class USBUartComponent : public usb_host::USBClient { void add_channel(USBUartChannel *channel) { this->channels_.push_back(channel); } - void start_input(USBUartChannel *channel); + virtual void start_input(USBUartChannel *channel); void start_output(USBUartChannel *channel); + // Begin configuring all channels (full initialisation). Called from on_connected(). + void enable_channels(); + // Re-apply line settings to a single, already-open channel (used by + // USBUartChannel::load_settings()). + void apply_channel_settings(USBUartChannel *channel); + + // Called from loop() when input_buffer_ has insufficient space for the incoming chunk. + // Default is a no-op; override in device-specific subclasses that need resync on overflow. + virtual void on_rx_overflow(USBUartChannel *channel) {} + // Lock-free data transfer from USB task to main loop static constexpr int USB_DATA_QUEUE_SIZE = 32; LockFreeQueue usb_data_queue_; @@ -202,7 +215,41 @@ class USBUartComponent : public usb_host::USBClient { EventPool chunk_pool_; protected: + // Issue one control transfer as part of the setup state machine. The completion + // callback (USB-task context) records the result/IN data, marks the step done and + // wakes the loop so run_config_machine_() advances on the loop thread. Call exactly + // once from config_step_()/config_device_step_() when issuing a step. + void config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index, + const std::vector &data = {}); + // (Re)start the config state machine. reload=false runs full init over all channels; + // reload=true re-applies settings to cfg_single_ only. + void start_config_(bool reload); + // Advance the config state machine; called from loop(). Returns true if it did work. + bool run_config_machine_(); + + // Per-subclass per-channel settings sequence. For the given zero-based step, issue the + // next control transfer via config_transfer_() and return true, or return false when the + // channel has no more steps. reload=true ⇒ apply only baud/parity/stop/data (skip + // enable/reset/DTR-RTS). ok/response carry the previous step's result and IN data. + virtual bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) = 0; + // Optional one-time device-level setup run before the per-channel phase on init only + // (e.g. CH34x chip detection). Same contract as config_step_(). Default: no steps. + virtual bool config_device_step(uint8_t step, bool ok, const uint8_t *response) { return false; } + std::vector channels_{}; + + // Config state machine + USBUartChannel *cfg_single_{nullptr}; // non-null: reload of a single channel + USBUartChannel *cfg_pending_reload_{nullptr}; // reload requested while the machine was busy + std::atomic cfg_done_{false}; // synchronizes cfg_ok_/cfg_response_ across threads + uint8_t cfg_response_[8]{}; // last IN transfer payload (for detection reads) + uint8_t cfg_channel_idx_{0}; + uint8_t cfg_step_{0}; + bool cfg_active_{false}; + bool cfg_reload_{false}; + bool cfg_device_phase_{false}; + bool cfg_in_flight_{false}; + bool cfg_ok_{true}; }; class USBUartTypeCdcAcm : public USBUartComponent { @@ -213,11 +260,7 @@ class USBUartTypeCdcAcm : public USBUartComponent { virtual std::vector parse_descriptors(usb_device_handle_t dev_hdl); void on_connected() override; void on_disconnected() override; - virtual void enable_channels(); - /// Resets per-channel transfer flags and posts the first bulk IN transfer. - /// Called by enable_channels() and by vendor-specific subclass overrides that - /// handle their own line-coding setup before starting data flow. - void start_channels_(); + bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; }; class USBUartTypeCP210X : public USBUartTypeCdcAcm { @@ -226,7 +269,7 @@ class USBUartTypeCP210X : public USBUartTypeCdcAcm { protected: std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; - void enable_channels() override; + bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; }; class USBUartTypeCH34X : public USBUartTypeCdcAcm { public: @@ -234,11 +277,11 @@ class USBUartTypeCH34X : public USBUartTypeCdcAcm { void dump_config() override; protected: - void enable_channels() override; + bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; + bool config_device_step(uint8_t step, bool ok, const uint8_t *response) override; std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; private: - void apply_line_settings_(); CH34xChipType chiptype_{CHIP_UNKNOWN}; const char *chip_name_{"unknown"}; uint8_t num_ports_{1}; @@ -248,16 +291,12 @@ class USBUartTypeFT23XX : public USBUartTypeCdcAcm { public: USBUartTypeFT23XX(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} - void start_input(USBUartChannel *channel); + void start_input(USBUartChannel *channel) override; + void on_rx_overflow(USBUartChannel *channel) override; protected: std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; - void enable_channels() override; - - int reset_(USBUartChannel *channel); - int set_baudrate_(USBUartChannel *channel, uint32_t baudrate = 0); - int set_line_properties_(USBUartChannel *channel); - int set_dtr_rts_(USBUartChannel *channel); + bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; uint8_t chip_type_{255}; }; @@ -280,7 +319,7 @@ class USBUartTypePL2303 : public USBUartTypeCdcAcm { protected: std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; - void enable_channels() override; + bool config_step(USBUartChannel *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; Pl2303ChipType chip_type_{PL2303_TYPE_UNKNOWN}; }; diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index d82a9fdec2..7d98af402d 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -87,7 +87,9 @@ _VALVE_SCHEMA = ( { cv.GenerateID(): cv.declare_id(Valve), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTValveComponent), - cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), + cv.Optional( + CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED + ): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index 08c9f4e011..63d03a889b 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -6,7 +6,7 @@ namespace esphome::valve { -template class OpenAction : public Action { +template class OpenAction final : public Action { public: explicit OpenAction(Valve *valve) : valve_(valve) {} @@ -16,7 +16,7 @@ template class OpenAction : public Action { Valve *valve_; }; -template class CloseAction : public Action { +template class CloseAction final : public Action { public: explicit CloseAction(Valve *valve) : valve_(valve) {} @@ -26,7 +26,7 @@ template class CloseAction : public Action { Valve *valve_; }; -template class StopAction : public Action { +template class StopAction final : public Action { public: explicit StopAction(Valve *valve) : valve_(valve) {} @@ -36,7 +36,7 @@ template class StopAction : public Action { Valve *valve_; }; -template class ToggleAction : public Action { +template class ToggleAction final : public Action { public: explicit ToggleAction(Valve *valve) : valve_(valve) {} @@ -58,7 +58,7 @@ template class ToggleAction : public Action { // (e.g. `const T & &` if Ts already carries a reference, or `const const // T &` if Ts already carries a const). This keeps trigger args no-copy // regardless of whether the trigger supplies `T`, `T &`, or `const T &`. -template class ControlAction : public Action { +template class ControlAction final : public Action { public: using ApplyFn = void (*)(ValveCall &, const std::remove_cvref_t &...); ControlAction(Valve *valve, ApplyFn apply) : valve_(valve), apply_(apply) {} @@ -74,7 +74,7 @@ template class ControlAction : public Action { ApplyFn apply_; }; -template class ValveIsOpenCondition : public Condition { +template class ValveIsOpenCondition final : public Condition { public: ValveIsOpenCondition(Valve *valve) : valve_(valve) {} bool check(const Ts &...x) override { return this->valve_->is_fully_open(); } @@ -83,7 +83,7 @@ template class ValveIsOpenCondition : public Condition { Valve *valve_; }; -template class ValveIsClosedCondition : public Condition { +template class ValveIsClosedCondition final : public Condition { public: ValveIsClosedCondition(Valve *valve) : valve_(valve) {} bool check(const Ts &...x) override { return this->valve_->is_fully_closed(); } @@ -92,7 +92,7 @@ template class ValveIsClosedCondition : public Condition Valve *valve_; }; -class ValveOpenTrigger : public Trigger<> { +class ValveOpenTrigger final : public Trigger<> { public: ValveOpenTrigger(Valve *a_valve) : valve_(a_valve) { a_valve->add_on_state_callback([this]() { @@ -106,7 +106,7 @@ class ValveOpenTrigger : public Trigger<> { Valve *valve_; }; -class ValveClosedTrigger : public Trigger<> { +class ValveClosedTrigger final : public Trigger<> { public: ValveClosedTrigger(Valve *a_valve) : valve_(a_valve) { a_valve->add_on_state_callback([this]() { diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h index 8d372f45d6..a77fc7f56a 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -5,7 +5,7 @@ namespace esphome::vbus { -class DeltaSolBSPlusBSensor : public VBusListener, public Component { +class DeltaSolBSPlusBSensor final : public VBusListener, public Component { public: void dump_config() override; void set_relay1_bsensor(binary_sensor::BinarySensor *bsensor) { this->relay1_bsensor_ = bsensor; } @@ -38,7 +38,7 @@ class DeltaSolBSPlusBSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; -class DeltaSolBS2009BSensor : public VBusListener, public Component { +class DeltaSolBS2009BSensor final : public VBusListener, public Component { public: void dump_config() override; void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } @@ -59,7 +59,7 @@ class DeltaSolBS2009BSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; -class DeltaSolCBSensor : public VBusListener, public Component { +class DeltaSolCBSensor final : public VBusListener, public Component { public: void dump_config() override; void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } @@ -76,7 +76,7 @@ class DeltaSolCBSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; -class DeltaSolCS2BSensor : public VBusListener, public Component { +class DeltaSolCS2BSensor final : public VBusListener, public Component { public: void dump_config() override; void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } @@ -93,7 +93,7 @@ class DeltaSolCS2BSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; -class DeltaSolCS4BSensor : public VBusListener, public Component { +class DeltaSolCS4BSensor final : public VBusListener, public Component { public: void dump_config() override; void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } @@ -110,7 +110,7 @@ class DeltaSolCS4BSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; -class DeltaSolCSPlusBSensor : public VBusListener, public Component { +class DeltaSolCSPlusBSensor final : public VBusListener, public Component { public: void dump_config() override; void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } @@ -127,7 +127,7 @@ class DeltaSolCSPlusBSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; -class DeltaSolBS2BSensor : public VBusListener, public Component { +class DeltaSolBS2BSensor final : public VBusListener, public Component { public: void dump_config() override; void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } @@ -146,7 +146,7 @@ class DeltaSolBS2BSensor : public VBusListener, public Component { class VBusCustomSubBSensor; -class VBusCustomBSensor : public VBusListener, public Component { +class VBusCustomBSensor final : public VBusListener, public Component { public: void dump_config() override; void set_bsensors(std::vector bsensors) { this->bsensors_ = std::move(bsensors); }; @@ -156,7 +156,7 @@ class VBusCustomBSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; -class VBusCustomSubBSensor : public binary_sensor::BinarySensor, public Component { +class VBusCustomSubBSensor final : public binary_sensor::BinarySensor, public Component { public: void set_message_parser(message_parser_t parser) { this->message_parser_ = std::move(parser); }; void parse_message(std::vector &message); diff --git a/esphome/components/vbus/vbus.h b/esphome/components/vbus/vbus.h index ff523178ef..c8cd0cb4a4 100644 --- a/esphome/components/vbus/vbus.h +++ b/esphome/components/vbus/vbus.h @@ -25,7 +25,7 @@ class VBusListener { virtual void handle_message(std::vector &message) = 0; }; -class VBus : public uart::UARTDevice, public Component { +class VBus final : public uart::UARTDevice, public Component { public: void dump_config() override; void loop() override; diff --git a/esphome/components/veml3235/sensor.py b/esphome/components/veml3235/sensor.py index 862fac302f..08d3685d1f 100644 --- a/esphome/components/veml3235/sensor.py +++ b/esphome/components/veml3235/sensor.py @@ -22,13 +22,13 @@ veml3235_ns = cg.esphome_ns.namespace("veml3235") VEML3235Sensor = veml3235_ns.class_( "VEML3235Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) -VEML3235IntegrationTime = veml3235_ns.enum("VEML3235IntegrationTime") +VEML3235ComponentIntegrationTime = veml3235_ns.enum("VEML3235ComponentIntegrationTime") VEML3235_INTEGRATION_TIMES = { - "50ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_50MS, - "100ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_100MS, - "200ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_200MS, - "400ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_400MS, - "800ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_800MS, + "50ms": VEML3235ComponentIntegrationTime.VEML3235_INTEGRATION_TIME_50MS, + "100ms": VEML3235ComponentIntegrationTime.VEML3235_INTEGRATION_TIME_100MS, + "200ms": VEML3235ComponentIntegrationTime.VEML3235_INTEGRATION_TIME_200MS, + "400ms": VEML3235ComponentIntegrationTime.VEML3235_INTEGRATION_TIME_400MS, + "800ms": VEML3235ComponentIntegrationTime.VEML3235_INTEGRATION_TIME_800MS, } VEML3235ComponentDigitalGain = veml3235_ns.enum("VEML3235ComponentDigitalGain") DIGITAL_GAINS = { @@ -40,10 +40,18 @@ GAINS = { "1X": VEML3235ComponentGain.VEML3235_GAIN_1X, "2X": VEML3235ComponentGain.VEML3235_GAIN_2X, "4X": VEML3235ComponentGain.VEML3235_GAIN_4X, - "AUTO": VEML3235ComponentGain.VEML3235_GAIN_AUTO, } -CONFIG_SCHEMA = ( + +def _validate_auto_gain_thresholds(config): + if config[CONF_AUTO_GAIN_THRESHOLD_LOW] >= config[CONF_AUTO_GAIN_THRESHOLD_HIGH]: + raise cv.Invalid( + f"'{CONF_AUTO_GAIN_THRESHOLD_LOW}' must be less than '{CONF_AUTO_GAIN_THRESHOLD_HIGH}'" + ) + return config + + +CONFIG_SCHEMA = cv.All( sensor.sensor_schema( VEML3235Sensor, unit_of_measurement=UNIT_LUX, @@ -67,7 +75,8 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x10)) + .extend(i2c.i2c_device_schema(0x10)), + _validate_auto_gain_thresholds, ) diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp index 59892936b0..b3170469b9 100644 --- a/esphome/components/veml3235/veml3235.cpp +++ b/esphome/components/veml3235/veml3235.cpp @@ -6,6 +6,16 @@ namespace esphome::veml3235 { static const char *const TAG = "veml3235.sensor"; +// ADC counts at or above this value (98% of full scale) are treated as clipped: the true light level cannot +// be estimated from such a reading, so auto-gain restarts from minimum sensitivity instead +static const uint16_t CLIPPED_COUNTS = 64224; + +// Maximum sensitivity multiplier: integration time 800 ms (16x) * gain 4x * digital gain 2x +static const uint16_t MAX_SENSITIVITY_FACTOR = 128; + +// At most one restart from clipping plus one proportional adjustment per update cycle +static const uint8_t MAX_ADJUSTMENTS_PER_UPDATE = 2; + void VEML3235Sensor::setup() { uint8_t device_id[] = {0, 0}; if (!this->refresh_config_reg()) { @@ -22,186 +32,156 @@ void VEML3235Sensor::setup() { } } -bool VEML3235Sensor::refresh_config_reg(bool force_on) { - uint16_t data = this->power_on_ || force_on ? 0 : SHUTDOWN_BITS; +bool VEML3235Sensor::refresh_config_reg() { + uint16_t data = 0x1; // mandatory 1 per RM; shutdown bits cleared (device powered on) - data |= (uint16_t(this->integration_time_ << CONFIG_REG_IT_BIT)); - data |= (uint16_t(this->digital_gain_ << CONFIG_REG_DG_BIT)); - data |= (uint16_t(this->gain_ << CONFIG_REG_G_BIT)); - data |= 0x1; // mandatory 1 here per RM + data |= (uint16_t(this->integration_time_) << CONFIG_REG_IT_BIT); + data |= (uint16_t(this->digital_gain_) << CONFIG_REG_DG_BIT); + data |= (uint16_t(this->gain_) << CONFIG_REG_G_BIT); ESP_LOGVV(TAG, "Writing 0x%.4x to register 0x%.2x", data, CONFIG_REG); return this->write_byte_16(CONFIG_REG, data); } -float VEML3235Sensor::read_lx_() { - if (!this->power_on_) { // if off, turn on - if (!this->refresh_config_reg(true)) { - ESP_LOGW(TAG, "Turning on failed"); - this->status_set_warning(); - return NAN; - } - delay(4); // from RM: a wait time of 4 ms should be observed before the first measurement is picked up, to allow - // for a correct start of the signal processor and oscillator +void VEML3235Sensor::update() { + if (this->measurement_in_progress_) { + ESP_LOGV(TAG, "'%s': Previous measurement still in progress; skipping update", this->get_name().c_str()); + return; } + this->measurement_in_progress_ = true; + this->read_and_publish_(MAX_ADJUSTMENTS_PER_UPDATE); +} +void VEML3235Sensor::read_and_publish_(uint8_t adjustments_left) { uint8_t als_regs[] = {0, 0}; if ((this->read_register(ALS_REG, als_regs, sizeof als_regs) != i2c::ERROR_OK)) { this->status_set_warning(); - return NAN; + this->publish_state(NAN); + this->measurement_in_progress_ = false; + return; } this->status_clear_warning(); - float als_raw_value_multiplier = LUX_MULTIPLIER_BASE; - uint16_t als_raw_value = encode_uint16(als_regs[1], als_regs[0]); - // determine multiplier value based on gains and integration time - if (this->digital_gain_ == VEML3235_DIGITAL_GAIN_1X) { - als_raw_value_multiplier *= 2; - } - switch (this->gain_) { - case VEML3235_GAIN_1X: - als_raw_value_multiplier *= 4; - break; - case VEML3235_GAIN_2X: - als_raw_value_multiplier *= 2; - break; - default: - break; - } - switch (this->integration_time_) { - case VEML3235_INTEGRATION_TIME_50MS: - als_raw_value_multiplier *= 16; - break; - case VEML3235_INTEGRATION_TIME_100MS: - als_raw_value_multiplier *= 8; - break; - case VEML3235_INTEGRATION_TIME_200MS: - als_raw_value_multiplier *= 4; - break; - case VEML3235_INTEGRATION_TIME_400MS: - als_raw_value_multiplier *= 2; - break; - default: - break; - } - // finally, determine and return the actual lux value - float lx = float(als_raw_value) * als_raw_value_multiplier; - ESP_LOGVV(TAG, "'%s': ALS raw = %u, multiplier = %.5f", this->get_name().c_str(), als_raw_value, - als_raw_value_multiplier); - ESP_LOGD(TAG, "'%s': Illuminance = %.4flx", this->get_name().c_str(), lx); + uint16_t als_counts = encode_uint16(als_regs[1], als_regs[0]); - if (!this->power_on_) { // turn off if required - if (!this->refresh_config_reg()) { - ESP_LOGW(TAG, "Turning off failed"); - this->status_set_warning(); + if (this->auto_gain_ && adjustments_left > 0) { + // A sample integrated with the previous settings may still be in the data register after the + // configuration changes, so wait out the old integration period plus two new ones before re-reading + const uint32_t old_integration_time_ms = this->integration_time_ms_(); + if (this->adjust_sensitivity_(als_counts)) { + const uint32_t wait_ms = old_integration_time_ms + 2 * this->integration_time_ms_(); + this->set_timeout("reread", wait_ms, + [this, adjustments_left]() { this->read_and_publish_(adjustments_left - 1); }); + return; } } - if (this->auto_gain_) { - this->adjust_gain_(als_raw_value); - } - - return lx; + float lux = this->counts_to_lux_(als_counts); + ESP_LOGVV(TAG, "'%s': ALS counts = %u, sensitivity = %ux", this->get_name().c_str(), als_counts, + this->sensitivity_factor_()); + ESP_LOGV(TAG, "'%s': Illuminance = %.4flx", this->get_name().c_str(), lux); + this->publish_state(lux); + this->measurement_in_progress_ = false; } -void VEML3235Sensor::adjust_gain_(const uint16_t als_raw_value) { - if ((als_raw_value > UINT16_MAX * this->auto_gain_threshold_low_) && - (als_raw_value < UINT16_MAX * this->auto_gain_threshold_high_)) { - return; +float VEML3235Sensor::counts_to_lux_(uint16_t counts) const { + float resolution = LUX_MULTIPLIER_BASE * (float(MAX_SENSITIVITY_FACTOR) / float(this->sensitivity_factor_())); + return float(counts) * resolution; +} + +uint8_t VEML3235Sensor::gain_factor_() const { + switch (this->gain_) { + case VEML3235_GAIN_4X: + return 4; + case VEML3235_GAIN_2X: + return 2; + default: + return 1; + } +} + +uint16_t VEML3235Sensor::sensitivity_factor_() const { + const uint8_t digital_gain_factor = this->digital_gain_ == VEML3235_DIGITAL_GAIN_2X ? 2 : 1; + return (1 << this->integration_time_) * this->gain_factor_() * digital_gain_factor; +} + +void VEML3235Sensor::set_sensitivity_factor_(uint16_t factor) { + // The factor is a power of two in [1, 128]. Prefer integration time (improves the signal-to-noise ratio), + // then analog gain; digital gain is a plain doubling of the output and is used only as a last resort. + uint8_t it_exponent = 0; // integration time is 2^n * 50 ms + while (it_exponent < VEML3235_INTEGRATION_TIME_800MS && (1u << it_exponent) < factor) { + it_exponent++; + } + this->integration_time_ = static_cast(it_exponent); + factor >>= it_exponent; + + if (factor >= 4) { + this->gain_ = VEML3235_GAIN_4X; + factor >>= 2; + } else if (factor == 2) { + this->gain_ = VEML3235_GAIN_2X; + factor >>= 1; + } else { + this->gain_ = VEML3235_GAIN_1X; } - if (als_raw_value >= UINT16_MAX * 0.9) { // over-saturated, reset all gains and start over - this->digital_gain_ = VEML3235_DIGITAL_GAIN_1X; - this->gain_ = VEML3235_GAIN_1X; - this->integration_time_ = VEML3235_INTEGRATION_TIME_50MS; - this->refresh_config_reg(); - return; + this->digital_gain_ = factor >= 2 ? VEML3235_DIGITAL_GAIN_2X : VEML3235_DIGITAL_GAIN_1X; +} + +bool VEML3235Sensor::adjust_sensitivity_(uint16_t counts) { + // Test for clipping before the window test: with an upper threshold configured at or above the clip + // point, a saturated reading would otherwise count as "in window" and sensitivity would never recover + const bool clipped = counts >= CLIPPED_COUNTS; + const uint16_t low = uint16_t(UINT16_MAX * this->auto_gain_threshold_low_); + const uint16_t high = uint16_t(UINT16_MAX * this->auto_gain_threshold_high_); + if (!clipped && counts >= low && counts <= high) { + return false; } - if (this->gain_ != VEML3235_GAIN_4X) { // increase gain if possible - switch (this->gain_) { - case VEML3235_GAIN_1X: - this->gain_ = VEML3235_GAIN_2X; - break; - case VEML3235_GAIN_2X: - this->gain_ = VEML3235_GAIN_4X; - break; - default: - break; + const uint16_t current_factor = this->sensitivity_factor_(); + uint16_t new_factor; + if (clipped) { + new_factor = 1; + } else if (counts == 0) { + new_factor = MAX_SENSITIVITY_FACTOR; + } else { + // Counts scale linearly with the sensitivity factor: in one step, pick the power of two that puts the + // next reading closest below the middle of the configured window. Rounding down means the target is + // never overshot, which also keeps the sensitivity stable when the window is narrower than one step. + float desired = float(current_factor) * ((float(low) + float(high)) * 0.5f / float(counts)); + desired = clamp(desired, 1.0f, float(MAX_SENSITIVITY_FACTOR)); + new_factor = 1; + while (new_factor * 2 <= uint16_t(desired)) { + new_factor *= 2; } - this->refresh_config_reg(); - return; } - // gain is maxed out; reset it and try to increase digital gain - if (this->digital_gain_ != VEML3235_DIGITAL_GAIN_2X) { // increase digital gain if possible - this->digital_gain_ = VEML3235_DIGITAL_GAIN_2X; - this->gain_ = VEML3235_GAIN_1X; - this->refresh_config_reg(); - return; + + if (new_factor == current_factor) { + return false; } - // digital gain is maxed out; reset it and try to increase integration time - if (this->integration_time_ != VEML3235_INTEGRATION_TIME_800MS) { // increase integration time if possible - switch (this->integration_time_) { - case VEML3235_INTEGRATION_TIME_50MS: - this->integration_time_ = VEML3235_INTEGRATION_TIME_100MS; - break; - case VEML3235_INTEGRATION_TIME_100MS: - this->integration_time_ = VEML3235_INTEGRATION_TIME_200MS; - break; - case VEML3235_INTEGRATION_TIME_200MS: - this->integration_time_ = VEML3235_INTEGRATION_TIME_400MS; - break; - case VEML3235_INTEGRATION_TIME_400MS: - this->integration_time_ = VEML3235_INTEGRATION_TIME_800MS; - break; - default: - break; - } - this->digital_gain_ = VEML3235_DIGITAL_GAIN_1X; - this->gain_ = VEML3235_GAIN_1X; - this->refresh_config_reg(); - return; + + const VEML3235ComponentIntegrationTime old_integration_time = this->integration_time_; + const VEML3235ComponentGain old_gain = this->gain_; + const VEML3235ComponentDigitalGain old_digital_gain = this->digital_gain_; + + this->set_sensitivity_factor_(new_factor); + if (!this->refresh_config_reg()) { + // Keep our state consistent with the device, which still has the old configuration + this->integration_time_ = old_integration_time; + this->gain_ = old_gain; + this->digital_gain_ = old_digital_gain; + this->status_set_warning(); + return false; } + + ESP_LOGV(TAG, "'%s': Sensitivity adjusted from %ux to %ux (ALS counts = %u)", this->get_name().c_str(), + current_factor, new_factor, counts); + return true; } void VEML3235Sensor::dump_config() { - uint8_t digital_gain = 1; - uint8_t gain = 1; - uint16_t integration_time = 0; - - if (this->digital_gain_ == VEML3235_DIGITAL_GAIN_2X) { - digital_gain = 2; - } - switch (this->gain_) { - case VEML3235_GAIN_2X: - gain = 2; - break; - case VEML3235_GAIN_4X: - gain = 4; - break; - default: - break; - } - switch (this->integration_time_) { - case VEML3235_INTEGRATION_TIME_50MS: - integration_time = 50; - break; - case VEML3235_INTEGRATION_TIME_100MS: - integration_time = 100; - break; - case VEML3235_INTEGRATION_TIME_200MS: - integration_time = 200; - break; - case VEML3235_INTEGRATION_TIME_400MS: - integration_time = 400; - break; - case VEML3235_INTEGRATION_TIME_800MS: - integration_time = 800; - break; - default: - break; - } + const uint8_t digital_gain = this->digital_gain_ == VEML3235_DIGITAL_GAIN_2X ? 2 : 1; LOG_SENSOR("", "VEML3235", this); LOG_I2C_DEVICE(this); @@ -212,8 +192,9 @@ void VEML3235Sensor::dump_config() { ESP_LOGCONFIG(TAG, " Auto-gain enabled: %s", YESNO(this->auto_gain_)); if (this->auto_gain_) { ESP_LOGCONFIG(TAG, - " Auto-gain upper threshold: %f%%\n" - " Auto-gain lower threshold: %f%%\n" + " Auto-gain thresholds:\n" + " Upper: %.0f%%\n" + " Lower: %.0f%%\n" " Values below will be used as initial values only", this->auto_gain_threshold_high_ * 100.0f, this->auto_gain_threshold_low_ * 100.0f); } @@ -221,7 +202,7 @@ void VEML3235Sensor::dump_config() { " Digital gain: %uX\n" " Gain: %uX\n" " Integration time: %ums", - digital_gain, gain, integration_time); + digital_gain, this->gain_factor_(), this->integration_time_ms_()); } } // namespace esphome::veml3235 diff --git a/esphome/components/veml3235/veml3235.h b/esphome/components/veml3235/veml3235.h index df88bc6ff5..c19fc17b65 100644 --- a/esphome/components/veml3235/veml3235.h +++ b/esphome/components/veml3235/veml3235.h @@ -1,7 +1,6 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" @@ -16,6 +15,10 @@ static const uint8_t ID_REG = 0x09; // Bit offsets within CONFIG_REG // +// The device expects the low data byte first while write_byte_16() sends the high byte first, so the 16-bit +// configuration word used here is byte-swapped relative to the datasheet: datasheet low-byte bits are word +// bits 15:8 here and datasheet high-byte bits are word bits 7:0. +// static const uint8_t CONFIG_REG_IT_BIT = 12; static const uint8_t CONFIG_REG_DG_BIT = 5; static const uint8_t CONFIG_REG_G_BIT = 3; @@ -23,18 +26,17 @@ static const uint8_t CONFIG_REG_G_BIT = 3; // Other important constants // static const uint8_t DEVICE_ID = 0x35; -static const uint16_t SHUTDOWN_BITS = 0x0018; -// Base multiplier value for lux computation +// Resolution (lx/count) at maximum sensitivity (integration time 800 ms, gain 4x, digital gain 2x) // -static const float LUX_MULTIPLIER_BASE = 0.00213; +static const float LUX_MULTIPLIER_BASE = 0.00213f; // Enum for conversion/integration time settings for the VEML3235. // // Specific values of the enum constants are register values taken from the VEML3235 datasheet. // Longer times mean more accurate results, but will take more energy/more time. // -enum VEML3235ComponentIntegrationTime { +enum VEML3235ComponentIntegrationTime : uint8_t { VEML3235_INTEGRATION_TIME_50MS = 0b000, VEML3235_INTEGRATION_TIME_100MS = 0b001, VEML3235_INTEGRATION_TIME_200MS = 0b010, @@ -45,7 +47,7 @@ enum VEML3235ComponentIntegrationTime { // Enum for digital gain settings for the VEML3235. // Higher values are better for low light situations, but can increase noise. // -enum VEML3235ComponentDigitalGain { +enum VEML3235ComponentDigitalGain : uint8_t { VEML3235_DIGITAL_GAIN_1X = 0b0, VEML3235_DIGITAL_GAIN_2X = 0b1, }; @@ -53,17 +55,17 @@ enum VEML3235ComponentDigitalGain { // Enum for gain settings for the VEML3235. // Higher values are better for low light situations, but can increase noise. // -enum VEML3235ComponentGain { +enum VEML3235ComponentGain : uint8_t { VEML3235_GAIN_1X = 0b00, VEML3235_GAIN_2X = 0b01, VEML3235_GAIN_4X = 0b11, }; -class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { +class VEML3235Sensor final : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - void update() override { this->publish_state(this->read_lx_()); } + void update() override; // Used by ESPHome framework. Does NOT actually set the value on the device. void set_auto_gain(bool auto_gain) { this->auto_gain_ = auto_gain; } @@ -73,7 +75,6 @@ class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2 void set_auto_gain_threshold_low(float auto_gain_threshold_low) { this->auto_gain_threshold_low_ = auto_gain_threshold_low; } - void set_power_on(bool power_on) { this->power_on_ = power_on; } void set_digital_gain(VEML3235ComponentDigitalGain digital_gain) { this->digital_gain_ = digital_gain; } void set_gain(VEML3235ComponentGain gain) { this->gain_ = gain; } void set_integration_time(VEML3235ComponentIntegrationTime integration_time) { @@ -88,19 +89,32 @@ class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2 VEML3235ComponentIntegrationTime integration_time() { return this->integration_time_; } // Updates the configuration register on the device - bool refresh_config_reg(bool force_on = false); + bool refresh_config_reg(); protected: - float read_lx_(); - void adjust_gain_(uint16_t als_raw_value); + // One measurement pass: reads the ALS counts, possibly adjusts the sensitivity and schedules a re-read, + // otherwise publishes the result + void read_and_publish_(uint8_t adjustments_left); + // Chooses a new sensitivity for the given ALS reading and writes it to the device. + // Returns true only if the device configuration was changed. + bool adjust_sensitivity_(uint16_t counts); + float counts_to_lux_(uint16_t counts) const; - bool auto_gain_{true}; - bool power_on_{true}; + // Overall sensitivity multiplier (1x-128x, always a power of two) relative to the least sensitive + // configuration (integration time 50 ms, gain 1x, digital gain 1x). ALS counts scale linearly with it. + uint16_t sensitivity_factor_() const; + void set_sensitivity_factor_(uint16_t factor); + uint8_t gain_factor_() const; + uint16_t integration_time_ms_() const { return 50 << this->integration_time_; } + + // Members are ordered largest to smallest to minimize padding float auto_gain_threshold_high_{0.9}; float auto_gain_threshold_low_{0.2}; VEML3235ComponentDigitalGain digital_gain_{VEML3235_DIGITAL_GAIN_1X}; VEML3235ComponentGain gain_{VEML3235_GAIN_1X}; VEML3235ComponentIntegrationTime integration_time_{VEML3235_INTEGRATION_TIME_50MS}; + bool auto_gain_{true}; + bool measurement_in_progress_{false}; }; } // namespace esphome::veml3235 diff --git a/esphome/components/veml7700/sensor.py b/esphome/components/veml7700/sensor.py index 6ad2eb417f..d0d3584dc2 100644 --- a/esphome/components/veml7700/sensor.py +++ b/esphome/components/veml7700/sensor.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_INFRARED, CONF_INTEGRATION_TIME, CONF_NAME, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, ICON_BRIGHTNESS_5, ICON_BRIGHTNESS_6, @@ -91,7 +92,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_6, accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, @@ -111,7 +112,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_7, accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h index a036bdf002..4a1e25fb8a 100644 --- a/esphome/components/veml7700/veml7700.h +++ b/esphome/components/veml7700/veml7700.h @@ -95,7 +95,7 @@ union PSMRegister { } __attribute__((packed)); }; -class VEML7700Component : public PollingComponent, public i2c::I2CDevice { +class VEML7700Component final : public PollingComponent, public i2c::I2CDevice { public: // // EspHome framework functions diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 7c916f4fde..0aa01685c4 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -22,7 +22,7 @@ struct SequenceStepTimeouts { enum VcselPeriodType { VCSEL_PERIOD_PRE_RANGE, VCSEL_PERIOD_FINAL_RANGE }; -class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { +class VL53L0XSensor final : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: VL53L0XSensor(); diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index f13ea39fa2..76ae145b16 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -978,11 +978,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) { #ifdef USE_SPEAKER // We should never get to this function if there is no speaker anyway if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { - if (this->speaker_buffer_index_ + msg.data_len < SPEAKER_BUFFER_SIZE) { + if (this->speaker_buffer_index_ + msg.data_len <= SPEAKER_BUFFER_SIZE) { memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data, msg.data_len); this->speaker_buffer_index_ += msg.data_len; this->speaker_buffer_size_ += msg.data_len; this->speaker_bytes_received_ += msg.data_len; + this->write_speaker_(); ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data_len); } else { ESP_LOGE(TAG, "Cannot receive audio, buffer is full"); diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 76b076a366..d46b089c2e 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -81,12 +81,6 @@ struct Timer { this->id.c_str(), this->name.c_str(), this->total_seconds, this->seconds_left, YESNO(this->is_active)); return buffer.data(); } - // Remove before 2026.8.0 - ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0") - std::string to_string() const { // NOLINT - char buffer[TO_STR_BUFFER_SIZE]; - return this->to_str(buffer); - } }; struct WakeWord { @@ -110,7 +104,7 @@ enum class MediaPlayerResponseState { }; #endif -class VoiceAssistant : public Component { +class VoiceAssistant final : public Component { public: VoiceAssistant(); @@ -353,7 +347,7 @@ class VoiceAssistant : public Component { #endif }; -template class StartAction : public Action, public Parented { +template class StartAction final : public Action, public Parented { TEMPLATABLE_VALUE(std::string, wake_word); public: @@ -368,22 +362,22 @@ template class StartAction : public Action, public Parent bool silence_detection_; }; -template class StartContinuousAction : public Action, public Parented { +template class StartContinuousAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->request_start(true, true); } }; -template class StopAction : public Action, public Parented { +template class StopAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->request_stop(); } }; -template class IsRunningCondition : public Condition, public Parented { +template class IsRunningCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } }; -template class ConnectedCondition : public Condition, public Parented { +template class ConnectedCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->get_api_connection() != nullptr; } }; diff --git a/esphome/components/wake_on_lan/button.py b/esphome/components/wake_on_lan/button.py index b09e87e811..e1a4e4f4b0 100644 --- a/esphome/components/wake_on_lan/button.py +++ b/esphome/components/wake_on_lan/button.py @@ -8,7 +8,7 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(): - if CORE.is_esp8266 or CORE.is_rp2040: + if CORE.is_esp8266 or CORE.is_rp2: return [] return ["socket"] diff --git a/esphome/components/wake_on_lan/wake_on_lan.h b/esphome/components/wake_on_lan/wake_on_lan.h index 84bc26e064..ddf3433e7d 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.h +++ b/esphome/components/wake_on_lan/wake_on_lan.h @@ -11,7 +11,7 @@ namespace esphome::wake_on_lan { -class WakeOnLanButton : public button::Button, public Component { +class WakeOnLanButton final : public button::Button, public Component { public: void set_macaddr(uint8_t a, uint8_t b, uint8_t c, uint8_t d, uint8_t e, uint8_t f); diff --git a/esphome/components/watchdog/watchdog.cpp b/esphome/components/watchdog/watchdog.cpp index b05d7d4f6d..2063faeb91 100644 --- a/esphome/components/watchdog/watchdog.cpp +++ b/esphome/components/watchdog/watchdog.cpp @@ -9,7 +9,7 @@ #include "esp_idf_version.h" #include "esp_task_wdt.h" #endif -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "hardware/watchdog.h" #include "pico/stdlib.h" #endif @@ -53,7 +53,7 @@ void WatchdogManager::set_timeout_(uint32_t timeout_ms) { esp_task_wdt_reconfigure(&wdt_config); #endif // USE_ESP32 -#ifdef USE_RP2040 +#ifdef USE_RP2 watchdog_enable(timeout_ms, true); #endif } @@ -65,7 +65,7 @@ uint32_t WatchdogManager::get_timeout_() { timeout_ms = (uint32_t) CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; #endif // USE_ESP32 -#ifdef USE_RP2040 +#ifdef USE_RP2 timeout_ms = watchdog_get_count() / 1000; #endif diff --git a/esphome/components/water_heater/__init__.py b/esphome/components/water_heater/__init__.py index f3eec16a40..6bb2b2f8fe 100644 --- a/esphome/components/water_heater/__init__.py +++ b/esphome/components/water_heater/__init__.py @@ -76,7 +76,7 @@ def water_heater_schema( @setup_entity("water_heater") async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None: """Set up the core water heater properties in C++.""" - visual = config[CONF_VISUAL] + visual = config.get(CONF_VISUAL, {}) if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES") cg.add(var.set_visual_min_temperature_override(min_temp)) diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index a1e1ca10a6..995b815440 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -90,10 +90,6 @@ class WaterHeaterCall { float get_target_temperature() const { return this->target_temperature_; } float get_target_temperature_low() const { return this->target_temperature_low_; } float get_target_temperature_high() const { return this->target_temperature_high_; } - /// Get state flags value - ESPDEPRECATED("get_state() is deprecated, use get_away() and get_on() instead. (Removed in 2026.8.0)", "2026.2.0") - uint32_t get_state() const { return this->state_; } - optional get_away() const { if (this->state_mask_ & WATER_HEATER_STATE_AWAY) { return (this->state_ & WATER_HEATER_STATE_AWAY) != 0; @@ -187,6 +183,9 @@ class WaterHeaterTraits { const WaterHeaterModeMask &get_supported_modes() const { return this->supported_modes_; } bool supports_mode(WaterHeaterMode mode) const { return this->supported_modes_.count(mode); } + TemperatureUnit get_temperature_unit() const { return this->temperature_unit_; } + void set_temperature_unit(TemperatureUnit unit) { this->temperature_unit_ = unit; } + protected: // Ordered to minimize padding: 4-byte members first uint32_t feature_flags_{0}; @@ -194,6 +193,7 @@ class WaterHeaterTraits { float max_temperature_{0.0f}; float target_temperature_step_{0.0f}; WaterHeaterModeMask supported_modes_; + TemperatureUnit temperature_unit_{TemperatureUnit::CELSIUS}; }; class WaterHeater : public EntityBase { diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 788bedec34..2587d13b9e 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import gzip import logging +import re import esphome.codegen as cg from esphome.components import web_server_base @@ -24,6 +25,7 @@ from esphome.const import ( CONF_OTA, CONF_PASSWORD, CONF_PORT, + CONF_TYPE, CONF_USERNAME, CONF_VERSION, CONF_WEB_SERVER, @@ -32,7 +34,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_RTL87XX, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -43,9 +45,13 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["json", "web_server_base"] +AUTH_TYPE_BASIC = "basic" +AUTH_TYPE_DIGEST = "digest" + CONF_SORTING_GROUP_ID = "sorting_group_id" CONF_SORTING_GROUPS = "sorting_groups" CONF_SORTING_WEIGHT = "sorting_weight" +CONF_ALLOWED_ORIGINS = "allowed_origins" web_server_ns = cg.esphome_ns.namespace("web_server") @@ -83,6 +89,19 @@ def validate_version_deprecated(config: ConfigType) -> ConfigType: return config +def validate_auth_type_deprecated(auth: ConfigType) -> ConfigType: + # Remove before 2027.1.0: the default auth scheme changes from basic to digest. + if CONF_TYPE not in auth: + _LOGGER.warning( + "The 'web_server' 'auth' scheme currently defaults to 'basic', which sends the " + "password over the network in an easily reversible form. The default will change " + "to 'digest' in ESPHome 2027.1.0. To keep using basic authentication, set " + "'type: basic' under 'auth:' explicitly; otherwise set 'type: digest' now to " + "adopt the more secure scheme." + ) + return auth + + def validate_local(config: ConfigType) -> ConfigType: if CONF_LOCAL in config and config[CONF_VERSION] == 1: raise cv.Invalid("'local' is not supported in version 1") @@ -104,6 +123,41 @@ def validate_ota(config: ConfigType) -> ConfigType: return config +# An Origin header is always "scheme://host[:port]" with no path or trailing slash. +_ORIGIN_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*://[^/\s]+$") + + +def validate_origin(value: str) -> str: + # "*" is the wildcard that allows any origin. + if value == "*": + return value + value = cv.string_strict(value) + if not _ORIGIN_RE.match(value): + raise cv.Invalid( + f"'{value}' is not a valid origin. An origin must be 'scheme://host[:port]' with no " + f"path or trailing slash (e.g. 'https://example.com'), or '*' to allow any origin." + ) + # Browsers send the scheme and host lowercased in the Origin header, so normalize to match. + return value.lower() + + +def validate_private_network_access(config: ConfigType) -> ConfigType: + # PNA preflights are always cross-origin, so they can only be authorized against the + # allowed_origins list. Enabling PNA without any origins would deny every PNA request. + if ( + config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS] + and config.get(CONF_ALLOWED_ORIGINS) is None + ): + raise cv.Invalid( + f"'{CONF_ALLOWED_ORIGINS}' must be set when " + f"'{CONF_ENABLE_PRIVATE_NETWORK_ACCESS}' is enabled. List each origin that is " + f"allowed to reach the device (e.g. 'https://example.com'). '*' allows any origin " + f"but is not recommended.", + path=[CONF_ENABLE_PRIVATE_NETWORK_ACCESS], + ) + return config + + def validate_sorting_groups(config: ConfigType) -> ConfigType: if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3: raise cv.Invalid( @@ -172,7 +226,9 @@ sorting_group = { WEBSERVER_SORTING_SCHEMA = cv.Schema( { - cv.Optional(CONF_WEB_SERVER): cv.Schema( + # The per-entity web_server block is cosmetic dashboard ordering — + # mark the whole block advanced; the children inherit via the cascade. + cv.Optional(CONF_WEB_SERVER, visibility=cv.Visibility.ADVANCED): cv.Schema( { cv.OnlyWith(CONF_WEB_SERVER_ID, "web_server"): cv.use_id(WebServer), cv.Optional(CONF_SORTING_WEIGHT): cv.All( @@ -199,16 +255,25 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CSS_INCLUDE): cv.file_, cv.Optional(CONF_JS_URL): cv.string, cv.Optional(CONF_JS_INCLUDE): cv.file_, - cv.Optional(CONF_ENABLE_PRIVATE_NETWORK_ACCESS, default=True): cv.boolean, - cv.Optional(CONF_AUTH): cv.Schema( - { - cv.Required(CONF_USERNAME): cv.All( - cv.string_strict, cv.Length(min=1) - ), - cv.Required(CONF_PASSWORD): cv.sensitive( - cv.All(cv.string_strict, cv.Length(min=1)) - ), - } + cv.Optional(CONF_ENABLE_PRIVATE_NETWORK_ACCESS, default=False): cv.boolean, + cv.Optional(CONF_ALLOWED_ORIGINS): cv.All( + cv.ensure_list(validate_origin), cv.Length(min=1) + ), + cv.Optional(CONF_AUTH): cv.All( + cv.Schema( + { + cv.Required(CONF_USERNAME): cv.All( + cv.string_strict, cv.Length(min=1) + ), + cv.Required(CONF_PASSWORD): cv.sensitive( + cv.All(cv.string_strict, cv.Length(min=1)) + ), + cv.Optional(CONF_TYPE): cv.one_of( + AUTH_TYPE_BASIC, AUTH_TYPE_DIGEST, lower=True + ), + } + ), + validate_auth_type_deprecated, ), cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( web_server_base.WebServerBase @@ -227,7 +292,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_LN882X, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_RTL87XX, ] ), @@ -236,6 +301,7 @@ CONFIG_SCHEMA = cv.All( validate_local, validate_sorting_groups, validate_ota, + validate_private_network_access, _consume_web_server_sockets, ) @@ -332,10 +398,18 @@ async def to_code(config): request_log_listener() # Request a log listener slot for web server log streaming if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") - if CONF_AUTH in config: + if (allowed_origins := config.get(CONF_ALLOWED_ORIGINS)) is not None: + cg.add_define("USE_WEBSERVER_ALLOWED_ORIGINS") + cg.add(var.set_allowed_origins(allowed_origins)) + if (auth := config.get(CONF_AUTH)) is not None: cg.add_define("USE_WEBSERVER_AUTH") - cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) - cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD])) + # The scheme is fixed at build time so the unused Basic/Digest code path is compiled + # out. Basic is the current default (the absence of this define); an explicit + # 'type: digest' opts in early. Default changes to digest in 2027.1.0. + if auth.get(CONF_TYPE) == AUTH_TYPE_DIGEST: + cg.add_define("USE_WEBSERVER_AUTH_DIGEST") + cg.add(paren.set_auth_username(auth[CONF_USERNAME])) + cg.add(paren.set_auth_password(auth[CONF_PASSWORD])) if CONF_CSS_INCLUDE in config: cg.add_define("USE_WEBSERVER_CSS_INCLUDE") path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index ac2195f387..0c1a6c7f79 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -10,1308 +10,1310 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP constexpr uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xd9, 0x72, 0xdb, 0x48, 0xb6, 0xe0, 0xf3, - 0xd4, 0x57, 0x40, 0x28, 0xb5, 0x8c, 0x2c, 0x26, 0xc1, 0x45, 0x92, 0x2d, 0x83, 0x4a, 0xb2, 0x65, 0xd9, 0xd5, 0x76, - 0x97, 0xb7, 0xb6, 0xec, 0xda, 0x58, 0x6c, 0x09, 0x02, 0x92, 0x44, 0x96, 0x41, 0x80, 0x05, 0x24, 0xb5, 0x14, 0x89, - 0x1b, 0xf3, 0x01, 0x13, 0x31, 0x11, 0xf3, 0x34, 0x2f, 0x13, 0x73, 0x1f, 0xe6, 0x23, 0xe6, 0xf9, 0x7e, 0xca, 0xfd, - 0x81, 0x99, 0x4f, 0x98, 0x38, 0xb9, 0x00, 0x09, 0x2e, 0xb2, 0x5c, 0x55, 0x7d, 0xef, 0x7d, 0x98, 0xa8, 0x28, 0x99, - 0x48, 0xe4, 0x72, 0xf2, 0xe4, 0xc9, 0xb3, 0x67, 0xe2, 0x78, 0x27, 0x4c, 0x03, 0x7e, 0x3b, 0xa3, 0x56, 0xc4, 0xa7, - 0x71, 0xff, 0x58, 0xfd, 0xa5, 0x7e, 0xd8, 0x3f, 0x8e, 0x59, 0xf2, 0xd1, 0xca, 0x68, 0x4c, 0x58, 0x90, 0x26, 0x56, - 0x94, 0xd1, 0x31, 0x09, 0x7d, 0xee, 0x7b, 0x6c, 0xea, 0x4f, 0xa8, 0xd5, 0xea, 0x1f, 0x4f, 0x29, 0xf7, 0xad, 0x20, - 0xf2, 0xb3, 0x9c, 0x72, 0xf2, 0xe1, 0xfd, 0xd7, 0xcd, 0xa3, 0xfe, 0x71, 0x1e, 0x64, 0x6c, 0xc6, 0x2d, 0xe8, 0x92, - 0x4c, 0xd3, 0x70, 0x1e, 0xd3, 0x7e, 0xab, 0x75, 0x7d, 0x7d, 0xed, 0xfe, 0x9c, 0x7f, 0x11, 0xa4, 0x49, 0xce, 0xad, - 0xa7, 0xe4, 0x9a, 0x25, 0x61, 0x7a, 0x8d, 0x73, 0x4e, 0x9e, 0xba, 0x67, 0x91, 0x1f, 0xa6, 0xd7, 0xef, 0xd2, 0x94, - 0xef, 0xed, 0x39, 0xf2, 0xf1, 0xf6, 0xf4, 0xec, 0x8c, 0x10, 0x72, 0x95, 0xb2, 0xd0, 0x6a, 0x2f, 0x97, 0x55, 0xa1, - 0x9b, 0xf8, 0x9c, 0x5d, 0x51, 0xd9, 0x04, 0xed, 0xed, 0xd9, 0x7e, 0x98, 0xce, 0x38, 0x0d, 0xcf, 0xf8, 0x6d, 0x4c, - 0xcf, 0x22, 0x4a, 0x79, 0x6e, 0xb3, 0xc4, 0x7a, 0x9a, 0x06, 0xf3, 0x29, 0x4d, 0xb8, 0x3b, 0xcb, 0x52, 0x9e, 0x02, - 0x24, 0x7b, 0x7b, 0x76, 0x46, 0x67, 0xb1, 0x1f, 0x50, 0x78, 0x7f, 0x7a, 0x76, 0x56, 0xb5, 0xa8, 0x2a, 0xe1, 0x84, - 0x93, 0xb3, 0xdb, 0xe9, 0x65, 0x1a, 0x3b, 0x08, 0x47, 0x9c, 0x24, 0xf4, 0xda, 0xfa, 0x8e, 0xfa, 0x1f, 0x5f, 0xf9, - 0xb3, 0x5e, 0x10, 0xfb, 0x79, 0x6e, 0x9d, 0xf0, 0x85, 0x98, 0x42, 0x36, 0x0f, 0x78, 0x9a, 0x39, 0x1c, 0x53, 0xcc, - 0xd0, 0x82, 0x8d, 0x1d, 0x1e, 0xb1, 0xdc, 0x3d, 0xdf, 0x0d, 0xf2, 0xfc, 0x1d, 0xcd, 0xe7, 0x31, 0xdf, 0x25, 0x3b, - 0x6d, 0xcc, 0x76, 0x08, 0x49, 0x38, 0xe2, 0x51, 0x96, 0x5e, 0x5b, 0xcf, 0xb2, 0x2c, 0xcd, 0x1c, 0xfb, 0xf4, 0xec, - 0x4c, 0xd6, 0xb0, 0x58, 0x6e, 0x25, 0x29, 0xb7, 0xca, 0xfe, 0xfc, 0xcb, 0x98, 0xba, 0xd6, 0x87, 0x9c, 0x5a, 0x17, - 0xf3, 0x24, 0xf7, 0xc7, 0xf4, 0xf4, 0xec, 0xec, 0xc2, 0x4a, 0x33, 0xeb, 0x22, 0xc8, 0xf3, 0x0b, 0x8b, 0x25, 0x39, - 0xa7, 0x7e, 0xe8, 0xda, 0xa8, 0x27, 0x06, 0x0b, 0xf2, 0xfc, 0x3d, 0xbd, 0xe1, 0x84, 0x63, 0xf1, 0xc8, 0x09, 0x2d, - 0x26, 0x94, 0x5b, 0x79, 0x39, 0x2f, 0x07, 0x2d, 0x62, 0xca, 0x2d, 0x4e, 0xc4, 0xfb, 0xb4, 0x27, 0x71, 0x4f, 0xe5, - 0x23, 0xef, 0xb1, 0xb1, 0x93, 0xf3, 0xbd, 0x3d, 0x5e, 0xe2, 0x19, 0xc9, 0xa9, 0x59, 0x8c, 0xd0, 0x1d, 0x5d, 0xb6, - 0xb7, 0x47, 0xdd, 0x98, 0x26, 0x13, 0x1e, 0x11, 0x42, 0x3a, 0x3d, 0xb6, 0xb7, 0xe7, 0x70, 0x12, 0x71, 0x77, 0x42, - 0xb9, 0x43, 0x11, 0xc2, 0x55, 0xeb, 0xbd, 0x3d, 0x47, 0x22, 0x21, 0x25, 0x12, 0x71, 0x35, 0x1c, 0x23, 0x57, 0x61, - 0xff, 0xec, 0x36, 0x09, 0x1c, 0x13, 0x7e, 0x84, 0xd9, 0xde, 0x5e, 0xc4, 0xdd, 0x1c, 0x7a, 0xc4, 0x1c, 0xa1, 0x22, - 0xa3, 0x7c, 0x9e, 0x25, 0x16, 0x2f, 0x78, 0x7a, 0xc6, 0x33, 0x96, 0x4c, 0x1c, 0xb4, 0xd0, 0x65, 0x46, 0xc3, 0xa2, - 0x90, 0xe0, 0xbe, 0xe3, 0x24, 0x21, 0x7d, 0x18, 0xf1, 0x84, 0x3b, 0xb0, 0x8a, 0xe9, 0xd8, 0x4a, 0x08, 0xb1, 0x73, - 0xd1, 0xd6, 0x1e, 0x24, 0x5e, 0xd2, 0xb0, 0x6d, 0x2c, 0xa1, 0xc4, 0x09, 0x47, 0xf8, 0x23, 0x71, 0x12, 0xec, 0xba, - 0x2e, 0x47, 0xa4, 0xbf, 0xd0, 0x58, 0x49, 0x8c, 0x79, 0x0e, 0x92, 0x61, 0x7b, 0xe4, 0x71, 0x37, 0xa3, 0xe1, 0x3c, - 0xa0, 0x8e, 0xc3, 0x70, 0x8e, 0x33, 0x44, 0xfa, 0xac, 0xe1, 0xa4, 0xa4, 0x0f, 0xcb, 0x9d, 0xd6, 0xd7, 0x9a, 0x90, - 0x9d, 0x36, 0x52, 0x30, 0xa6, 0x1a, 0x40, 0xc0, 0xb0, 0x82, 0x27, 0x25, 0xc4, 0x4e, 0xe6, 0xd3, 0x4b, 0x9a, 0xd9, - 0x65, 0xb5, 0x5e, 0x8d, 0x2c, 0xe6, 0x39, 0xb5, 0x82, 0x3c, 0xb7, 0xc6, 0xf3, 0x24, 0xe0, 0x2c, 0x4d, 0x2c, 0xbb, - 0x91, 0x36, 0x6c, 0x49, 0x0e, 0x25, 0x35, 0xd8, 0xa8, 0x40, 0x4e, 0x8e, 0x1a, 0xc9, 0x30, 0x6b, 0x74, 0x46, 0x18, - 0xa0, 0x44, 0x3d, 0xd5, 0x9f, 0x42, 0x00, 0xc5, 0x09, 0xcc, 0xb1, 0xc0, 0x4f, 0x38, 0xcc, 0x52, 0x4c, 0x31, 0xe7, - 0x83, 0xc4, 0x5d, 0xdf, 0x28, 0x84, 0xbb, 0x53, 0x7f, 0xe6, 0x50, 0xd2, 0xa7, 0x82, 0xb8, 0xfc, 0x24, 0x00, 0x58, - 0x6b, 0xeb, 0x36, 0xa0, 0x1e, 0x75, 0x2b, 0x92, 0x42, 0x1e, 0x77, 0xc7, 0x69, 0xf6, 0xcc, 0x0f, 0x22, 0x68, 0x57, - 0x12, 0x4c, 0xa8, 0xf7, 0x5b, 0x90, 0x51, 0x9f, 0xd3, 0x67, 0x31, 0x85, 0x27, 0xc7, 0x16, 0x2d, 0x6d, 0x84, 0x73, - 0xf2, 0xd4, 0x8d, 0x19, 0x7f, 0x9d, 0x26, 0x01, 0xed, 0xe5, 0x06, 0x75, 0x31, 0x58, 0xf7, 0x13, 0xce, 0x33, 0x76, - 0x39, 0xe7, 0xd4, 0xb1, 0x13, 0xa8, 0x61, 0xe3, 0x1c, 0x61, 0xe6, 0x72, 0x7a, 0xc3, 0x4f, 0xd3, 0x84, 0xd3, 0x84, - 0x13, 0xaa, 0x91, 0x8a, 0x13, 0xd7, 0x9f, 0xcd, 0x68, 0x12, 0x9e, 0x46, 0x2c, 0x0e, 0x1d, 0x86, 0x0a, 0x54, 0xe0, - 0x80, 0x13, 0x98, 0x23, 0xe9, 0x27, 0x1e, 0xfc, 0xd9, 0x3e, 0x1b, 0x87, 0x93, 0xbe, 0xd8, 0x14, 0x94, 0xd8, 0x76, - 0x6f, 0x9c, 0x66, 0x8e, 0x9a, 0x81, 0x95, 0x8e, 0x2d, 0x0e, 0x63, 0xbc, 0x9b, 0xc7, 0x34, 0x47, 0xb4, 0x41, 0x58, - 0xb9, 0x8c, 0x0a, 0xc1, 0xef, 0x80, 0xe2, 0x0b, 0xe4, 0x24, 0xc8, 0x4b, 0x7a, 0x57, 0x7e, 0x66, 0xfd, 0xa8, 0x76, - 0xd4, 0xcf, 0x9a, 0x9b, 0x85, 0x9c, 0xfc, 0xec, 0xf2, 0x6c, 0x9e, 0x73, 0x1a, 0xbe, 0xbf, 0x9d, 0xd1, 0x1c, 0x3f, - 0xe7, 0x24, 0xe4, 0x83, 0x90, 0xbb, 0x74, 0x3a, 0xe3, 0xb7, 0x67, 0x82, 0x31, 0x7a, 0xb6, 0x8d, 0xe7, 0x50, 0x33, - 0xa3, 0x7e, 0x00, 0xcc, 0x4c, 0x61, 0xeb, 0x6d, 0x1a, 0xdf, 0x8e, 0x59, 0x1c, 0x9f, 0xcd, 0x67, 0xb3, 0x34, 0xe3, - 0x98, 0x73, 0xb2, 0xe0, 0x69, 0x85, 0x1b, 0x58, 0xcc, 0x45, 0x7e, 0xcd, 0x78, 0x10, 0x39, 0x1c, 0x2d, 0x02, 0x3f, - 0xa7, 0xd6, 0x93, 0x34, 0x8d, 0xa9, 0x0f, 0xb3, 0x4e, 0x06, 0xcf, 0xb9, 0x97, 0xcc, 0xe3, 0xb8, 0x77, 0x99, 0x51, - 0xff, 0x63, 0x4f, 0xbc, 0x7e, 0x73, 0xf9, 0x33, 0x0d, 0xb8, 0x27, 0x7e, 0x9f, 0x64, 0x99, 0x7f, 0x0b, 0x15, 0x09, - 0x81, 0x6a, 0x83, 0xc4, 0xfb, 0xeb, 0xd9, 0x9b, 0xd7, 0xae, 0xdc, 0x25, 0x6c, 0x7c, 0xeb, 0x24, 0xe5, 0xce, 0x4b, - 0x0a, 0x3c, 0xce, 0xd2, 0xe9, 0xca, 0xd0, 0x12, 0x6d, 0x49, 0x6f, 0x0b, 0x08, 0x94, 0x24, 0x3b, 0xb2, 0x6b, 0x13, - 0x82, 0xd7, 0x82, 0xe8, 0xe1, 0x25, 0xd1, 0xe3, 0xce, 0xe3, 0xd8, 0x93, 0xc5, 0x4e, 0x82, 0xee, 0x86, 0x96, 0x67, - 0xb7, 0x0b, 0x4a, 0x04, 0x9c, 0x33, 0x10, 0x31, 0x00, 0x63, 0xe0, 0xf3, 0x20, 0x5a, 0x50, 0xd1, 0x59, 0xa1, 0x21, - 0xa6, 0x45, 0x81, 0x9f, 0x95, 0x04, 0xcf, 0x01, 0x10, 0xc1, 0xa9, 0x08, 0x5f, 0x2e, 0x61, 0xc2, 0x08, 0xff, 0x95, - 0x2c, 0x7c, 0x3d, 0x1f, 0x6f, 0xa7, 0x8d, 0x61, 0x63, 0x7a, 0x92, 0xbd, 0xe0, 0x20, 0x4d, 0xae, 0x68, 0xc6, 0x69, - 0xe6, 0x71, 0x8e, 0x33, 0x3a, 0x8e, 0x01, 0x8c, 0x9d, 0x0e, 0x8e, 0xfc, 0xfc, 0x34, 0xf2, 0x93, 0x09, 0x0d, 0xbd, - 0x67, 0xbc, 0xc0, 0x94, 0x13, 0x7b, 0xcc, 0x12, 0x3f, 0x66, 0xbf, 0xd2, 0xd0, 0x56, 0x02, 0xe1, 0x99, 0x45, 0x6f, - 0x38, 0x4d, 0xc2, 0xdc, 0x7a, 0xfe, 0xfe, 0xd5, 0x4b, 0xb5, 0x94, 0x35, 0x19, 0x81, 0x16, 0xf9, 0x7c, 0x46, 0x33, - 0x07, 0x61, 0x25, 0x23, 0x9e, 0x31, 0xc1, 0x1f, 0x5f, 0xf9, 0x33, 0x59, 0xc2, 0xf2, 0x0f, 0xb3, 0xd0, 0xe7, 0xf4, - 0x2d, 0x4d, 0x42, 0x96, 0x4c, 0xc8, 0x4e, 0x47, 0x96, 0x47, 0xbe, 0x7a, 0x11, 0x96, 0x45, 0xe7, 0xbb, 0xcf, 0x62, - 0x31, 0xf3, 0xf2, 0x71, 0xee, 0xa0, 0x22, 0xe7, 0x3e, 0x67, 0x81, 0xe5, 0x87, 0xe1, 0x8b, 0x84, 0x71, 0x26, 0x00, - 0xcc, 0x60, 0x81, 0x80, 0x4a, 0xa9, 0x94, 0x16, 0x1a, 0x70, 0x07, 0x61, 0xc7, 0x51, 0x32, 0x20, 0x42, 0x6a, 0xc5, - 0xf6, 0xf6, 0x2a, 0x8e, 0x3f, 0xa0, 0x9e, 0x7c, 0x49, 0x86, 0x23, 0xe4, 0xce, 0xe6, 0x39, 0x2c, 0xb5, 0x1e, 0x02, - 0x04, 0x4c, 0x7a, 0x99, 0xd3, 0xec, 0x8a, 0x86, 0x25, 0x79, 0xe4, 0x0e, 0x5a, 0xac, 0x8c, 0xa1, 0x76, 0x06, 0x27, - 0xc3, 0x51, 0xcf, 0x64, 0xdd, 0x54, 0x91, 0x7a, 0x96, 0xce, 0x68, 0xc6, 0x19, 0xcd, 0x4b, 0x6e, 0xe2, 0x80, 0x20, - 0x2d, 0x39, 0x4a, 0x4e, 0xf4, 0xfc, 0x66, 0x0e, 0xc3, 0x14, 0xd5, 0x78, 0x86, 0x96, 0xb5, 0xcf, 0xae, 0x84, 0xd0, - 0xc8, 0x31, 0x43, 0x98, 0x4b, 0x48, 0x73, 0x84, 0x0a, 0x84, 0xb9, 0x06, 0x57, 0x72, 0x23, 0x35, 0xda, 0x2d, 0x48, - 0x6b, 0xf2, 0x57, 0x21, 0xad, 0x81, 0xa7, 0xf9, 0x9c, 0xee, 0xed, 0x39, 0xd4, 0x2d, 0xc9, 0x82, 0xec, 0x74, 0xd4, - 0x1a, 0x19, 0xc8, 0xda, 0x02, 0x36, 0x0c, 0xcc, 0x31, 0x45, 0x78, 0x87, 0xba, 0x49, 0x7a, 0x12, 0x04, 0x34, 0xcf, - 0xd3, 0x6c, 0x6f, 0x6f, 0x47, 0xd4, 0x2f, 0x15, 0x0a, 0x58, 0xc3, 0x37, 0xd7, 0x49, 0x05, 0x01, 0xaa, 0x84, 0xac, - 0x12, 0x0d, 0x1c, 0x44, 0x95, 0xd0, 0x39, 0xec, 0x81, 0xd6, 0x3d, 0x3c, 0xfb, 0xfc, 0xdc, 0x6e, 0x70, 0xac, 0xd0, - 0x30, 0xa1, 0x7a, 0xe8, 0xdb, 0xa7, 0x54, 0x6a, 0x57, 0x42, 0xf7, 0x58, 0xc3, 0x8c, 0xdc, 0x41, 0x6e, 0x48, 0xc7, - 0x2c, 0x31, 0xa6, 0x5d, 0x03, 0x09, 0x73, 0x9c, 0xa3, 0xc2, 0x58, 0xd0, 0x8d, 0x5d, 0x0b, 0xb5, 0x46, 0xae, 0xdc, - 0x62, 0x22, 0x54, 0x09, 0x63, 0x19, 0x87, 0x74, 0x54, 0x60, 0x81, 0x7a, 0x3d, 0x9b, 0x4c, 0x00, 0x3a, 0xe4, 0xa3, - 0x9e, 0x7a, 0x4f, 0x72, 0x89, 0xb9, 0x8c, 0xfe, 0x32, 0xa7, 0x39, 0x97, 0x74, 0xec, 0x70, 0x9c, 0x61, 0x06, 0xfc, - 0x3a, 0x4d, 0xc6, 0x6c, 0x32, 0xcf, 0x40, 0xe3, 0x81, 0xcd, 0x48, 0x93, 0xf9, 0x94, 0xea, 0xa7, 0x4d, 0xb0, 0xbd, - 0x99, 0x81, 0x4c, 0xcc, 0x81, 0xa6, 0xef, 0x26, 0x27, 0x80, 0x95, 0xa3, 0xe5, 0xf2, 0xaf, 0xba, 0x93, 0x6a, 0x29, - 0x4b, 0x2d, 0x6d, 0x65, 0x4d, 0x28, 0x47, 0x4a, 0x26, 0xef, 0x74, 0x14, 0xf8, 0x7c, 0x44, 0x76, 0xda, 0x25, 0x0d, - 0x2b, 0xac, 0x4a, 0x70, 0x24, 0x12, 0xdf, 0xc8, 0xae, 0x90, 0x10, 0xf1, 0x35, 0x72, 0x71, 0xa3, 0x35, 0x4a, 0x8d, - 0xc8, 0x10, 0x94, 0x0d, 0x37, 0x1a, 0x6d, 0x23, 0x27, 0xcd, 0x0f, 0x1c, 0xbe, 0xfe, 0xae, 0x62, 0x1b, 0x57, 0x75, - 0xb6, 0xb1, 0x32, 0x0d, 0x7b, 0x56, 0x36, 0xb1, 0x4b, 0x2a, 0x53, 0x1b, 0xbd, 0x7a, 0x85, 0x99, 0x00, 0xa6, 0x9a, - 0x92, 0xd1, 0xc5, 0x6b, 0x7f, 0x4a, 0x73, 0x87, 0x22, 0xbc, 0xad, 0x82, 0x24, 0x4f, 0xa8, 0x32, 0x32, 0x64, 0x67, - 0x0e, 0xb2, 0x93, 0x21, 0xa9, 0x9a, 0xd5, 0x37, 0x5c, 0x8e, 0xe9, 0x30, 0x1f, 0x55, 0x1a, 0x9d, 0x31, 0x79, 0x21, - 0x94, 0x15, 0x7d, 0x6b, 0xfc, 0xc9, 0x32, 0x89, 0x34, 0xa1, 0x39, 0xe4, 0x08, 0xef, 0xb4, 0x57, 0x57, 0x52, 0xd7, - 0xaa, 0xe6, 0x38, 0x1c, 0xc1, 0x3a, 0x08, 0x91, 0xe1, 0xb2, 0x5c, 0xfc, 0x5b, 0xdb, 0x69, 0x80, 0xb6, 0x33, 0x20, - 0x0c, 0x77, 0x1c, 0xfb, 0xdc, 0xe9, 0xb4, 0xda, 0xa0, 0x8e, 0x5e, 0x51, 0x90, 0x28, 0x08, 0xad, 0x4f, 0x85, 0xba, - 0xf3, 0x24, 0x8f, 0xd8, 0x98, 0x3b, 0x01, 0x17, 0x2c, 0x85, 0xc6, 0x39, 0xb5, 0x78, 0x4d, 0x29, 0x16, 0xec, 0x26, - 0x00, 0x62, 0x2b, 0x35, 0x30, 0xaa, 0x21, 0x15, 0x6c, 0x0b, 0xb8, 0x43, 0xa5, 0x50, 0x57, 0x5c, 0x46, 0xd7, 0x66, - 0xa0, 0x34, 0x76, 0x06, 0xb2, 0x47, 0x4f, 0x31, 0x03, 0x66, 0xe8, 0xad, 0xcc, 0x33, 0x39, 0x84, 0x2a, 0xe4, 0x2e, - 0x4f, 0x5f, 0xa6, 0xd7, 0x34, 0x3b, 0xf5, 0x01, 0x78, 0x4f, 0x36, 0x2f, 0xa4, 0x20, 0x10, 0xfc, 0x9e, 0xf7, 0x34, - 0xbd, 0x9c, 0x8b, 0x89, 0xbf, 0xcd, 0xd2, 0x29, 0xcb, 0x29, 0xa8, 0x6b, 0x12, 0xff, 0x09, 0xec, 0x33, 0xb1, 0x21, - 0x41, 0xd8, 0xd0, 0x92, 0xbe, 0x4e, 0x5e, 0xd6, 0xe9, 0xeb, 0x7c, 0xf7, 0xd9, 0x44, 0x33, 0xc0, 0xfa, 0x36, 0x46, - 0xd8, 0x51, 0x46, 0x85, 0x21, 0xe7, 0xdc, 0x08, 0x29, 0x11, 0xbf, 0x5c, 0x72, 0xc3, 0x76, 0xab, 0x29, 0x8c, 0x54, - 0x6e, 0x1b, 0x54, 0xf8, 0x61, 0x08, 0xaa, 0x5d, 0x96, 0xc6, 0xb1, 0x21, 0xaa, 0x30, 0xeb, 0x95, 0xc2, 0xe9, 0x7c, - 0xf7, 0xd9, 0xd9, 0x5d, 0xf2, 0x09, 0xde, 0x9b, 0x22, 0x4a, 0x03, 0x9a, 0x84, 0x34, 0x03, 0x5b, 0xd2, 0x58, 0x2d, - 0x25, 0x65, 0x4f, 0xd3, 0x24, 0xa1, 0x01, 0xa7, 0x21, 0x98, 0x2a, 0x8c, 0x70, 0x37, 0x4a, 0x73, 0x5e, 0x16, 0x56, - 0xd0, 0x33, 0x03, 0x7a, 0xe6, 0x06, 0x7e, 0x1c, 0x3b, 0xd2, 0x2c, 0x99, 0xa6, 0x57, 0x74, 0x03, 0xd4, 0xbd, 0x1a, - 0xc8, 0x65, 0x37, 0xd4, 0xe8, 0x86, 0xba, 0xf9, 0x2c, 0x66, 0x01, 0x2d, 0x45, 0xd7, 0x99, 0xcb, 0x92, 0x90, 0xde, - 0x00, 0x1f, 0x41, 0xfd, 0x7e, 0xbf, 0x8d, 0x3b, 0xa8, 0x90, 0x08, 0x5f, 0xac, 0x21, 0xf6, 0x0e, 0xa1, 0x09, 0x44, - 0x46, 0xfa, 0x8b, 0x8d, 0x6c, 0x0d, 0x19, 0x92, 0x92, 0x69, 0xf3, 0x4a, 0x72, 0x67, 0x84, 0x43, 0x1a, 0x53, 0x4e, - 0x35, 0x37, 0x07, 0x25, 0x5a, 0x6e, 0xdd, 0x77, 0x25, 0xfe, 0x4a, 0x72, 0xd2, 0xbb, 0x4c, 0xaf, 0x79, 0x5e, 0x9a, - 0xeb, 0xd5, 0xf2, 0x54, 0xd8, 0x1e, 0x70, 0xb9, 0x3c, 0x3e, 0xe7, 0x7e, 0x10, 0x49, 0x3b, 0xdd, 0x59, 0x9b, 0x52, - 0xd5, 0x87, 0xe2, 0xec, 0xe5, 0x26, 0x7a, 0xa2, 0xc1, 0xdc, 0x84, 0x82, 0x33, 0xc5, 0x14, 0x28, 0x98, 0x7e, 0x72, - 0xd9, 0x4e, 0xfd, 0x38, 0xbe, 0xf4, 0x83, 0x8f, 0x75, 0xea, 0xaf, 0xc8, 0x80, 0xac, 0x72, 0x63, 0xe3, 0x95, 0xc1, - 0xb2, 0xcc, 0x79, 0x6b, 0x2e, 0x5d, 0xdb, 0x28, 0xce, 0x4e, 0xbb, 0x22, 0xfb, 0xfa, 0x42, 0x6f, 0xa5, 0x76, 0x01, - 0x11, 0x53, 0x33, 0x73, 0x80, 0x0b, 0x7c, 0x92, 0xe2, 0x34, 0x3f, 0x50, 0x74, 0x07, 0x06, 0x47, 0xb1, 0x02, 0x08, - 0x47, 0x8b, 0x22, 0x64, 0xf9, 0x76, 0x0c, 0xfc, 0x21, 0x50, 0x3e, 0x35, 0x46, 0xb8, 0x2f, 0xa0, 0x25, 0x8f, 0x53, - 0x5a, 0x73, 0x09, 0x99, 0xd2, 0x27, 0x34, 0xa3, 0xf9, 0x06, 0x74, 0x17, 0x41, 0xef, 0x6f, 0xe4, 0x2b, 0xd0, 0xca, - 0x00, 0x8a, 0xbc, 0x67, 0xaa, 0x13, 0x35, 0x0a, 0x50, 0x3c, 0x95, 0x09, 0x91, 0x9b, 0xd5, 0x2c, 0x48, 0xa5, 0xb1, - 0x4b, 0x23, 0x5c, 0xb1, 0xdc, 0x94, 0x38, 0x8e, 0x93, 0x83, 0x11, 0xa7, 0x75, 0xfb, 0x6a, 0x12, 0xf9, 0xda, 0x24, - 0x72, 0xd7, 0x30, 0xb4, 0x50, 0x45, 0xcb, 0x46, 0x73, 0x8f, 0x73, 0x64, 0xd6, 0x02, 0x7d, 0xd5, 0x05, 0x06, 0x8d, - 0x4a, 0x7e, 0x1b, 0x13, 0x8e, 0x53, 0x65, 0xe5, 0x28, 0x52, 0x03, 0x8e, 0x51, 0x35, 0xc9, 0x90, 0xdc, 0x1b, 0x35, - 0x93, 0x37, 0xc3, 0x29, 0x5a, 0x51, 0xee, 0x8b, 0x42, 0x21, 0x89, 0x22, 0xb5, 0x38, 0x35, 0xad, 0xd8, 0x40, 0x0b, - 0xce, 0x88, 0xd2, 0x84, 0xa5, 0xe2, 0xb3, 0x8a, 0x9c, 0xb2, 0xdf, 0x1d, 0x42, 0xb2, 0x0a, 0x37, 0x35, 0x95, 0x52, - 0xeb, 0x56, 0x19, 0xc2, 0x91, 0x56, 0x4a, 0xd3, 0x6a, 0xe2, 0x84, 0xd8, 0xda, 0x27, 0x61, 0x0f, 0x16, 0x35, 0xbb, - 0xd0, 0x33, 0xaa, 0x15, 0x1e, 0xf0, 0xd4, 0x74, 0x13, 0xbe, 0x37, 0x11, 0x4d, 0xad, 0x1f, 0x03, 0xe3, 0x69, 0x0d, - 0xe3, 0x06, 0x6a, 0x33, 0xc9, 0xbb, 0xb2, 0x11, 0x89, 0xea, 0x8d, 0x1d, 0x8a, 0x53, 0xb9, 0x10, 0x6b, 0x58, 0x5c, - 0x55, 0x3e, 0x05, 0x11, 0x82, 0x19, 0x9b, 0x83, 0x7a, 0x67, 0x4a, 0x08, 0x07, 0x80, 0x67, 0xcb, 0xe5, 0x1a, 0xd9, - 0x6d, 0xd4, 0x41, 0x91, 0x5b, 0x59, 0x86, 0xcb, 0xe5, 0x33, 0x8e, 0x1c, 0xa5, 0xfd, 0x62, 0x8a, 0x06, 0x9a, 0xe7, - 0x9e, 0xbc, 0x84, 0x5a, 0x42, 0x19, 0xad, 0x4a, 0x4a, 0xb3, 0xa1, 0x4e, 0xb5, 0xf5, 0x85, 0xe2, 0x06, 0xe3, 0x3e, - 0x5d, 0xe3, 0x5f, 0xa2, 0x50, 0x09, 0xea, 0x6a, 0xca, 0xa7, 0xaa, 0x6b, 0x86, 0x10, 0xf2, 0x72, 0x61, 0xc9, 0xec, - 0x6c, 0x32, 0x2e, 0xf7, 0xf6, 0x72, 0xa3, 0xa3, 0xf3, 0x92, 0x51, 0xfc, 0xec, 0x80, 0x50, 0xce, 0x6f, 0x13, 0xa1, - 0xbd, 0xfc, 0xac, 0xc5, 0xd0, 0x9a, 0x69, 0xda, 0xee, 0x81, 0x4d, 0xee, 0x5f, 0xfb, 0x8c, 0x5b, 0x65, 0x2f, 0xd2, - 0x26, 0x77, 0x28, 0x5a, 0x28, 0x65, 0xc3, 0xcd, 0x28, 0xa8, 0x8f, 0xc0, 0x15, 0xb4, 0x12, 0x2d, 0x09, 0x3f, 0x88, - 0x28, 0xf8, 0x83, 0xb5, 0x1e, 0x51, 0xda, 0x86, 0x3b, 0x4a, 0x8e, 0xa8, 0x8e, 0x37, 0xc3, 0x5e, 0xac, 0x36, 0xaf, - 0xd9, 0x02, 0x33, 0x9a, 0x8d, 0xd3, 0x6c, 0xaa, 0xdf, 0x15, 0x2b, 0xcf, 0x8a, 0x37, 0xb2, 0xb1, 0xb3, 0xb1, 0x6f, - 0x65, 0x01, 0xf4, 0x56, 0x0c, 0xef, 0xca, 0x64, 0xaf, 0x09, 0xd3, 0x52, 0xfe, 0x4a, 0xb7, 0xa0, 0xa6, 0xcc, 0xdc, - 0x34, 0xf1, 0x95, 0x4f, 0xb5, 0x27, 0xdd, 0x26, 0x3b, 0x9d, 0x5e, 0x69, 0xf7, 0x69, 0x6a, 0xe8, 0x49, 0xf7, 0x86, - 0x12, 0xaa, 0xe9, 0x3c, 0x0e, 0x15, 0xb0, 0x0c, 0x61, 0xaa, 0xe8, 0xe8, 0x9a, 0xc5, 0x71, 0x55, 0xfa, 0x39, 0x9c, - 0x3d, 0x57, 0x9c, 0x3d, 0xd3, 0x9c, 0x1d, 0x58, 0x05, 0x70, 0x76, 0xd9, 0x5d, 0xd5, 0x3c, 0x5b, 0xdb, 0x9e, 0x99, - 0xe4, 0xe9, 0xb9, 0xb0, 0xa5, 0x61, 0xbc, 0xb9, 0x86, 0x00, 0x95, 0xba, 0xd7, 0x47, 0x47, 0xb9, 0x62, 0xc0, 0x08, - 0x94, 0x9e, 0x4c, 0x6a, 0xba, 0x29, 0x3e, 0x3a, 0x08, 0xe7, 0x05, 0x2d, 0x29, 0xfb, 0xe4, 0x19, 0xf8, 0xea, 0x8c, - 0xe9, 0x80, 0x18, 0x13, 0xc5, 0x9f, 0xa5, 0x46, 0xe9, 0xd9, 0x31, 0x35, 0xbb, 0x5c, 0xcf, 0x0e, 0x78, 0x7d, 0x35, - 0xbb, 0xf0, 0x6e, 0x6e, 0x2f, 0xa6, 0xc7, 0xca, 0xe9, 0x55, 0xeb, 0xbd, 0x5c, 0x3a, 0x2b, 0x25, 0xe0, 0xc6, 0x57, - 0x46, 0x4a, 0x56, 0xf6, 0x0e, 0x3c, 0xc0, 0xc4, 0x0c, 0x14, 0x14, 0x72, 0xd2, 0xa5, 0x90, 0x7b, 0xf9, 0x29, 0x27, - 0x8f, 0xf0, 0xd6, 0xcb, 0xf6, 0xa7, 0xe9, 0x74, 0x06, 0xfa, 0xd8, 0x0a, 0x49, 0x4f, 0xa8, 0x1a, 0xb0, 0x7a, 0x5f, - 0x6c, 0x28, 0xab, 0xb5, 0x11, 0xfb, 0xb1, 0x46, 0x4d, 0xa5, 0xcd, 0xbc, 0xd3, 0x2e, 0xe6, 0x65, 0x51, 0xc9, 0x38, - 0x36, 0x39, 0x56, 0x4e, 0x57, 0xdd, 0x32, 0xfa, 0xc5, 0x1b, 0x87, 0x49, 0x3e, 0xcc, 0x80, 0xd7, 0x19, 0xec, 0x47, - 0x93, 0xbb, 0xb9, 0xfe, 0x45, 0x85, 0x9c, 0x45, 0xb1, 0x82, 0xbe, 0x45, 0x51, 0x3c, 0x53, 0x76, 0x36, 0x7e, 0xb6, - 0xdd, 0x20, 0xae, 0xde, 0x29, 0x7b, 0x71, 0x38, 0xc2, 0xcf, 0xd6, 0xb5, 0x47, 0xb2, 0x98, 0xa6, 0x21, 0xf5, 0xec, - 0x74, 0x46, 0x13, 0xbb, 0x00, 0xef, 0xaa, 0x5a, 0xfc, 0x39, 0x77, 0x16, 0xef, 0xea, 0x6e, 0x56, 0xef, 0x59, 0x01, - 0x2e, 0xb0, 0x1f, 0xd7, 0x1d, 0xb0, 0xdf, 0xd2, 0x2c, 0x17, 0xba, 0x68, 0xa9, 0xd6, 0xfe, 0x58, 0x09, 0xa6, 0x1f, - 0xbd, 0xad, 0xf5, 0x2b, 0x2b, 0xc4, 0xee, 0xb8, 0x0f, 0xdd, 0x7d, 0x1b, 0x09, 0xf7, 0xf0, 0x37, 0x6a, 0xc7, 0xff, - 0xa2, 0xdd, 0xc3, 0x67, 0xe4, 0x97, 0xba, 0x77, 0x78, 0xc6, 0xc9, 0xd9, 0xe0, 0x4c, 0x1b, 0xcd, 0x69, 0xcc, 0x82, - 0x5b, 0xc7, 0x8e, 0x19, 0x6f, 0x42, 0x08, 0xce, 0xc6, 0x0b, 0xf9, 0x02, 0xfc, 0x8a, 0xc2, 0xad, 0x5d, 0x68, 0x73, - 0x0f, 0x33, 0x4e, 0xec, 0xdd, 0x98, 0xf1, 0x5d, 0x1b, 0xdf, 0x92, 0x0b, 0xf8, 0xb1, 0xbb, 0x70, 0x5e, 0xf9, 0x3c, - 0x72, 0x33, 0x3f, 0x09, 0xd3, 0xa9, 0x83, 0x1a, 0xb6, 0x8d, 0xdc, 0x5c, 0x98, 0x1c, 0x8f, 0x51, 0xb1, 0x7b, 0x81, - 0xcf, 0x38, 0xb1, 0x07, 0x76, 0xe3, 0x16, 0xbf, 0xe2, 0xe4, 0xe2, 0x78, 0x77, 0x71, 0xc6, 0x8b, 0xfe, 0x05, 0x3e, - 0x29, 0x3d, 0xf7, 0xf8, 0x35, 0x71, 0x10, 0xe9, 0x9f, 0x28, 0x68, 0x4e, 0xd3, 0xa9, 0xf4, 0xe0, 0xdb, 0x08, 0xbf, - 0x13, 0xf1, 0x95, 0x8a, 0xdd, 0xa8, 0x10, 0xcb, 0x0e, 0xb1, 0x53, 0xe1, 0x25, 0xb0, 0xf7, 0xf6, 0x8c, 0xb2, 0x52, - 0x59, 0xc0, 0xa7, 0x9c, 0xd4, 0x6c, 0x72, 0xfc, 0x52, 0x44, 0x6a, 0x4e, 0xb9, 0x93, 0x20, 0xdd, 0x8d, 0xa3, 0xdd, - 0xd1, 0x6a, 0x6f, 0x26, 0x43, 0xe9, 0x64, 0x70, 0x19, 0xa7, 0x99, 0xcf, 0xd3, 0x6c, 0x84, 0x4c, 0x05, 0x04, 0xff, - 0x8d, 0x5c, 0x0c, 0xad, 0xff, 0xf4, 0xc5, 0x4f, 0xe3, 0x9f, 0xb2, 0xd1, 0x05, 0xfe, 0x40, 0x5a, 0xc7, 0xce, 0xc0, - 0x73, 0x76, 0x9a, 0xcd, 0xe5, 0x4f, 0xad, 0xe1, 0xdf, 0xfd, 0xe6, 0xaf, 0x27, 0xcd, 0x1f, 0x47, 0x68, 0xe9, 0xfc, - 0xd4, 0x1a, 0x0c, 0xd5, 0xd3, 0xf0, 0xef, 0xfd, 0x9f, 0xf2, 0xd1, 0x57, 0xb2, 0x70, 0x17, 0xa1, 0xd6, 0x04, 0x4f, - 0x38, 0x69, 0x35, 0x9b, 0xfd, 0xd6, 0x04, 0x4f, 0x39, 0x69, 0xc1, 0xbf, 0xd7, 0xe4, 0x1d, 0x9d, 0x3c, 0xbb, 0x99, - 0x39, 0x17, 0xfd, 0xe5, 0xee, 0xe2, 0x6f, 0x05, 0xf4, 0x3a, 0xfc, 0xfb, 0x4f, 0x3f, 0xe5, 0xf6, 0x83, 0x3e, 0x69, - 0x8d, 0x1a, 0xc8, 0x81, 0xd2, 0xaf, 0x88, 0xf8, 0xeb, 0x0c, 0xbc, 0xe1, 0xdf, 0x15, 0x14, 0xf6, 0x83, 0x9f, 0x2e, - 0x8e, 0xfb, 0x64, 0xb4, 0x74, 0xec, 0xe5, 0x03, 0xb4, 0x44, 0x68, 0xb9, 0x8b, 0x2e, 0xb0, 0x3d, 0xb1, 0x11, 0x1e, - 0x73, 0xd2, 0x7a, 0xd0, 0x9a, 0xe0, 0x73, 0x4e, 0x5a, 0x76, 0x6b, 0x82, 0xdf, 0x70, 0xd2, 0xfa, 0xbb, 0x33, 0xf0, - 0xa4, 0x9b, 0x6d, 0x29, 0x3c, 0x1c, 0x4b, 0x08, 0x72, 0xf8, 0x19, 0xf5, 0x97, 0x9c, 0xf1, 0x98, 0xa2, 0xdd, 0x16, - 0xc3, 0x1f, 0x05, 0x9a, 0x1c, 0x0e, 0x7e, 0x18, 0x30, 0xef, 0x9c, 0xc5, 0x39, 0x2c, 0x36, 0xd0, 0xcc, 0xae, 0x97, - 0x60, 0xe9, 0x0a, 0xc8, 0x3d, 0x8e, 0xaf, 0xfc, 0x78, 0x4e, 0x73, 0x8f, 0x16, 0x08, 0xc7, 0xe4, 0x23, 0x77, 0x3a, - 0x08, 0xbf, 0xe0, 0xf0, 0xa3, 0x8b, 0xf0, 0xa9, 0x0a, 0x64, 0xc2, 0x4e, 0x96, 0x44, 0x95, 0xa4, 0x52, 0x65, 0xb1, - 0x11, 0x9e, 0x6c, 0x78, 0xc9, 0x23, 0x70, 0x30, 0x20, 0x7c, 0x55, 0x0b, 0x7b, 0xe2, 0x1b, 0xa2, 0x49, 0xe2, 0x7d, - 0x46, 0xe9, 0x77, 0x7e, 0xfc, 0x91, 0x66, 0xce, 0x09, 0xee, 0x74, 0x1f, 0x63, 0xe1, 0x87, 0xde, 0xe9, 0xa0, 0x5e, - 0x19, 0xb3, 0x7a, 0xcb, 0x65, 0xa8, 0x00, 0xa4, 0x6c, 0xdd, 0x1d, 0x03, 0x2b, 0xbe, 0x93, 0xac, 0xf9, 0xac, 0x32, - 0xff, 0xda, 0x46, 0xf5, 0xf8, 0x28, 0x4b, 0xae, 0xfc, 0x98, 0x85, 0x16, 0xa7, 0xd3, 0x59, 0xec, 0x73, 0x6a, 0xa9, - 0xf9, 0x5a, 0x3e, 0x74, 0x64, 0x97, 0x3a, 0xc3, 0xcc, 0xb0, 0x39, 0x67, 0x3a, 0xf0, 0x04, 0x7b, 0xc5, 0x81, 0x28, - 0x95, 0xd2, 0x3b, 0x9e, 0x56, 0x41, 0xb0, 0xd5, 0x38, 0x5f, 0xb3, 0x03, 0xbe, 0xb0, 0x91, 0x90, 0xcf, 0x39, 0xce, - 0x08, 0x48, 0xd1, 0xee, 0xc0, 0x3e, 0xce, 0xaf, 0x26, 0x7d, 0x1b, 0x62, 0x34, 0x29, 0xf9, 0x20, 0x5c, 0x43, 0x50, - 0x21, 0x22, 0xed, 0x5e, 0x74, 0x4c, 0x7b, 0x51, 0xa3, 0xa1, 0xb5, 0x68, 0x9f, 0x24, 0xc3, 0x48, 0x36, 0x0f, 0x70, - 0x88, 0xe7, 0xa4, 0xd9, 0xc1, 0x33, 0xd2, 0x16, 0x4d, 0x7a, 0xb3, 0x63, 0x5f, 0x0d, 0xb3, 0xb7, 0xe7, 0xa4, 0x6e, - 0xec, 0xe7, 0xfc, 0x05, 0xd8, 0xfb, 0x64, 0x86, 0x43, 0x92, 0xba, 0xf4, 0x86, 0x06, 0x8e, 0x8f, 0x70, 0xa8, 0x38, - 0x0d, 0xea, 0xa1, 0x19, 0x31, 0xaa, 0x81, 0x19, 0x41, 0x3e, 0x0c, 0xc2, 0x61, 0x67, 0x44, 0x08, 0xb1, 0x77, 0x9a, - 0x4d, 0x7b, 0x90, 0x92, 0x09, 0xf7, 0xa0, 0xc4, 0x50, 0x96, 0xc9, 0x14, 0x8a, 0xba, 0x46, 0x91, 0xf3, 0x86, 0xbb, - 0x9c, 0xe6, 0xdc, 0x81, 0x62, 0xf0, 0x00, 0xe4, 0x9a, 0xb0, 0xed, 0xe3, 0x96, 0xdd, 0x80, 0x52, 0x41, 0x9c, 0x08, - 0xa7, 0xe4, 0x1a, 0x79, 0xe1, 0x70, 0x7f, 0x64, 0x0a, 0x00, 0x51, 0x08, 0x83, 0x5f, 0x0f, 0xc2, 0x61, 0x5b, 0x0c, - 0xde, 0xb7, 0x07, 0x4e, 0x4a, 0x72, 0xa9, 0xa1, 0x0d, 0x72, 0xef, 0x83, 0x98, 0x2a, 0xf2, 0x14, 0x70, 0x6a, 0xdc, - 0x39, 0x69, 0x76, 0x3d, 0x67, 0x6e, 0x4e, 0xa2, 0x09, 0x83, 0x29, 0x2c, 0xe0, 0x80, 0x40, 0x7d, 0x9c, 0x12, 0x18, - 0xb1, 0x6a, 0x76, 0xed, 0xa9, 0xe7, 0x07, 0xf6, 0x83, 0xc1, 0x39, 0xf7, 0xc6, 0x5c, 0x0e, 0x7f, 0xce, 0x97, 0x4b, - 0xf8, 0x77, 0xcc, 0x07, 0x29, 0xb9, 0x16, 0x45, 0x13, 0x55, 0x34, 0x85, 0xa2, 0x0f, 0x1e, 0x80, 0x8a, 0xf3, 0x52, - 0xcb, 0x92, 0x6b, 0x32, 0x25, 0x02, 0xf6, 0xbd, 0xbd, 0x64, 0x18, 0x35, 0x3a, 0x23, 0x70, 0xf2, 0x67, 0x3c, 0xff, - 0x8e, 0xf1, 0xc8, 0xb1, 0x5b, 0x7d, 0x1b, 0x0d, 0x6c, 0x0b, 0x96, 0xb6, 0x97, 0x35, 0x88, 0xc4, 0xb0, 0xdf, 0x78, - 0xc5, 0xbd, 0x79, 0x9f, 0xb4, 0x07, 0x0e, 0x53, 0x2e, 0x3d, 0x84, 0x7d, 0xc5, 0x38, 0xdb, 0x78, 0x8e, 0x1a, 0x8c, - 0x37, 0xf4, 0xf3, 0x1c, 0x35, 0x6e, 0x1b, 0x53, 0xe4, 0xf9, 0x8d, 0xdb, 0x86, 0x33, 0x27, 0x84, 0x34, 0xbb, 0x65, - 0x33, 0x2d, 0xfe, 0x22, 0xe4, 0x4d, 0xb5, 0xbf, 0x73, 0x28, 0xb6, 0x43, 0xd6, 0x70, 0x92, 0x21, 0x1d, 0x2d, 0x97, - 0xf6, 0xf1, 0xa0, 0x6f, 0xa3, 0x86, 0xa3, 0x09, 0xad, 0xa5, 0x29, 0x0d, 0x21, 0xcc, 0x46, 0x85, 0x8a, 0x27, 0x3d, - 0xa9, 0xc5, 0x8e, 0x16, 0xd5, 0x66, 0x37, 0x78, 0x00, 0x2d, 0x4a, 0x43, 0x46, 0x2a, 0xac, 0x33, 0x98, 0xa6, 0x26, - 0xe6, 0x8c, 0xb4, 0x71, 0x4a, 0xb4, 0xfb, 0x3a, 0x22, 0xbc, 0x22, 0x78, 0x9f, 0x54, 0xd5, 0xf1, 0x30, 0xc0, 0xe1, - 0x88, 0x3c, 0x95, 0x06, 0x49, 0x4f, 0x3b, 0xc7, 0x69, 0x4c, 0x9e, 0xac, 0x44, 0x71, 0x03, 0x08, 0xb0, 0xdc, 0xb8, - 0xc1, 0x3c, 0xcb, 0x68, 0xc2, 0x5f, 0xa7, 0xa1, 0xd2, 0xd3, 0x68, 0x0c, 0xa6, 0x12, 0x84, 0x67, 0x31, 0x28, 0x69, - 0x5d, 0xbd, 0x33, 0xe6, 0x6b, 0xaf, 0x67, 0x64, 0x2e, 0xf5, 0x27, 0x11, 0xb4, 0xed, 0xcd, 0x94, 0x65, 0xec, 0x20, - 0x3c, 0x57, 0xd1, 0x5c, 0xc7, 0x75, 0xdd, 0x99, 0x1b, 0xc0, 0x6b, 0x18, 0x20, 0x47, 0x85, 0xd8, 0x47, 0x4e, 0x4e, - 0x6e, 0xdc, 0x84, 0xde, 0x88, 0x51, 0x1d, 0x54, 0x49, 0x66, 0xbd, 0xbd, 0x8e, 0xa3, 0x9e, 0x60, 0x37, 0xb9, 0x9b, - 0xa4, 0x21, 0x05, 0xf4, 0x40, 0xfc, 0x5e, 0x15, 0x45, 0x7e, 0x6e, 0x06, 0xa9, 0x2a, 0xf8, 0x86, 0xa6, 0xff, 0x7a, - 0x06, 0x4e, 0x5f, 0xa1, 0x6c, 0x95, 0x95, 0xa5, 0x27, 0x1c, 0x21, 0x36, 0x76, 0x66, 0x2e, 0x04, 0xf7, 0x04, 0x09, - 0x31, 0xb0, 0xe5, 0x66, 0x26, 0x51, 0xdd, 0x96, 0x7d, 0x4e, 0x49, 0x38, 0x4c, 0x1b, 0x0d, 0xe1, 0x88, 0x9e, 0x4b, - 0x92, 0x98, 0x21, 0x3c, 0x2d, 0xf7, 0x96, 0xae, 0xf7, 0x96, 0xd4, 0x47, 0x72, 0xa6, 0x75, 0x87, 0x6e, 0x83, 0x71, - 0x24, 0x7c, 0x85, 0xdc, 0xb9, 0x45, 0x78, 0x4c, 0x5a, 0xce, 0xd0, 0x1d, 0xfc, 0x79, 0x84, 0x06, 0x8e, 0xfb, 0x15, - 0x6a, 0x49, 0xc6, 0x31, 0x45, 0x3d, 0x5f, 0x0e, 0xb1, 0x10, 0x51, 0xcc, 0x0e, 0x16, 0xbe, 0x44, 0x2f, 0xc3, 0x89, - 0x3f, 0xa5, 0xde, 0x18, 0xf6, 0xb8, 0xa6, 0x9b, 0xb7, 0x18, 0xe8, 0xc8, 0x1b, 0x2b, 0x4e, 0xe2, 0xda, 0x83, 0x5f, - 0x78, 0xf9, 0x34, 0xb0, 0x07, 0x5f, 0x57, 0x4f, 0x7f, 0xb6, 0x07, 0xdf, 0x72, 0xef, 0xdb, 0x42, 0xb9, 0xbb, 0x6b, - 0x43, 0x3c, 0xd4, 0x43, 0x14, 0x72, 0x61, 0x0c, 0xcc, 0xcd, 0xd1, 0xba, 0xa3, 0x63, 0x86, 0x0a, 0x36, 0x2e, 0x59, - 0x51, 0xee, 0x72, 0x7f, 0x02, 0x28, 0x35, 0x56, 0x20, 0x37, 0xa3, 0xfb, 0xd5, 0x84, 0x81, 0x50, 0x34, 0xb5, 0x02, - 0x2a, 0x67, 0xfd, 0x36, 0x5a, 0xd4, 0xea, 0x0a, 0x8d, 0xa9, 0x1e, 0x4d, 0x2f, 0xb9, 0xf4, 0x94, 0xb4, 0x7b, 0xd3, - 0xe3, 0x59, 0x6f, 0xda, 0x68, 0xa0, 0x5c, 0x13, 0xd6, 0x7c, 0x38, 0x1d, 0xe1, 0xd7, 0xe0, 0xd5, 0x33, 0x29, 0x09, - 0xd7, 0xa6, 0xd7, 0x55, 0xd3, 0x6b, 0x34, 0xb2, 0x02, 0xf5, 0x8c, 0xa6, 0x33, 0xd9, 0xb4, 0x28, 0x24, 0x4e, 0x56, - 0x09, 0xed, 0x08, 0x89, 0x12, 0x48, 0x89, 0x22, 0x84, 0x9c, 0x71, 0xb4, 0xb1, 0x57, 0xe8, 0x13, 0x9a, 0x8b, 0x1d, - 0x0b, 0xcc, 0x53, 0xca, 0x08, 0x07, 0xb0, 0x00, 0x4d, 0x4b, 0x57, 0xf0, 0x2d, 0x9e, 0x37, 0x3a, 0x82, 0xc8, 0x9b, - 0x9d, 0x5e, 0xbd, 0xaf, 0x47, 0x55, 0x5f, 0x78, 0xde, 0x20, 0xb7, 0x25, 0x96, 0x8a, 0xac, 0xd1, 0x28, 0xea, 0xf1, - 0x4e, 0xbd, 0x6f, 0x6b, 0x11, 0x88, 0x93, 0xd5, 0xd4, 0x0c, 0x2d, 0x5f, 0x2b, 0x89, 0xca, 0x5c, 0x96, 0x24, 0x34, - 0x03, 0x19, 0x4a, 0x38, 0x66, 0x45, 0x51, 0xca, 0xf5, 0x37, 0x20, 0x44, 0x31, 0x25, 0x09, 0xf0, 0x1d, 0x61, 0x76, - 0xe1, 0x0c, 0xa7, 0x38, 0x12, 0x5c, 0x83, 0x10, 0x72, 0xaa, 0x93, 0x5a, 0xb8, 0xe0, 0x40, 0x3e, 0x61, 0x86, 0x44, - 0xca, 0x09, 0x75, 0xcf, 0x77, 0x4f, 0xd3, 0x3b, 0x4d, 0xb2, 0x21, 0x1b, 0x79, 0xa2, 0x5a, 0xac, 0xf8, 0x56, 0x40, - 0xde, 0x39, 0x1c, 0x95, 0xe1, 0x11, 0x57, 0xb0, 0xbf, 0xa7, 0x2c, 0xa3, 0x42, 0x03, 0xdf, 0xd5, 0x66, 0x9f, 0x5f, - 0x57, 0x1f, 0x7d, 0xd3, 0x79, 0x03, 0x88, 0x0c, 0xc0, 0xb7, 0x93, 0x91, 0xb5, 0x6a, 0xe7, 0xbb, 0x27, 0x6f, 0x36, - 0x99, 0xc0, 0xcb, 0xa5, 0x32, 0x7e, 0x7d, 0xd0, 0x6c, 0x70, 0x50, 0x41, 0xea, 0xab, 0x1f, 0x9e, 0xe3, 0x0b, 0x05, - 0x29, 0x70, 0x12, 0xa0, 0xa2, 0xf3, 0xdd, 0x93, 0xf7, 0x4e, 0x22, 0x5c, 0x4b, 0x08, 0x9b, 0xd3, 0x76, 0x52, 0xe2, - 0x44, 0x84, 0x22, 0x39, 0xf7, 0x92, 0x71, 0xa5, 0x86, 0xf8, 0xf6, 0x22, 0xf1, 0x12, 0xec, 0x87, 0x21, 0x1b, 0x11, - 0x5f, 0x61, 0x80, 0xf8, 0x08, 0xfb, 0x35, 0xb3, 0x8c, 0xc0, 0x02, 0x88, 0xb1, 0xce, 0x60, 0x25, 0x5c, 0xa9, 0xf8, - 0x21, 0xec, 0x8b, 0x51, 0x79, 0x21, 0x45, 0xc7, 0xcf, 0x6b, 0xb9, 0x69, 0x95, 0x35, 0xfa, 0x2d, 0x58, 0x4e, 0xfa, - 0xe1, 0xb5, 0xea, 0xba, 0x2c, 0x78, 0xaa, 0x93, 0xc8, 0xce, 0x77, 0x4f, 0x5e, 0xa9, 0x3c, 0xb2, 0x99, 0xaf, 0xb9, - 0xfd, 0x9a, 0x85, 0x79, 0xf2, 0xca, 0xad, 0xde, 0x8a, 0xca, 0xe7, 0xbb, 0x27, 0x1f, 0x36, 0x55, 0x83, 0xf2, 0x62, - 0x5e, 0x99, 0xf8, 0x02, 0xbe, 0x05, 0x8d, 0xbd, 0x85, 0x12, 0x0d, 0x1e, 0x2b, 0xb0, 0x10, 0x47, 0x5e, 0x5e, 0x94, - 0x9e, 0x91, 0xa7, 0x38, 0x23, 0x22, 0x0e, 0x54, 0x5f, 0x35, 0xa5, 0xe4, 0xb1, 0x34, 0x39, 0x0b, 0xd2, 0x19, 0xdd, - 0x12, 0x1c, 0x3a, 0x41, 0x2e, 0x9b, 0x42, 0x02, 0x8d, 0x00, 0x9d, 0xe1, 0x9d, 0x36, 0xea, 0xd5, 0x85, 0x57, 0x26, - 0x88, 0x34, 0xad, 0x49, 0x16, 0x1c, 0x91, 0x36, 0xf6, 0x49, 0x1b, 0x07, 0x24, 0x1f, 0xb6, 0xa5, 0x78, 0xe8, 0x05, - 0x65, 0xbf, 0x52, 0xc8, 0x40, 0x6e, 0x58, 0x20, 0x77, 0xab, 0x14, 0xbf, 0x61, 0x2f, 0x10, 0xae, 0x47, 0x21, 0xd1, - 0x43, 0x69, 0xb4, 0x3a, 0x29, 0x4e, 0x45, 0xc7, 0x67, 0xec, 0x32, 0x86, 0xec, 0x12, 0x98, 0x15, 0xe6, 0xc8, 0x2b, - 0xab, 0x76, 0x54, 0xd5, 0xc0, 0x15, 0xeb, 0x94, 0xe2, 0xc0, 0x05, 0xc6, 0x8d, 0x03, 0x95, 0x8c, 0x93, 0xaf, 0x37, - 0x79, 0xb8, 0xb7, 0xe7, 0xc8, 0x46, 0xdf, 0x71, 0x27, 0xd5, 0xef, 0xab, 0xd0, 0xdd, 0xb7, 0x92, 0x57, 0x84, 0x48, - 0xc0, 0xdf, 0x68, 0xf8, 0xa3, 0x02, 0xe2, 0xd0, 0x4e, 0x50, 0xc7, 0xa0, 0x06, 0x5e, 0x68, 0x7a, 0xf5, 0xe9, 0x37, - 0x1a, 0x65, 0x98, 0xb6, 0x8e, 0xad, 0x13, 0x9c, 0x15, 0x57, 0x4e, 0x99, 0xff, 0xd3, 0x5e, 0xcb, 0x9a, 0xd2, 0x20, - 0x20, 0x66, 0xd2, 0x2c, 0xd3, 0x93, 0x31, 0xb6, 0x04, 0x83, 0x7a, 0x2f, 0x54, 0xe2, 0x02, 0x16, 0x39, 0x56, 0xaa, - 0x92, 0x66, 0x67, 0x5d, 0xe4, 0xe9, 0x4a, 0x10, 0x96, 0x82, 0x4a, 0x8d, 0x42, 0x91, 0xf7, 0xab, 0xf5, 0xcc, 0x4b, - 0x9c, 0x23, 0xe5, 0xe3, 0x12, 0x50, 0x08, 0x64, 0x75, 0x4b, 0xa4, 0x3c, 0x27, 0x93, 0xed, 0x24, 0x7f, 0x62, 0x90, - 0xfc, 0x13, 0x42, 0x0d, 0xf2, 0x97, 0x1e, 0x0e, 0x37, 0x55, 0xae, 0x85, 0x5c, 0xbf, 0x3a, 0x9d, 0x11, 0xf0, 0xa1, - 0xd5, 0x31, 0x5a, 0x8b, 0x2b, 0x6e, 0x61, 0x28, 0xe6, 0x0e, 0x11, 0x5e, 0x48, 0xac, 0x83, 0xc0, 0x4e, 0x15, 0x55, - 0x83, 0xa1, 0x37, 0xb9, 0xf4, 0x4c, 0x0e, 0x78, 0xf2, 0xe1, 0xee, 0x80, 0xe8, 0xe9, 0x6c, 0x7d, 0xe7, 0x1a, 0x19, - 0xa0, 0x30, 0x6b, 0x63, 0xe3, 0xd6, 0xf3, 0x41, 0x61, 0xfc, 0x32, 0x90, 0x5d, 0x67, 0x3e, 0x2b, 0x9b, 0x50, 0xcb, - 0x3f, 0x80, 0xb6, 0xd3, 0x11, 0x35, 0xa8, 0xd1, 0x2d, 0xf0, 0x23, 0x99, 0x87, 0xea, 0x67, 0x5b, 0xd8, 0xc7, 0x89, - 0xa8, 0x40, 0x93, 0x70, 0xf3, 0xeb, 0x27, 0x85, 0x22, 0x13, 0x09, 0x1a, 0x5a, 0x00, 0xff, 0x93, 0x24, 0x0f, 0x74, - 0x23, 0xe4, 0x02, 0x20, 0x68, 0x22, 0xf0, 0x54, 0x21, 0xcc, 0xb6, 0x2b, 0xe7, 0xfb, 0xf3, 0x1d, 0x42, 0x26, 0x95, - 0xf3, 0xf1, 0x5d, 0x95, 0x7d, 0x05, 0x64, 0x81, 0x3c, 0x30, 0x1e, 0xcb, 0x02, 0x19, 0xbf, 0x3c, 0xd5, 0xd5, 0x85, - 0x01, 0xe9, 0x56, 0xfa, 0xb6, 0x11, 0xdb, 0x14, 0x5e, 0x39, 0xf9, 0x5e, 0xa3, 0x61, 0xe5, 0xed, 0x2e, 0xbc, 0x7d, - 0xc9, 0x05, 0x8c, 0xf0, 0xfc, 0x5e, 0xd4, 0xd6, 0xfd, 0x16, 0x1f, 0x57, 0x53, 0x58, 0x56, 0x16, 0xc5, 0x65, 0x49, - 0x4e, 0x33, 0xfe, 0x84, 0x8e, 0xd3, 0x0c, 0x42, 0x16, 0x25, 0x4e, 0x50, 0xb1, 0x6b, 0xb8, 0xed, 0xc4, 0xfc, 0x8c, - 0x38, 0xc1, 0xca, 0x04, 0xc5, 0xaf, 0x8f, 0x22, 0x6a, 0x7d, 0xbe, 0xda, 0x6a, 0xb2, 0xb7, 0xf7, 0xae, 0x42, 0x93, - 0x82, 0x52, 0x40, 0x61, 0x30, 0x2d, 0xa9, 0xd2, 0xa8, 0x50, 0xee, 0xae, 0x53, 0xba, 0x00, 0x34, 0xc3, 0x30, 0x79, - 0xcf, 0x73, 0xc2, 0x8b, 0xc9, 0x2a, 0x8b, 0x57, 0xae, 0x09, 0x66, 0x9a, 0x2d, 0xc0, 0xe1, 0xc1, 0xd0, 0x96, 0xbe, - 0xa2, 0xbc, 0x4a, 0x89, 0x2d, 0x61, 0x38, 0x05, 0x64, 0x39, 0xc2, 0x08, 0x31, 0x28, 0x70, 0xa3, 0x51, 0xf2, 0x16, - 0xf4, 0xca, 0x08, 0xe7, 0x6e, 0x04, 0x49, 0xb0, 0xb5, 0x2d, 0x8b, 0x10, 0x96, 0x99, 0x39, 0x46, 0x2e, 0xc1, 0xc9, - 0xf3, 0x4d, 0x1e, 0x65, 0x4d, 0xd4, 0x54, 0x48, 0x1d, 0xa8, 0x91, 0xa1, 0xb2, 0x81, 0x7b, 0xe5, 0x30, 0xa5, 0xb8, - 0xe9, 0xb8, 0x19, 0x30, 0xe0, 0x9f, 0xb9, 0x23, 0x63, 0x51, 0x20, 0x33, 0x52, 0x77, 0xee, 0xd4, 0x86, 0xee, 0xa5, - 0xa2, 0x19, 0x56, 0x88, 0x8b, 0x4c, 0x34, 0xa5, 0x22, 0xae, 0x77, 0x5a, 0xf1, 0xd2, 0x2b, 0x99, 0x47, 0xcd, 0x35, - 0x17, 0xac, 0x32, 0x49, 0x8c, 0xe9, 0x5f, 0xc9, 0xd4, 0xe8, 0xb2, 0x12, 0xa8, 0x61, 0xf4, 0xda, 0x7a, 0x22, 0xd6, - 0x80, 0x16, 0x40, 0x5f, 0x8b, 0x53, 0x6e, 0xac, 0xa8, 0xf6, 0x61, 0x8b, 0x31, 0x0d, 0xa9, 0xff, 0x0e, 0x72, 0x5d, - 0x56, 0xf7, 0xfc, 0x73, 0x21, 0x0b, 0x19, 0xce, 0x6b, 0x8c, 0x3d, 0x13, 0x8c, 0x1d, 0x81, 0x9e, 0xa6, 0xd3, 0xbf, - 0x07, 0x2a, 0xe5, 0x45, 0xe5, 0x2e, 0x3a, 0x8a, 0xc4, 0x5e, 0x97, 0xe1, 0x72, 0xe3, 0xf7, 0xca, 0x6a, 0x78, 0x8c, - 0x40, 0x1a, 0x10, 0x56, 0x9c, 0x3d, 0x43, 0x38, 0x6f, 0x34, 0x7a, 0xf9, 0x31, 0xad, 0x5c, 0x24, 0x15, 0x8c, 0x0c, - 0x22, 0xba, 0x40, 0xf0, 0x35, 0x19, 0x0a, 0x31, 0x7f, 0x9d, 0x9f, 0x9d, 0x83, 0xab, 0xfd, 0xe4, 0x9d, 0x63, 0x72, - 0x35, 0xb3, 0x6e, 0x19, 0x34, 0x85, 0xf9, 0x38, 0x55, 0xbc, 0xe5, 0xed, 0xdd, 0x19, 0x1e, 0x00, 0xf7, 0x4e, 0x07, - 0x43, 0x36, 0x1a, 0xea, 0x71, 0xc9, 0x12, 0xca, 0xdd, 0xd7, 0x43, 0x55, 0x62, 0xa2, 0x39, 0x58, 0x8f, 0x57, 0xa6, - 0x2c, 0x27, 0x79, 0x51, 0xe4, 0xb4, 0x8a, 0xef, 0xaf, 0x64, 0x60, 0x0a, 0xe1, 0xb2, 0xee, 0x6c, 0x3f, 0x9d, 0x11, - 0x8e, 0x0d, 0x42, 0x7d, 0xbb, 0x2d, 0xf4, 0x51, 0x81, 0x09, 0xfb, 0x5a, 0x09, 0xc5, 0x6f, 0x37, 0x09, 0x45, 0x9c, - 0xa9, 0x2d, 0x2f, 0x04, 0x62, 0xe7, 0x1e, 0x02, 0x51, 0x39, 0xd9, 0xb5, 0x4c, 0x04, 0x75, 0xa4, 0x26, 0x13, 0xeb, - 0x4b, 0x4a, 0x32, 0xcc, 0xd4, 0x6a, 0xf4, 0xbb, 0xcb, 0x25, 0x1b, 0xb6, 0xc1, 0x89, 0x64, 0xdb, 0xf0, 0xb3, 0x23, - 0x7f, 0x1a, 0x9c, 0x58, 0x3a, 0x81, 0x1d, 0x56, 0x9a, 0x2c, 0xc8, 0x85, 0x34, 0x67, 0x47, 0x64, 0x65, 0x09, 0x9a, - 0x56, 0x14, 0xa4, 0x08, 0x9c, 0xb0, 0x32, 0xca, 0x04, 0x10, 0x0b, 0x59, 0xa1, 0x0c, 0x48, 0x67, 0x63, 0xfa, 0x9f, - 0x36, 0x2f, 0x3f, 0xad, 0x89, 0xd6, 0xe4, 0x8a, 0x54, 0x1f, 0x6a, 0x09, 0x07, 0x0a, 0x02, 0xa5, 0x1f, 0xee, 0x08, - 0x13, 0xb4, 0x12, 0xe5, 0xc8, 0x94, 0x43, 0xb8, 0x0d, 0x2e, 0xb4, 0x9d, 0x77, 0x32, 0xc0, 0xbb, 0x41, 0x9a, 0xe0, - 0xd4, 0xa0, 0xeb, 0xe7, 0x84, 0xd7, 0x58, 0x49, 0x44, 0x94, 0xa5, 0x84, 0x03, 0x41, 0xa6, 0x9c, 0x64, 0xc3, 0xf6, - 0x08, 0x14, 0xd0, 0x9e, 0x7f, 0x9c, 0x55, 0x26, 0xb0, 0xdf, 0x68, 0xa0, 0x40, 0x8f, 0x1a, 0x0d, 0x59, 0xc3, 0x1f, - 0x61, 0x8a, 0x7d, 0x69, 0x98, 0x9c, 0xee, 0xed, 0x39, 0x41, 0x35, 0xee, 0xd0, 0x1f, 0x21, 0x9c, 0x2e, 0x97, 0x8e, - 0x00, 0x2b, 0x40, 0xcb, 0x65, 0x60, 0x82, 0x25, 0x5e, 0x43, 0xb3, 0xc9, 0x80, 0x93, 0x89, 0x10, 0x80, 0x13, 0x80, - 0xb0, 0x41, 0x9c, 0x40, 0x39, 0xf7, 0x02, 0x70, 0x46, 0x35, 0xb2, 0xa1, 0xdf, 0xe8, 0x8c, 0x0c, 0xc6, 0x35, 0xf4, - 0x47, 0x24, 0x28, 0xd2, 0xbd, 0xbd, 0x9d, 0x5c, 0x89, 0xc8, 0x9f, 0x41, 0x94, 0xfd, 0x2c, 0x24, 0x8b, 0xec, 0xd0, - 0x5c, 0x8d, 0x55, 0x67, 0x40, 0x49, 0x51, 0x6a, 0x59, 0x75, 0xbd, 0x5a, 0x16, 0x44, 0x59, 0x09, 0xab, 0x58, 0xf0, - 0x00, 0x2c, 0xfb, 0x92, 0xcc, 0x7f, 0xe1, 0x65, 0x9a, 0xf5, 0xb7, 0x1b, 0x93, 0xab, 0x5d, 0xd7, 0xf5, 0xb3, 0x89, - 0x88, 0x64, 0xe8, 0x28, 0xac, 0x20, 0xfe, 0x7d, 0x05, 0xa6, 0x31, 0xf0, 0xb0, 0x1c, 0x6b, 0x44, 0x24, 0xf8, 0x5a, - 0xb5, 0xd1, 0x27, 0x4a, 0x7e, 0xdd, 0xe8, 0x65, 0x90, 0x90, 0x7c, 0xfd, 0x5b, 0x21, 0x39, 0x50, 0x90, 0x48, 0xf2, - 0x58, 0xc1, 0xd9, 0x16, 0x5c, 0xfc, 0xca, 0x57, 0x70, 0xb6, 0x1d, 0xb7, 0x25, 0x43, 0xd8, 0x06, 0x9f, 0xc1, 0x1b, - 0x24, 0xa0, 0x55, 0x81, 0x01, 0xe5, 0xe1, 0xaa, 0xee, 0x25, 0x59, 0x29, 0x08, 0x53, 0x4e, 0x1c, 0x56, 0xdf, 0x00, - 0x95, 0x36, 0x6a, 0x18, 0xbe, 0xcc, 0x9b, 0x20, 0xc3, 0x25, 0x50, 0x4f, 0x5d, 0x01, 0x72, 0x52, 0xbe, 0x76, 0x48, - 0x45, 0xd8, 0x91, 0x4a, 0x9c, 0x1b, 0xf8, 0x33, 0x3e, 0xcf, 0x40, 0x95, 0xca, 0xf5, 0x6f, 0x28, 0x86, 0xb3, 0x20, - 0xa2, 0x0c, 0x7e, 0x40, 0xc1, 0xcc, 0xcf, 0x73, 0x76, 0x25, 0xcb, 0xd4, 0x6f, 0x9c, 0x12, 0x4d, 0xca, 0xb9, 0xd4, - 0x09, 0x33, 0xd4, 0xcb, 0x14, 0x9d, 0xd6, 0xd1, 0xf6, 0xec, 0x8a, 0x26, 0xfc, 0x25, 0xcb, 0x39, 0x4d, 0x60, 0xfa, - 0x15, 0xc5, 0xc1, 0x8c, 0x72, 0x04, 0x1b, 0xb6, 0xd6, 0xca, 0x0f, 0xc3, 0x3b, 0x9b, 0xf0, 0xba, 0x0e, 0x14, 0xf9, - 0x49, 0x18, 0xcb, 0x41, 0xcc, 0x84, 0x46, 0x9d, 0xc4, 0x59, 0xd6, 0x34, 0xf3, 0x69, 0x2a, 0x65, 0x43, 0x70, 0x77, - 0x87, 0x11, 0x2d, 0x09, 0xb4, 0xf4, 0xbc, 0x53, 0x6b, 0x81, 0x80, 0xf7, 0x96, 0x45, 0x30, 0x67, 0x82, 0xb9, 0xc1, - 0x51, 0xdd, 0x3a, 0x9c, 0x9a, 0x6e, 0xbe, 0xdb, 0x78, 0xb0, 0x6d, 0x93, 0x70, 0x10, 0x74, 0xf2, 0x70, 0xbb, 0x65, - 0xf5, 0x4a, 0x4b, 0x0e, 0x2d, 0x2d, 0xd8, 0x7d, 0x19, 0x33, 0x5a, 0x68, 0xf2, 0x42, 0x7a, 0x2b, 0xee, 0x72, 0xf2, - 0x0b, 0x9c, 0x1c, 0x7a, 0xce, 0xa7, 0xf1, 0xca, 0x01, 0x99, 0xde, 0x6e, 0xa9, 0xfd, 0xef, 0x72, 0xe7, 0x09, 0x7e, - 0x05, 0x61, 0xdd, 0x6f, 0xaa, 0xea, 0xeb, 0xe1, 0xdc, 0x6f, 0x2a, 0x04, 0x7d, 0xe3, 0xad, 0xd5, 0x33, 0xc2, 0xb8, - 0x5d, 0xf7, 0xc8, 0x6d, 0xdb, 0x5a, 0x5b, 0xfa, 0x51, 0x06, 0x91, 0x64, 0xaa, 0xa5, 0xd8, 0x0f, 0xb8, 0x4a, 0x54, - 0x83, 0x84, 0xb9, 0xba, 0x85, 0x44, 0x55, 0x8a, 0xa1, 0xd4, 0xe1, 0xb7, 0x2d, 0x8f, 0x92, 0x31, 0x99, 0xb4, 0x33, - 0xde, 0xfa, 0x19, 0xdf, 0x85, 0x5d, 0x96, 0xae, 0x9d, 0xc6, 0x8b, 0x08, 0x78, 0xd0, 0xee, 0x37, 0x84, 0x61, 0x6c, - 0xe7, 0xf2, 0x30, 0x90, 0xd9, 0x3f, 0x49, 0xb5, 0xee, 0x56, 0xb7, 0x32, 0x5e, 0x83, 0xfd, 0x8f, 0x70, 0xa4, 0x8f, - 0xc8, 0x51, 0xc5, 0x81, 0xa9, 0xb7, 0x28, 0x4a, 0xa7, 0x40, 0x2a, 0x95, 0xb7, 0x04, 0xe1, 0xb4, 0x10, 0xe1, 0xed, - 0xef, 0xf1, 0x0f, 0x8a, 0x25, 0x9e, 0x97, 0x1c, 0xe7, 0xd9, 0x7d, 0x39, 0xa2, 0x04, 0xbf, 0x8c, 0xde, 0x03, 0x1d, - 0x0b, 0x0a, 0x2d, 0x34, 0x15, 0x3d, 0x4d, 0xd5, 0x44, 0xb6, 0xe6, 0xa5, 0x62, 0x5a, 0x66, 0xd4, 0x88, 0x61, 0x36, - 0x24, 0x72, 0x6a, 0x2b, 0x9b, 0x97, 0xbb, 0xaa, 0x36, 0x2e, 0xda, 0x82, 0xc5, 0x2a, 0xb0, 0xb8, 0x5c, 0x3a, 0x75, - 0x54, 0x13, 0x66, 0xc4, 0x31, 0x10, 0x66, 0x46, 0x42, 0x45, 0x4d, 0xb3, 0x96, 0x6d, 0x1c, 0xb4, 0x9a, 0x4f, 0xa4, - 0x75, 0xf3, 0x1a, 0x1c, 0xa6, 0x0b, 0x41, 0x36, 0x37, 0x7d, 0x0a, 0x58, 0xce, 0xae, 0x1c, 0xc8, 0xc0, 0xd0, 0x8f, - 0x65, 0xae, 0x6c, 0x95, 0xd4, 0xba, 0x01, 0xbf, 0xe8, 0x8e, 0x6c, 0x59, 0x85, 0xba, 0xf5, 0xf7, 0x46, 0xae, 0xd1, - 0xd3, 0x74, 0x5b, 0xae, 0x51, 0x4d, 0xdb, 0xdd, 0x69, 0xa3, 0xbb, 0xf3, 0x52, 0xe5, 0x58, 0x9b, 0xab, 0xfc, 0x86, - 0xe1, 0x3a, 0x40, 0x9b, 0x12, 0xcd, 0x9a, 0xab, 0x9c, 0x16, 0xc5, 0x79, 0x79, 0x9a, 0x40, 0xa4, 0xee, 0x9c, 0x4b, - 0xfa, 0x57, 0x56, 0xa3, 0x38, 0x94, 0xeb, 0x7c, 0x4f, 0x26, 0x71, 0x7a, 0xe9, 0xc7, 0xef, 0x61, 0xbc, 0xea, 0xe5, - 0xf3, 0xdb, 0x30, 0xf3, 0x39, 0x55, 0xdc, 0xa5, 0x82, 0xe1, 0x7b, 0x03, 0x86, 0xef, 0x25, 0x9f, 0xae, 0xda, 0xe3, - 0xc5, 0xcb, 0xb2, 0x03, 0xef, 0xbc, 0xd0, 0x2c, 0xe3, 0x96, 0x6f, 0x1e, 0x63, 0x95, 0x85, 0xdd, 0x96, 0x2c, 0xec, - 0x96, 0x3b, 0xab, 0x5d, 0x39, 0xce, 0x0f, 0x9b, 0x7b, 0x59, 0xe7, 0x6c, 0x3f, 0x54, 0x1b, 0xff, 0x07, 0xef, 0xce, - 0x36, 0x06, 0x97, 0xdb, 0x77, 0xf7, 0x45, 0xb2, 0x8a, 0x04, 0xf9, 0x25, 0x24, 0x1d, 0x70, 0xd2, 0x37, 0x0e, 0x1d, - 0x54, 0x72, 0x4a, 0xe7, 0x01, 0x39, 0xc1, 0x3c, 0xe7, 0xe9, 0x54, 0xf5, 0x99, 0xab, 0x93, 0x46, 0xe2, 0x25, 0xb8, - 0xa2, 0x45, 0xac, 0xdd, 0xab, 0x9f, 0xe5, 0x5a, 0x7c, 0x64, 0x49, 0xe8, 0xe5, 0x58, 0x49, 0x91, 0xdc, 0xcb, 0x0a, - 0xa2, 0xb3, 0x8d, 0xd7, 0xdf, 0xe1, 0x31, 0x4b, 0x58, 0x1e, 0xd1, 0xcc, 0x49, 0xd1, 0x62, 0xdb, 0x60, 0x29, 0x04, - 0x64, 0xe4, 0x60, 0xf8, 0xaf, 0xd5, 0xa9, 0x3f, 0x17, 0x7a, 0x03, 0x3f, 0xd0, 0x94, 0xf2, 0x28, 0x0d, 0x21, 0x2d, - 0xc5, 0x0d, 0xcb, 0x43, 0x4d, 0x7b, 0x7b, 0x3b, 0x8e, 0x2d, 0xdc, 0x12, 0x70, 0x00, 0xdc, 0x7c, 0x83, 0x06, 0x0b, - 0x38, 0x9f, 0x53, 0x0d, 0x4d, 0xd1, 0x82, 0xae, 0x1e, 0x65, 0xe1, 0xee, 0x47, 0x7a, 0x8b, 0x13, 0x54, 0x14, 0x9e, - 0x84, 0xda, 0x1e, 0x33, 0x1a, 0x87, 0x36, 0xfe, 0x48, 0x6f, 0xbd, 0xf2, 0xcc, 0xb8, 0x38, 0xe2, 0x2c, 0x16, 0xd0, - 0x4e, 0xaf, 0x13, 0x1b, 0x57, 0x83, 0x78, 0x8b, 0x02, 0xa7, 0x19, 0x9b, 0x00, 0x71, 0x7e, 0x43, 0x6f, 0x3d, 0xd9, - 0x1f, 0x33, 0xce, 0xeb, 0xa1, 0x85, 0x46, 0xbd, 0x6b, 0x14, 0x9b, 0xcb, 0xa0, 0x0c, 0x8a, 0xa1, 0x68, 0x3b, 0x22, - 0xb5, 0x7a, 0x95, 0x79, 0x88, 0x50, 0x71, 0xdf, 0xa9, 0xe0, 0x6f, 0x4c, 0xd1, 0xc6, 0x6b, 0x99, 0xaf, 0x2b, 0x8d, - 0x28, 0x34, 0xa8, 0x32, 0x3d, 0x76, 0x9d, 0x44, 0xef, 0x3a, 0x75, 0x08, 0xc1, 0x70, 0x84, 0x7d, 0xc3, 0x55, 0xa7, - 0xde, 0x5f, 0x65, 0x42, 0x48, 0x15, 0x49, 0x7a, 0x51, 0xb5, 0xb3, 0x76, 0x1d, 0xc0, 0x3b, 0x24, 0xb4, 0xf8, 0xe2, - 0x4c, 0x66, 0xa1, 0xb3, 0x45, 0xff, 0xc6, 0x89, 0xb3, 0xd0, 0x53, 0xf0, 0x12, 0x13, 0x8b, 0xbc, 0x00, 0x2a, 0x54, - 0xf4, 0x25, 0x13, 0x00, 0xd9, 0xd8, 0x61, 0x6b, 0x52, 0x33, 0x13, 0x52, 0xd3, 0x35, 0x30, 0xbe, 0x45, 0x4a, 0x52, - 0x81, 0x0c, 0xa1, 0x44, 0x0a, 0xa1, 0xa7, 0x16, 0x57, 0x91, 0x90, 0xb9, 0xa0, 0xe5, 0x09, 0x3a, 0xb9, 0xe6, 0x59, - 0x0d, 0x2c, 0x47, 0xf4, 0x83, 0x0a, 0x0f, 0xa6, 0x44, 0x65, 0x85, 0xa2, 0x3c, 0x9a, 0xad, 0xd3, 0x5b, 0x9d, 0xd4, - 0xd5, 0xd3, 0x22, 0x1a, 0x25, 0x4e, 0x84, 0x16, 0x89, 0x13, 0xe1, 0x0c, 0xd2, 0x11, 0xd3, 0xa2, 0x84, 0x9f, 0x9a, - 0xab, 0x51, 0x4b, 0x56, 0xde, 0x7c, 0xca, 0x0f, 0x94, 0x79, 0x0e, 0x29, 0x9a, 0x38, 0xd1, 0x3c, 0x25, 0x71, 0xc4, - 0x71, 0x3b, 0x63, 0xd9, 0xbe, 0x57, 0x09, 0x3a, 0x0a, 0xb0, 0xbf, 0x71, 0x67, 0x61, 0xcc, 0xc2, 0x3c, 0xd1, 0xad, - 0x4e, 0xfd, 0xa9, 0x60, 0x5f, 0x95, 0x43, 0xea, 0xe4, 0x64, 0x45, 0xe2, 0xdc, 0x9d, 0x6a, 0xf9, 0xcb, 0x9c, 0x66, - 0xb7, 0x67, 0x14, 0x52, 0x9d, 0x53, 0x38, 0xf0, 0x5b, 0x2d, 0x43, 0x95, 0xa7, 0x3e, 0xc8, 0x84, 0xb2, 0x52, 0xd4, - 0xcf, 0x01, 0xae, 0x9e, 0x12, 0x2c, 0x44, 0xb4, 0xd1, 0x70, 0xc4, 0xc8, 0xdd, 0x42, 0xb7, 0x9e, 0x9f, 0xa4, 0x3d, - 0x06, 0xfe, 0xb5, 0x0a, 0xd3, 0x2a, 0x58, 0x80, 0x53, 0xf3, 0x4c, 0xea, 0x30, 0x1f, 0xad, 0x7a, 0x65, 0xa0, 0x08, - 0xc2, 0x77, 0xd9, 0xf6, 0xa9, 0x6e, 0x4a, 0x9a, 0xdd, 0x3e, 0xd5, 0x5a, 0xd0, 0x4f, 0x24, 0xfc, 0x60, 0x35, 0x4e, - 0x79, 0x82, 0x99, 0x15, 0x05, 0x2a, 0x00, 0xbc, 0xbf, 0xf4, 0x1c, 0xe7, 0x2f, 0x2a, 0x65, 0xd0, 0x85, 0x58, 0xec, - 0x59, 0x9c, 0x6a, 0x26, 0x5e, 0x8d, 0xff, 0x97, 0xb5, 0xf1, 0xff, 0x62, 0x9c, 0x3a, 0x05, 0xd3, 0x68, 0x92, 0xd0, - 0x50, 0xb3, 0x4e, 0x24, 0x09, 0x50, 0xe8, 0x6d, 0x19, 0x27, 0x1f, 0x2f, 0x3c, 0xd0, 0xb8, 0x16, 0xe3, 0x34, 0xe1, - 0xcd, 0xb1, 0x3f, 0x65, 0xf1, 0xad, 0x37, 0x67, 0xcd, 0x69, 0x9a, 0xa4, 0xf9, 0xcc, 0x0f, 0x28, 0xce, 0x6f, 0x73, - 0x4e, 0xa7, 0xcd, 0x39, 0xc3, 0xcf, 0x69, 0x7c, 0x45, 0x39, 0x0b, 0x7c, 0x6c, 0x9f, 0x64, 0xcc, 0x8f, 0xad, 0xd7, - 0x7e, 0x96, 0xa5, 0xd7, 0x36, 0x7e, 0x97, 0x5e, 0xa6, 0x3c, 0xc5, 0x6f, 0x6e, 0x6e, 0x27, 0x34, 0xc1, 0x1f, 0x2e, - 0xe7, 0x09, 0x9f, 0xe3, 0xdc, 0x4f, 0xf2, 0x66, 0x4e, 0x33, 0x36, 0xee, 0x05, 0x69, 0x9c, 0x66, 0x4d, 0xc8, 0xd8, - 0x9e, 0x52, 0x2f, 0x66, 0x93, 0x88, 0x5b, 0xa1, 0x9f, 0x7d, 0xec, 0x35, 0x9b, 0xb3, 0x8c, 0x4d, 0xfd, 0xec, 0xb6, - 0x29, 0x6a, 0x78, 0x5f, 0xb6, 0xf7, 0xfd, 0xc7, 0xe3, 0x83, 0x1e, 0xcf, 0xfc, 0x24, 0x67, 0xb0, 0x4c, 0x9e, 0x1f, - 0xc7, 0xd6, 0xfe, 0x61, 0x7b, 0x9a, 0xef, 0xc8, 0x40, 0x9e, 0x9f, 0xf0, 0xe2, 0x02, 0xbf, 0x07, 0xb8, 0xdd, 0x4b, - 0x9e, 0xe0, 0xcb, 0x39, 0xe7, 0x69, 0xb2, 0x08, 0xe6, 0x59, 0x9e, 0x66, 0xde, 0x2c, 0x65, 0x09, 0xa7, 0x59, 0xef, - 0x32, 0xcd, 0x42, 0x9a, 0x35, 0x33, 0x3f, 0x64, 0xf3, 0xdc, 0x3b, 0x98, 0xdd, 0xf4, 0x40, 0xb3, 0x98, 0x64, 0xe9, - 0x3c, 0x09, 0xd5, 0x58, 0x2c, 0x89, 0x68, 0xc6, 0xb8, 0xf9, 0x42, 0x5c, 0x64, 0xe2, 0xc5, 0x2c, 0xa1, 0x7e, 0xd6, - 0x9c, 0x40, 0x63, 0x30, 0x8b, 0xda, 0x21, 0x9d, 0xe0, 0x6c, 0x72, 0xe9, 0x3b, 0x9d, 0xee, 0x23, 0xac, 0xff, 0x77, - 0x0f, 0x91, 0xd5, 0xde, 0x5c, 0xdc, 0x69, 0xb7, 0xff, 0x84, 0x7a, 0x2b, 0xa3, 0x08, 0x80, 0xbc, 0xce, 0xec, 0xc6, - 0xca, 0x53, 0xc8, 0x68, 0xdb, 0xd4, 0xb2, 0x37, 0xf3, 0x43, 0xc8, 0x07, 0xf6, 0xba, 0xb3, 0x9b, 0x02, 0x66, 0xe7, - 0xc9, 0x14, 0x53, 0x35, 0x49, 0xf5, 0xb4, 0xf8, 0xad, 0x10, 0x1f, 0x6d, 0x86, 0xb8, 0xab, 0x21, 0xae, 0xb0, 0xde, - 0x0c, 0xe7, 0x99, 0x88, 0xad, 0x7a, 0x9d, 0x5c, 0x02, 0x12, 0xa5, 0x57, 0x34, 0xd3, 0x70, 0x88, 0x87, 0xdf, 0x0c, - 0x46, 0x77, 0x33, 0x18, 0x47, 0x9f, 0x02, 0x23, 0x4b, 0xc2, 0x45, 0x7d, 0x5d, 0x3b, 0x19, 0x9d, 0xf6, 0x22, 0x0a, - 0xf4, 0xe4, 0x75, 0xe1, 0xf7, 0x35, 0x0b, 0x79, 0x24, 0x7f, 0x0a, 0x72, 0xbe, 0x96, 0xef, 0x0e, 0xdb, 0x6d, 0xf9, - 0x9c, 0xb3, 0x5f, 0xa9, 0xd7, 0x71, 0xa1, 0x42, 0x71, 0x81, 0x7f, 0x28, 0x4f, 0xf3, 0xd6, 0xb9, 0x27, 0xfe, 0x8b, - 0x79, 0xcc, 0xd7, 0x48, 0x51, 0xac, 0x0e, 0x45, 0xe3, 0x54, 0xcb, 0x4a, 0x29, 0x7c, 0xc0, 0x6d, 0x27, 0xb8, 0x23, - 0x61, 0xfd, 0xf2, 0x18, 0x27, 0x1b, 0xfc, 0x45, 0xe6, 0x5d, 0x78, 0x10, 0xe9, 0x30, 0x52, 0x0d, 0xd3, 0x5e, 0xd6, - 0x27, 0xed, 0x5e, 0xd6, 0x6c, 0x22, 0x27, 0x25, 0xc9, 0x30, 0x53, 0xc9, 0x79, 0x0e, 0x1b, 0xa4, 0xc2, 0xd8, 0xce, - 0x91, 0x97, 0xc2, 0x59, 0xd3, 0xe5, 0xb2, 0x0a, 0x03, 0x30, 0x71, 0x5a, 0xe3, 0x07, 0xae, 0x2a, 0xe0, 0xdc, 0xe0, - 0xe4, 0xbe, 0xbe, 0xde, 0x25, 0xd1, 0xbc, 0x22, 0x4e, 0x03, 0x81, 0x39, 0x77, 0xe6, 0xf3, 0x08, 0xbc, 0x14, 0xa5, - 0xf8, 0xa9, 0x52, 0x98, 0xec, 0x96, 0x8d, 0x06, 0x49, 0x99, 0xdf, 0x06, 0x79, 0x7c, 0x49, 0x01, 0xbd, 0x5c, 0x72, - 0x02, 0x3d, 0x56, 0xfd, 0x7f, 0xe0, 0x86, 0xa4, 0x4e, 0x5c, 0x96, 0x04, 0xf1, 0x3c, 0xa4, 0xb9, 0xe8, 0xa1, 0x12, - 0xe7, 0x70, 0x37, 0x44, 0x59, 0x4b, 0x34, 0x81, 0xde, 0x45, 0x36, 0x0f, 0x54, 0x84, 0x5b, 0x54, 0xca, 0xe7, 0xa6, - 0x78, 0xae, 0xda, 0xbe, 0xae, 0x92, 0x45, 0xa1, 0xa5, 0x3b, 0x4f, 0xd8, 0x2f, 0x73, 0x7a, 0xce, 0x42, 0xe3, 0xe4, - 0x2e, 0x4d, 0x82, 0x34, 0xa4, 0x1f, 0xde, 0xbd, 0x80, 0x6c, 0xf7, 0x34, 0x01, 0x12, 0x4b, 0xa4, 0xbf, 0x0b, 0xe7, - 0x24, 0x71, 0x43, 0x7a, 0xc5, 0x02, 0x3a, 0xb8, 0xd8, 0x5d, 0x6c, 0xac, 0x28, 0x5f, 0xa3, 0xa2, 0x75, 0x21, 0x92, - 0xfe, 0x04, 0x94, 0x17, 0xbb, 0x8b, 0x4b, 0x5e, 0xb4, 0x76, 0x17, 0x89, 0x1b, 0xa6, 0x53, 0x9f, 0x25, 0xf0, 0x3b, - 0x2f, 0x76, 0x17, 0x0c, 0x7e, 0xf0, 0xe2, 0xa2, 0xa8, 0x12, 0x45, 0x4b, 0x88, 0x8c, 0x29, 0x28, 0xdc, 0x75, 0x90, - 0xfb, 0x73, 0xca, 0x12, 0x51, 0x74, 0x57, 0xcf, 0x54, 0xf7, 0x0a, 0x48, 0xfe, 0x95, 0x48, 0x83, 0x59, 0x9b, 0xcb, - 0xe7, 0xf7, 0x35, 0x97, 0x69, 0xc2, 0x99, 0x48, 0x8b, 0xd7, 0xe1, 0x9c, 0xc8, 0xcf, 0xcf, 0x03, 0x79, 0x12, 0x35, - 0xaf, 0x4e, 0x5d, 0xf8, 0x02, 0xb1, 0xd2, 0x02, 0xa6, 0x99, 0x30, 0xf6, 0xe9, 0xf6, 0xa3, 0x92, 0xc9, 0x5d, 0xc6, - 0x5f, 0x49, 0x55, 0x79, 0x3a, 0xcf, 0x02, 0x88, 0xf5, 0x2a, 0x95, 0x62, 0xdd, 0x2b, 0x66, 0x0b, 0xfd, 0xcd, 0xc6, - 0xdc, 0x48, 0xb2, 0xe5, 0x70, 0xa6, 0xaf, 0xba, 0xb6, 0x83, 0x8a, 0x78, 0x22, 0xac, 0x19, 0x13, 0xab, 0x77, 0xce, - 0x42, 0x08, 0xbc, 0xb0, 0x50, 0x25, 0x2c, 0xd6, 0x26, 0x09, 0x2a, 0x52, 0x28, 0x32, 0x48, 0xe1, 0xb2, 0x9d, 0xb4, - 0x5a, 0x05, 0x42, 0x88, 0x8c, 0xeb, 0x81, 0xf0, 0x6d, 0x76, 0xf6, 0xf6, 0xf2, 0xea, 0x44, 0x1b, 0x53, 0x38, 0x5f, - 0x2e, 0x39, 0x75, 0x72, 0x79, 0xea, 0x26, 0x22, 0xa0, 0x8c, 0x31, 0x2c, 0xdf, 0x78, 0x29, 0x2e, 0x7b, 0xf2, 0xf2, - 0xa2, 0x17, 0x09, 0x24, 0x4a, 0x94, 0x11, 0x8d, 0xd4, 0x13, 0xad, 0x92, 0x61, 0xf3, 0x75, 0x79, 0x90, 0xbf, 0x86, - 0xf5, 0xf6, 0xca, 0xe2, 0x48, 0xab, 0x2a, 0x5a, 0x2d, 0xcd, 0xd3, 0x8c, 0x3b, 0x8e, 0x8f, 0x03, 0x44, 0xfa, 0xbe, - 0x98, 0xfd, 0xb1, 0xcc, 0xf7, 0x18, 0x34, 0x3b, 0x5e, 0xa7, 0xf4, 0x87, 0xd4, 0xce, 0x57, 0xcb, 0x6c, 0x33, 0x75, - 0x46, 0x17, 0xf0, 0x84, 0xcb, 0xdf, 0x0a, 0x7d, 0x55, 0x81, 0x9c, 0x5d, 0xf5, 0x5c, 0x4e, 0x12, 0x2b, 0x86, 0x26, - 0x95, 0x01, 0xa7, 0x06, 0xd5, 0x30, 0x1b, 0x61, 0xb6, 0x65, 0x6c, 0x54, 0x54, 0x88, 0x28, 0x37, 0xf7, 0x85, 0x54, - 0x82, 0xce, 0x0d, 0xea, 0xbe, 0x60, 0xda, 0x8d, 0x57, 0xa7, 0xbb, 0x42, 0xa1, 0xc8, 0xe0, 0x0c, 0x9b, 0xaa, 0x49, - 0x58, 0x6e, 0x49, 0xb2, 0x91, 0x78, 0x5d, 0xf9, 0x48, 0x25, 0x6d, 0x6c, 0xae, 0x22, 0x92, 0x21, 0x37, 0x01, 0x06, - 0x8e, 0x81, 0x9c, 0xeb, 0x29, 0x00, 0x8f, 0x19, 0x53, 0x38, 0xa9, 0xa4, 0x38, 0x0e, 0x5e, 0x48, 0xed, 0xde, 0xb3, - 0xdf, 0xbe, 0x39, 0x7b, 0x6f, 0x63, 0xb8, 0xea, 0x8c, 0x66, 0xb9, 0xb7, 0xb0, 0x55, 0x8e, 0x61, 0x13, 0xe2, 0xd5, - 0xb6, 0x67, 0xfb, 0x33, 0x38, 0xb4, 0x2d, 0x98, 0x6a, 0xeb, 0xa6, 0x79, 0x7d, 0x7d, 0xdd, 0x84, 0x13, 0x65, 0xcd, - 0x79, 0x16, 0x4b, 0x76, 0x13, 0xda, 0x45, 0x81, 0x5c, 0x1e, 0xd1, 0xa4, 0xbc, 0x0c, 0x29, 0x8d, 0xa9, 0x1b, 0xa7, - 0x13, 0x79, 0x1e, 0x76, 0xd5, 0x3d, 0x11, 0x5f, 0x1c, 0x8b, 0x4b, 0xbe, 0xfa, 0xc7, 0x5c, 0x5e, 0xaf, 0xc6, 0x33, - 0xf8, 0xd9, 0x87, 0xe0, 0xd5, 0x71, 0x8b, 0x47, 0xe2, 0xe1, 0x0c, 0x76, 0x93, 0x78, 0xda, 0x5d, 0xac, 0x51, 0xdd, - 0x00, 0xba, 0x88, 0xfa, 0x72, 0x6a, 0xb9, 0xa8, 0x75, 0xe1, 0xc5, 0x17, 0x17, 0xc5, 0x71, 0x0b, 0xfa, 0x6a, 0xe9, - 0x7e, 0x2f, 0xd3, 0xf0, 0x56, 0xb7, 0x2f, 0x29, 0x11, 0x2e, 0x7b, 0x4a, 0x48, 0x1f, 0xba, 0x80, 0x71, 0xc3, 0xbe, - 0xc0, 0x99, 0x62, 0xa1, 0xc3, 0xea, 0xa1, 0x18, 0x59, 0xc0, 0x30, 0x0b, 0x28, 0x01, 0x72, 0x83, 0xce, 0xc3, 0xb2, - 0x81, 0xd8, 0xed, 0xb2, 0x68, 0x1b, 0x80, 0xb2, 0x62, 0xb5, 0x7f, 0xa4, 0x9b, 0xbb, 0x22, 0x0b, 0x0d, 0x71, 0x68, - 0x02, 0x7f, 0x81, 0xe0, 0x5f, 0x01, 0xf8, 0x71, 0x4b, 0xa2, 0xe9, 0xc2, 0xbc, 0x76, 0x46, 0x5e, 0x08, 0x51, 0x22, - 0x73, 0x98, 0x71, 0xfc, 0x9e, 0xe3, 0x8f, 0x17, 0xa2, 0xaa, 0xd6, 0x12, 0x40, 0x7d, 0x05, 0x6d, 0xaa, 0xad, 0xd5, - 0xc1, 0x20, 0x8d, 0x63, 0x7f, 0x96, 0x53, 0x4f, 0xff, 0x50, 0x0a, 0x03, 0xe8, 0x1d, 0xeb, 0x1a, 0x9a, 0xca, 0x7b, - 0x3a, 0x05, 0x3d, 0x6e, 0x5d, 0x7d, 0xbc, 0xf2, 0x33, 0xa7, 0xd9, 0x0c, 0x9a, 0x97, 0x13, 0x54, 0xf0, 0x68, 0x61, - 0xaa, 0x1b, 0x0f, 0xdb, 0xed, 0x1e, 0x24, 0xa9, 0x36, 0xfd, 0x98, 0x4d, 0x12, 0x2f, 0xa6, 0x63, 0x5e, 0x70, 0x38, - 0x3d, 0xb8, 0xd0, 0xfa, 0x9d, 0xdb, 0x3d, 0xcc, 0xe8, 0xd4, 0x72, 0xe1, 0xef, 0xdd, 0x03, 0x17, 0x3c, 0xf4, 0x12, - 0x1e, 0x35, 0x45, 0x32, 0x34, 0x1c, 0xe5, 0xe0, 0x51, 0xed, 0x79, 0x61, 0x0c, 0x14, 0x50, 0xd0, 0x7d, 0x0b, 0x9e, - 0x59, 0x3c, 0xc2, 0x3c, 0x33, 0xeb, 0x25, 0x68, 0xb1, 0x36, 0x83, 0x75, 0x15, 0x6c, 0x1f, 0x15, 0xb9, 0xb0, 0x58, - 0x16, 0x6b, 0x78, 0x31, 0x54, 0xe9, 0x82, 0x25, 0xb3, 0x39, 0x1f, 0x0a, 0xcf, 0x7f, 0x06, 0x67, 0x48, 0x46, 0xd8, - 0x28, 0x01, 0x78, 0x46, 0xaa, 0x7d, 0xe0, 0xc7, 0x81, 0x03, 0x9d, 0x58, 0x4d, 0xeb, 0x28, 0xa3, 0x53, 0xd4, 0x9b, - 0xb2, 0xa4, 0x29, 0xdf, 0x1d, 0x1a, 0xba, 0x9b, 0xfb, 0x08, 0x9e, 0x0a, 0x57, 0xf4, 0x86, 0x45, 0x82, 0xef, 0x86, - 0x79, 0x5d, 0x8c, 0x8a, 0xa2, 0x97, 0x72, 0x67, 0xf8, 0xc2, 0x41, 0x23, 0xfc, 0xab, 0x71, 0x89, 0x8d, 0xad, 0xa9, - 0xda, 0xc6, 0x5d, 0xb4, 0xa5, 0x8a, 0x49, 0x97, 0xa2, 0xda, 0xaf, 0x04, 0x2a, 0xbe, 0x74, 0x6c, 0x9a, 0xcf, 0x9a, - 0x92, 0xfd, 0x34, 0x05, 0xf9, 0xd8, 0xd0, 0x14, 0x29, 0x77, 0x36, 0xa5, 0x0b, 0xc1, 0x59, 0xd4, 0x39, 0x16, 0xe9, - 0x71, 0x19, 0x95, 0xe7, 0x9e, 0xd4, 0xb3, 0x79, 0xd2, 0x09, 0xd5, 0xb6, 0xfe, 0xc5, 0x49, 0x9d, 0x4d, 0x81, 0xfc, - 0x2f, 0xef, 0xfa, 0xf3, 0xe3, 0x18, 0x06, 0xbc, 0xd0, 0x4a, 0x83, 0x79, 0x35, 0xca, 0x90, 0x8f, 0x1c, 0x54, 0xa8, - 0x3d, 0xf3, 0x44, 0xe8, 0xdd, 0xc6, 0x05, 0x83, 0x3b, 0x5c, 0x47, 0xd4, 0xe4, 0x09, 0x66, 0x06, 0x39, 0x01, 0xb5, - 0xdc, 0xf1, 0x5e, 0xc5, 0x66, 0xa4, 0xd6, 0x6e, 0x89, 0x09, 0x11, 0x3b, 0x4b, 0x42, 0xdb, 0xfa, 0x73, 0x10, 0xb3, - 0xe0, 0x23, 0xb1, 0x77, 0x17, 0x0e, 0x5a, 0x3f, 0x1a, 0x2a, 0x76, 0xa8, 0xe6, 0xb9, 0xa8, 0x1e, 0x6d, 0xc8, 0x5c, - 0x83, 0x9d, 0xca, 0xdb, 0x83, 0xec, 0x3e, 0xa8, 0x36, 0xc7, 0x2d, 0x39, 0x4e, 0xff, 0xa2, 0x38, 0xaf, 0x6e, 0x05, - 0xab, 0xa0, 0x00, 0x34, 0xcb, 0x72, 0x4b, 0xd0, 0x1f, 0xb1, 0xe5, 0x16, 0xaa, 0x59, 0x80, 0xd8, 0xa4, 0x7d, 0x64, - 0x5b, 0x92, 0xc1, 0x00, 0x9c, 0x5c, 0xf1, 0x1a, 0xdb, 0xfa, 0x73, 0x59, 0x46, 0x4b, 0xb7, 0x8f, 0xc8, 0x5b, 0x21, - 0x36, 0x8c, 0x05, 0xb6, 0xbe, 0x1b, 0x52, 0xee, 0xb3, 0x58, 0x36, 0xe9, 0x69, 0x2f, 0xc5, 0xca, 0x8c, 0x96, 0xcb, - 0xbc, 0x3e, 0x17, 0x56, 0xc7, 0xa0, 0x98, 0xd9, 0x71, 0xab, 0x82, 0x5b, 0xcc, 0x4c, 0xec, 0x0f, 0x33, 0x7e, 0x5a, - 0xcd, 0x50, 0xbe, 0xb3, 0xfe, 0x1c, 0x88, 0x93, 0x55, 0x00, 0x60, 0xaa, 0x00, 0x84, 0xc8, 0xbe, 0x54, 0x42, 0x1c, - 0x9f, 0xa4, 0x2e, 0xf7, 0xb3, 0x09, 0xe5, 0x2b, 0x88, 0xf5, 0x65, 0x22, 0x6f, 0x4f, 0x47, 0xf1, 0xd7, 0xa0, 0x0d, - 0xea, 0xd0, 0x82, 0x9e, 0x5b, 0x0c, 0x40, 0x55, 0x25, 0x1b, 0x35, 0xde, 0x08, 0x81, 0xec, 0x13, 0x8b, 0x23, 0xb9, - 0x7d, 0x2a, 0xb8, 0xbd, 0x8c, 0xc3, 0x59, 0x62, 0x2c, 0x01, 0x62, 0x61, 0x5b, 0x03, 0x09, 0x39, 0x0d, 0x25, 0xcc, - 0x24, 0x13, 0xad, 0xd2, 0xe2, 0xb8, 0x25, 0x6b, 0x4b, 0x76, 0x2c, 0x2b, 0x01, 0x12, 0xc4, 0x3e, 0xad, 0x70, 0x00, - 0xc9, 0xdf, 0x26, 0x1e, 0x42, 0x76, 0x55, 0x12, 0x9b, 0x38, 0x63, 0xd6, 0x3f, 0x8e, 0xfd, 0x4b, 0x1a, 0xf7, 0x77, - 0x17, 0xd9, 0x72, 0xd9, 0x2e, 0x8e, 0x5b, 0xf2, 0xd1, 0x3a, 0x16, 0x7c, 0x43, 0xde, 0x0d, 0x2a, 0x96, 0x18, 0x0e, - 0x6e, 0x42, 0x4a, 0xac, 0xce, 0x05, 0xf3, 0x54, 0x07, 0x85, 0x6d, 0x89, 0x2c, 0x14, 0x51, 0xa9, 0xd4, 0x69, 0x0a, - 0xdb, 0x62, 0xe1, 0x7a, 0x59, 0xce, 0xe9, 0x0c, 0x4a, 0xa3, 0xe5, 0xb2, 0x53, 0xd8, 0xd6, 0x94, 0x25, 0xf0, 0x94, - 0x2d, 0x97, 0xe2, 0x4c, 0xe4, 0x94, 0x25, 0x4e, 0x1b, 0xc8, 0xd6, 0xb6, 0xa6, 0xfe, 0x8d, 0x98, 0xb0, 0x7e, 0xe3, - 0xdf, 0x38, 0x1d, 0xf5, 0xca, 0x2d, 0xf1, 0x93, 0x03, 0xc5, 0x55, 0x2b, 0xea, 0xab, 0x15, 0x0d, 0xf1, 0x5c, 0x9e, - 0xf6, 0x22, 0x4e, 0x48, 0xfc, 0xcd, 0x2b, 0x1a, 0xea, 0x15, 0x9d, 0x6f, 0x59, 0xd1, 0xf9, 0x1d, 0x2b, 0x1a, 0xa8, - 0xd5, 0xb3, 0x4a, 0xdc, 0xa5, 0xcb, 0x65, 0xa7, 0x5d, 0x61, 0xef, 0xb8, 0x15, 0xb2, 0x2b, 0x58, 0x0d, 0xd0, 0xd4, - 0x38, 0x9b, 0xd2, 0xcd, 0x44, 0x59, 0x47, 0x31, 0xfd, 0x2c, 0x4c, 0x56, 0x58, 0xc8, 0xea, 0x58, 0x30, 0xe9, 0xba, - 0x0c, 0x4c, 0xfe, 0x91, 0x94, 0xcd, 0x00, 0x0f, 0x39, 0xe0, 0x21, 0xd2, 0x77, 0x85, 0x3a, 0xf6, 0x7b, 0x1b, 0xdb, - 0x96, 0xad, 0xc9, 0xfa, 0xa2, 0x38, 0x07, 0x19, 0x21, 0xe6, 0x77, 0x2f, 0x5a, 0x84, 0xda, 0x76, 0x7f, 0x3b, 0xcd, - 0x41, 0x0e, 0xc1, 0x75, 0x9a, 0x85, 0xb6, 0x27, 0xab, 0x7e, 0x16, 0xaa, 0xa6, 0x2c, 0x51, 0x19, 0x69, 0x5b, 0x69, - 0xad, 0x7a, 0x6f, 0x52, 0x5c, 0xf7, 0xf0, 0x50, 0xd6, 0x98, 0xf9, 0x9c, 0xd3, 0x2c, 0x51, 0x94, 0x6b, 0xdb, 0xff, - 0x21, 0xa8, 0x70, 0x03, 0x5f, 0x09, 0xf4, 0x02, 0x68, 0x02, 0x54, 0x3a, 0xb7, 0xe2, 0xf9, 0x52, 0x3c, 0xed, 0x54, - 0xca, 0xe6, 0x2d, 0x32, 0xf5, 0x7e, 0x59, 0x04, 0x66, 0xc8, 0x7c, 0x4a, 0xc3, 0x73, 0xc1, 0xa0, 0x07, 0xf1, 0x85, - 0x52, 0x1e, 0x57, 0xc4, 0x5d, 0xd5, 0x00, 0xdb, 0x3f, 0xcd, 0xbb, 0x8f, 0x0e, 0x4e, 0x6d, 0x2c, 0x79, 0x7c, 0x3a, - 0x1e, 0xdb, 0xa8, 0xb0, 0xee, 0xd7, 0xac, 0x73, 0xf0, 0xd3, 0xfc, 0xeb, 0x67, 0xed, 0xaf, 0xcb, 0xc6, 0x09, 0x10, - 0x91, 0x4a, 0x82, 0xd0, 0xa2, 0xca, 0x80, 0x57, 0xcf, 0x68, 0xec, 0x27, 0xdb, 0xa7, 0x33, 0x34, 0xa7, 0x93, 0xcf, - 0x28, 0x0d, 0x81, 0x38, 0xf1, 0x5a, 0xe9, 0x79, 0x4c, 0xaf, 0xa8, 0xbe, 0xa1, 0x71, 0xc3, 0x60, 0x1b, 0x5a, 0x04, - 0xe9, 0x3c, 0xe1, 0x2a, 0x1b, 0x44, 0xb1, 0x5a, 0x63, 0x4a, 0x17, 0x62, 0x0e, 0xa6, 0x3a, 0x7f, 0x2b, 0xe5, 0x5c, - 0x5d, 0x7a, 0x15, 0x17, 0xd8, 0x36, 0x00, 0xd8, 0x0a, 0xd9, 0x60, 0x4b, 0xb9, 0xd7, 0xc6, 0xed, 0x6d, 0xb0, 0xe1, - 0x0e, 0xf2, 0x6c, 0x7b, 0xa4, 0xf1, 0x24, 0x1c, 0xba, 0xb5, 0x4b, 0x35, 0xb6, 0xe2, 0xeb, 0x93, 0x18, 0xb8, 0xcc, - 0xa0, 0xb3, 0x84, 0xe6, 0xf9, 0x56, 0x04, 0x94, 0x8b, 0x88, 0xed, 0xaa, 0xb6, 0xbd, 0xa5, 0x17, 0xdc, 0xc6, 0xb0, - 0xc3, 0x04, 0xc0, 0x65, 0x58, 0x59, 0xd5, 0xa2, 0xe3, 0x31, 0x0d, 0x4a, 0x7f, 0x38, 0x04, 0x08, 0xc7, 0x2c, 0xe6, - 0x10, 0x27, 0x13, 0x01, 0x2c, 0xfb, 0x75, 0x9a, 0x50, 0x1b, 0xe9, 0x94, 0x57, 0x05, 0xbf, 0x92, 0xff, 0x9b, 0xe1, - 0x91, 0x3d, 0xd6, 0x61, 0x51, 0xa3, 0x2c, 0x97, 0xda, 0x5d, 0x53, 0x2b, 0xaf, 0x23, 0x32, 0x15, 0xfe, 0x98, 0x6d, - 0x1b, 0xe8, 0x7e, 0xdb, 0x64, 0xd1, 0xf9, 0xfa, 0xb0, 0xd3, 0x2e, 0x6c, 0x6c, 0x43, 0x77, 0xf7, 0xdd, 0x25, 0xa2, - 0xd5, 0x3e, 0xb4, 0x9a, 0x27, 0x9f, 0xd3, 0xae, 0xdb, 0x79, 0xdc, 0xb1, 0xb1, 0xbc, 0x6b, 0x01, 0x15, 0x25, 0x33, - 0x08, 0xc0, 0x43, 0xfc, 0xbb, 0xa7, 0x52, 0xef, 0xfc, 0x7e, 0xf0, 0x3c, 0xec, 0xb4, 0x6d, 0x6c, 0xe7, 0x3c, 0x9d, - 0x7d, 0xc6, 0x14, 0xf6, 0x6d, 0x6c, 0x07, 0x71, 0x9a, 0x53, 0x73, 0x0e, 0x52, 0x9d, 0xfd, 0xfd, 0x93, 0x90, 0x10, - 0xcd, 0x32, 0x9a, 0xe7, 0x96, 0xd9, 0xbf, 0x22, 0xa5, 0x4f, 0x30, 0xcc, 0x8d, 0x14, 0x97, 0x53, 0x2e, 0xf0, 0x22, - 0xaf, 0x41, 0x30, 0xa9, 0x4a, 0x96, 0xad, 0x11, 0x9b, 0x10, 0x01, 0x25, 0x63, 0x93, 0xda, 0xd5, 0x27, 0x47, 0xde, - 0xb0, 0xf5, 0xe4, 0xc0, 0x32, 0x70, 0xbe, 0x3e, 0x40, 0xad, 0x64, 0xca, 0x92, 0xf3, 0x0d, 0xa5, 0xfe, 0xcd, 0x86, - 0x52, 0x50, 0xd9, 0x4a, 0xe8, 0xd4, 0x15, 0x3d, 0x9f, 0xc6, 0x7a, 0xa5, 0xf8, 0x98, 0x20, 0x86, 0xc2, 0xff, 0xf8, - 0x09, 0x48, 0x8d, 0x65, 0x10, 0x3d, 0xfc, 0xf6, 0xe1, 0xa0, 0xe4, 0x73, 0x86, 0x2b, 0x7b, 0xf9, 0x7d, 0x33, 0x84, - 0xd2, 0x26, 0x38, 0xf9, 0xe3, 0xcf, 0x9a, 0x2b, 0xbd, 0xf9, 0x34, 0xc1, 0x19, 0x5a, 0xd5, 0xef, 0x58, 0x7a, 0x75, - 0xd4, 0x7f, 0x75, 0xed, 0x37, 0x14, 0x2b, 0xc5, 0xa7, 0x5c, 0xff, 0x20, 0x66, 0xd3, 0x8a, 0x04, 0xd6, 0xc1, 0x14, - 0x1a, 0x0f, 0x64, 0x7c, 0x99, 0x9d, 0x48, 0xd5, 0xe7, 0x1c, 0xce, 0xb1, 0xc2, 0x55, 0x21, 0xf3, 0x8c, 0x9e, 0xc7, - 0xe9, 0xf5, 0xea, 0xe5, 0x67, 0xdb, 0x2b, 0x47, 0x6c, 0x12, 0x19, 0x87, 0xd3, 0x28, 0x29, 0x17, 0xe1, 0xce, 0x01, - 0x8a, 0x7f, 0xf9, 0x67, 0xd7, 0xfd, 0x97, 0x7f, 0xfe, 0x64, 0x55, 0xe8, 0xbe, 0xb8, 0xc0, 0xbc, 0xea, 0x76, 0xfb, - 0xee, 0xda, 0x3c, 0x52, 0x1d, 0xe7, 0x9b, 0xeb, 0xac, 0x2d, 0x02, 0xbc, 0x5f, 0x5b, 0x82, 0xb5, 0x42, 0xb9, 0xfb, - 0xac, 0xdf, 0x02, 0x18, 0xcc, 0xeb, 0x93, 0x90, 0x41, 0xa5, 0xdf, 0x05, 0xda, 0x05, 0xf2, 0xee, 0xb5, 0x22, 0xbf, - 0x1d, 0xc3, 0x9f, 0x9a, 0xc3, 0xef, 0x04, 0x5f, 0xf9, 0x27, 0xe2, 0x8b, 0x8b, 0x32, 0x0b, 0xd1, 0x6c, 0x0a, 0x77, - 0x1c, 0x0c, 0xd6, 0x4a, 0x94, 0xe2, 0xe1, 0xb5, 0x51, 0x5f, 0x9c, 0xa1, 0x24, 0xf1, 0xc5, 0x2b, 0xb8, 0xd8, 0xe8, - 0xf8, 0x32, 0xd3, 0xce, 0xd6, 0x3b, 0x84, 0x03, 0x74, 0x51, 0x9f, 0x95, 0xe8, 0x74, 0x4d, 0x32, 0x40, 0x29, 0x98, - 0x1b, 0x00, 0x26, 0x8e, 0x2f, 0x94, 0xb5, 0x79, 0x2a, 0xdd, 0x30, 0xde, 0x2a, 0x69, 0x2b, 0xf7, 0x4c, 0x0d, 0xe9, - 0xd8, 0x7a, 0x2f, 0xf0, 0x25, 0x2a, 0xd3, 0xca, 0xba, 0x17, 0xae, 0x2e, 0xb0, 0x23, 0x4a, 0xf6, 0x73, 0xe5, 0xc7, - 0x57, 0xf7, 0x63, 0x7c, 0xdb, 0x05, 0xea, 0xd2, 0x5a, 0xfe, 0xa3, 0x55, 0x82, 0x65, 0x73, 0xb9, 0x49, 0x1f, 0xb8, - 0xf6, 0x39, 0xcd, 0xce, 0x23, 0x48, 0x84, 0xca, 0x3e, 0xc1, 0x9c, 0x60, 0xa5, 0x31, 0x15, 0x7f, 0x19, 0x51, 0x17, - 0x49, 0xff, 0x83, 0x38, 0x15, 0x83, 0x2c, 0x46, 0x18, 0xca, 0x58, 0x84, 0xff, 0xcf, 0xb7, 0xfe, 0xc3, 0xf0, 0xad, - 0xbb, 0x87, 0xa8, 0x9d, 0x91, 0xfe, 0xec, 0x85, 0xfc, 0x8f, 0xcd, 0xee, 0x72, 0xc1, 0xee, 0x7e, 0x03, 0xa3, 0xcb, - 0xff, 0x31, 0x8c, 0x4e, 0xd8, 0xc8, 0x9a, 0xd3, 0xad, 0x85, 0x9a, 0x6f, 0x5d, 0xff, 0xda, 0xbf, 0xad, 0xf6, 0x55, - 0x7c, 0x71, 0x72, 0xed, 0xdf, 0x56, 0x8b, 0xb0, 0x9d, 0x5d, 0xac, 0xf6, 0x31, 0xb0, 0xdf, 0xbc, 0xb6, 0x3d, 0xfb, - 0xcd, 0xd7, 0x5f, 0xdb, 0xf8, 0x22, 0xa7, 0x7c, 0x00, 0x85, 0x64, 0x77, 0xb1, 0xb3, 0x5a, 0x11, 0xdc, 0x28, 0x30, - 0x45, 0x11, 0xf6, 0x82, 0xa4, 0x43, 0xe3, 0x3d, 0xcb, 0xcf, 0xd3, 0xc4, 0x84, 0xe6, 0x2d, 0x58, 0xf6, 0x9f, 0x0b, - 0x8e, 0xe8, 0x65, 0x0d, 0x1e, 0x51, 0xba, 0x0a, 0x90, 0x28, 0xac, 0x41, 0x54, 0x5d, 0x19, 0x74, 0x37, 0xff, 0xaf, - 0xae, 0x45, 0x90, 0xb7, 0x7d, 0x44, 0x83, 0xf8, 0xe2, 0x73, 0xc4, 0x87, 0x1c, 0xac, 0xf2, 0xd8, 0x69, 0x77, 0xa7, - 0x5f, 0xec, 0x2e, 0xa2, 0xbd, 0x3d, 0x36, 0xb0, 0xb1, 0xb8, 0xa7, 0xa9, 0xd8, 0x24, 0x5c, 0x72, 0xf8, 0x93, 0xc1, - 0x9f, 0xb4, 0x62, 0xd4, 0x2c, 0x19, 0x67, 0x7e, 0x46, 0xc3, 0xed, 0x4c, 0xba, 0xbc, 0xdf, 0x48, 0x91, 0x86, 0x4c, - 0xc0, 0xce, 0xcf, 0x45, 0xea, 0xd1, 0x94, 0x81, 0x3e, 0xba, 0x63, 0x7e, 0xc5, 0x47, 0x5d, 0x88, 0x56, 0x7e, 0x04, - 0xc0, 0x44, 0x38, 0x25, 0x79, 0x99, 0xeb, 0x00, 0xb7, 0x6a, 0xaa, 0xec, 0x10, 0x6c, 0x23, 0xe1, 0x75, 0x0f, 0x49, - 0x5f, 0xa4, 0x3d, 0xbc, 0x48, 0xb8, 0x13, 0xba, 0x3c, 0x63, 0x53, 0x07, 0xe1, 0x4e, 0x1b, 0x21, 0xed, 0x6c, 0x08, - 0x49, 0x7f, 0x87, 0xe5, 0xaf, 0xfd, 0xd7, 0x4e, 0x28, 0x2e, 0xe2, 0x12, 0x9f, 0xee, 0x81, 0x43, 0x92, 0x4f, 0xe6, - 0xe3, 0x31, 0xcd, 0x1c, 0x7d, 0x00, 0xf0, 0xab, 0x03, 0x38, 0x63, 0x0c, 0x6f, 0x9f, 0xfa, 0xdc, 0xff, 0x96, 0xd1, - 0x6b, 0x27, 0x45, 0xbd, 0xac, 0xba, 0x9c, 0x31, 0xc4, 0x73, 0x44, 0xfa, 0x11, 0x24, 0xc6, 0xbf, 0x48, 0xf8, 0x7e, - 0xd7, 0x99, 0x7f, 0x75, 0x80, 0x43, 0xb8, 0xf2, 0x42, 0x67, 0x75, 0xcb, 0xbb, 0x4a, 0x3e, 0xb0, 0x84, 0x1f, 0xc9, - 0x63, 0x98, 0x29, 0x52, 0xee, 0xc3, 0x32, 0x23, 0xc6, 0xf2, 0xcb, 0x0e, 0x43, 0xd2, 0x0f, 0x1a, 0x44, 0x1e, 0xca, - 0x14, 0xb7, 0xec, 0x9e, 0x46, 0x7e, 0x76, 0x0a, 0x07, 0xbe, 0x01, 0xd0, 0x4b, 0x9e, 0xfa, 0x4e, 0x50, 0x7e, 0xc9, - 0xc9, 0x69, 0xfd, 0xd4, 0x68, 0x4d, 0xb0, 0x48, 0x8a, 0xa9, 0x8a, 0x5a, 0x50, 0x74, 0x6e, 0x16, 0x91, 0xc6, 0x6e, - 0x0b, 0xc3, 0x1e, 0xec, 0x6d, 0xf4, 0xd1, 0xea, 0xa5, 0x6b, 0x5e, 0x67, 0xfe, 0xac, 0x8c, 0x1b, 0x9c, 0xfa, 0x59, - 0xc6, 0x68, 0x66, 0x39, 0xcf, 0x7f, 0x45, 0xde, 0xbf, 0xfc, 0xf3, 0xe6, 0xf8, 0x81, 0x0a, 0x19, 0x58, 0x90, 0x5c, - 0xd2, 0x14, 0xe9, 0xd8, 0xc4, 0x0e, 0x64, 0x43, 0x5b, 0x87, 0x3b, 0xf6, 0x8f, 0xda, 0xed, 0xb6, 0x0a, 0x09, 0x74, - 0xe4, 0x4f, 0x88, 0x01, 0xc0, 0x4f, 0x78, 0x10, 0x51, 0x65, 0x62, 0xcb, 0x00, 0xe5, 0x51, 0x7b, 0x76, 0x63, 0xf7, - 0x61, 0x3b, 0x28, 0x28, 0xde, 0xd1, 0x19, 0xf5, 0xf9, 0x67, 0x8d, 0x9f, 0x89, 0x26, 0xe5, 0xf0, 0x1d, 0x3d, 0x74, - 0x35, 0xee, 0xca, 0xa0, 0x87, 0xab, 0x83, 0xbe, 0x67, 0x53, 0x71, 0x75, 0xd3, 0xb6, 0x51, 0x85, 0xa7, 0xba, 0x36, - 0x26, 0x97, 0x2d, 0x6c, 0x4b, 0x60, 0x3c, 0x4a, 0xe3, 0x90, 0x66, 0xc4, 0xa6, 0xee, 0xc4, 0xb5, 0x1e, 0xb7, 0xdb, - 0x6d, 0xdc, 0x3c, 0x38, 0x6c, 0xb7, 0xf1, 0xe1, 0xc3, 0x36, 0x6e, 0xc2, 0x1f, 0xd7, 0x75, 0x57, 0x60, 0xb8, 0x2b, - 0x6a, 0xdb, 0x69, 0x67, 0x74, 0xaa, 0x00, 0xbc, 0x33, 0xac, 0x58, 0xed, 0x09, 0xb8, 0x60, 0x5a, 0xed, 0x7b, 0x29, - 0xd9, 0xd4, 0x05, 0x07, 0x2a, 0x1d, 0x55, 0xf8, 0x0b, 0xd3, 0x2a, 0x68, 0x4a, 0xe5, 0xc5, 0x7f, 0x2f, 0x14, 0x21, - 0x78, 0xd6, 0x29, 0xdc, 0x5e, 0x2a, 0xe2, 0xa5, 0x90, 0x0a, 0x04, 0x1f, 0x48, 0xe3, 0x3e, 0x4b, 0xe0, 0xdb, 0x59, - 0x3a, 0x6a, 0xaa, 0x19, 0x55, 0xba, 0x92, 0x74, 0xfb, 0x40, 0x86, 0xa5, 0x37, 0x11, 0xc4, 0xe8, 0x01, 0xc2, 0xfe, - 0x7d, 0x1a, 0xa8, 0x15, 0x84, 0xfa, 0xc1, 0x7d, 0xea, 0x6b, 0xec, 0x8f, 0x1e, 0x88, 0xe4, 0xa4, 0x9d, 0x68, 0xb9, - 0xdc, 0xf1, 0x97, 0xcb, 0x9d, 0xe0, 0xfe, 0x33, 0x94, 0xcb, 0xab, 0x4f, 0x41, 0xc0, 0xcd, 0x9f, 0x12, 0xe8, 0x17, - 0x50, 0xee, 0x45, 0x58, 0x82, 0x24, 0x9f, 0x7c, 0xac, 0x06, 0x94, 0x8f, 0x41, 0xb1, 0x82, 0x94, 0x90, 0x44, 0xd2, - 0x3e, 0x5f, 0x2e, 0x15, 0xf1, 0xe3, 0x39, 0xf1, 0xcb, 0xa2, 0x8e, 0x8d, 0x67, 0x24, 0x28, 0x1f, 0x6d, 0x01, 0xf2, - 0x4c, 0x71, 0xa9, 0x0a, 0xe2, 0x6b, 0x3f, 0x4b, 0x4c, 0x80, 0x5f, 0xa7, 0x96, 0x1a, 0xd6, 0x9a, 0x65, 0xe9, 0x15, - 0x83, 0xe4, 0x97, 0x95, 0x81, 0xa7, 0x04, 0x2e, 0xfe, 0xea, 0x99, 0xa1, 0x70, 0xa3, 0x83, 0xf7, 0x9a, 0xcf, 0xc2, - 0x2d, 0x93, 0xe5, 0x04, 0xbd, 0x50, 0xcd, 0xcd, 0x9b, 0xeb, 0x69, 0xbd, 0xf3, 0xaf, 0xbd, 0x99, 0x7e, 0x78, 0x26, - 0xf3, 0x6c, 0xbc, 0x69, 0x79, 0xb2, 0xe6, 0x2d, 0x79, 0x0d, 0xb1, 0x1f, 0x5b, 0xf3, 0x6d, 0xb8, 0x67, 0x53, 0xf2, - 0xb8, 0x77, 0x2f, 0xcf, 0xa8, 0x9f, 0x05, 0xd1, 0x5b, 0x3f, 0xf3, 0xa7, 0x79, 0x6f, 0xac, 0x6f, 0xf1, 0xd2, 0x14, - 0x70, 0x3e, 0x16, 0x99, 0x4e, 0x49, 0x70, 0x6b, 0xe3, 0x10, 0xe1, 0xea, 0xbd, 0x84, 0x40, 0xfa, 0xb9, 0x6d, 0x3c, - 0x37, 0x5f, 0xc1, 0x3a, 0xdb, 0x78, 0x8a, 0xb0, 0x4c, 0x20, 0x7a, 0xfb, 0x47, 0xa6, 0x0e, 0x61, 0xc8, 0x75, 0xf1, - 0xc6, 0x6e, 0xf5, 0x95, 0x3b, 0x9d, 0x4c, 0xf4, 0x7e, 0x25, 0x99, 0x68, 0x03, 0x1a, 0xad, 0x8c, 0xe6, 0xb3, 0x34, - 0xc9, 0xa9, 0x8d, 0xdf, 0x43, 0x3b, 0x79, 0x15, 0xb3, 0xd9, 0x70, 0x8d, 0xe6, 0xca, 0xa6, 0xe2, 0x8d, 0x6c, 0x07, - 0x41, 0x9d, 0xf7, 0xdf, 0x97, 0x71, 0x7c, 0x1d, 0xdf, 0x11, 0x89, 0xe8, 0x8c, 0x6e, 0xc9, 0x95, 0xcd, 0xe9, 0x27, - 0x73, 0x65, 0xe3, 0x7b, 0xe5, 0xca, 0xe6, 0xf4, 0x8f, 0xce, 0x95, 0x65, 0xd4, 0xc8, 0x95, 0x05, 0x39, 0xf7, 0xf5, - 0xbd, 0x52, 0x2e, 0x75, 0x26, 0x5c, 0x7a, 0x9d, 0x93, 0x8e, 0x8a, 0x81, 0xc4, 0xe9, 0x04, 0xf2, 0x2d, 0xff, 0xf1, - 0xe9, 0x93, 0x71, 0x3a, 0x31, 0x93, 0x27, 0xe1, 0xc3, 0x24, 0x40, 0x76, 0x38, 0x23, 0x0b, 0xfb, 0xa7, 0x9b, 0xce, - 0x93, 0x61, 0xa7, 0xb7, 0xdf, 0x99, 0xda, 0x9e, 0x0d, 0x4e, 0x47, 0x51, 0xd0, 0xee, 0xed, 0xef, 0x43, 0xc1, 0xb5, - 0x51, 0xd0, 0x85, 0x02, 0x66, 0x14, 0x1c, 0x42, 0x41, 0x60, 0x14, 0x3c, 0x84, 0x82, 0xd0, 0x28, 0x78, 0x04, 0x05, - 0x57, 0x76, 0x31, 0x64, 0x65, 0x42, 0xf0, 0x23, 0x24, 0x6e, 0x30, 0xdc, 0xc9, 0xea, 0xa7, 0xb7, 0x23, 0xa2, 0xab, - 0x3c, 0x2a, 0x6f, 0x7e, 0x68, 0x1e, 0xe8, 0x8b, 0x0a, 0x2f, 0xbe, 0xb8, 0x00, 0xd6, 0x0a, 0x17, 0xb1, 0x60, 0x88, - 0x49, 0xca, 0x9a, 0xfb, 0xfa, 0xb5, 0xed, 0x95, 0x59, 0xb3, 0x6d, 0xdc, 0xd5, 0x79, 0xb3, 0x9e, 0x8d, 0x04, 0x5f, - 0x92, 0x2f, 0x0e, 0x1b, 0xa1, 0xea, 0x16, 0xee, 0x00, 0xac, 0x2e, 0xe0, 0xdc, 0x47, 0x78, 0xaa, 0x15, 0x20, 0xea, - 0xc0, 0x07, 0x18, 0xde, 0xb3, 0x29, 0xd5, 0xfb, 0x45, 0x0f, 0x60, 0x89, 0xcc, 0xe2, 0x5e, 0x54, 0x29, 0x46, 0x6f, - 0xf1, 0xb8, 0xba, 0xf3, 0xf5, 0x3d, 0x91, 0x77, 0xe8, 0x65, 0x58, 0x86, 0xb9, 0x66, 0x98, 0xfb, 0x13, 0x0f, 0x52, - 0x28, 0x21, 0x63, 0xc4, 0x1b, 0x13, 0x42, 0xda, 0x83, 0xb9, 0xf7, 0x16, 0x5f, 0x47, 0x34, 0xf1, 0xa6, 0x45, 0xaf, - 0x5c, 0x7f, 0x99, 0xd2, 0xf9, 0xbe, 0xbc, 0x28, 0x5c, 0xd0, 0x44, 0xf5, 0x56, 0x42, 0xd9, 0x2c, 0x69, 0x67, 0x4b, - 0xce, 0x9f, 0xa1, 0xec, 0x8c, 0xe3, 0xf4, 0xba, 0x09, 0xe2, 0x7e, 0x63, 0x1e, 0x20, 0xcc, 0xad, 0xcc, 0x03, 0x7c, - 0x09, 0xb0, 0x96, 0x4f, 0xef, 0xfd, 0x49, 0xf9, 0xfb, 0x15, 0xcd, 0x73, 0x7f, 0xa2, 0x6a, 0x6e, 0xcf, 0xfb, 0x13, - 0x20, 0x9a, 0x39, 0x7f, 0x1a, 0x08, 0x48, 0xce, 0x03, 0x84, 0x40, 0x40, 0x57, 0xe5, 0xea, 0xc1, 0xcc, 0xeb, 0x69, - 0x7e, 0x02, 0x55, 0xf5, 0x22, 0xee, 0x4f, 0xaa, 0x82, 0xe3, 0x59, 0x46, 0x55, 0x02, 0x21, 0x60, 0xb1, 0x38, 0x6e, - 0x41, 0x81, 0x7c, 0xbd, 0x25, 0x9d, 0x4f, 0x73, 0x97, 0xed, 0x49, 0x7d, 0x96, 0x4e, 0xe7, 0x33, 0x4f, 0xa6, 0x94, - 0xc7, 0x52, 0xd6, 0x33, 0xf2, 0xbe, 0xec, 0x04, 0xf0, 0x9f, 0x3a, 0x78, 0xf1, 0xe5, 0x78, 0x3c, 0xbe, 0x33, 0xbd, - 0xef, 0xcb, 0x70, 0x4c, 0xbb, 0xf4, 0xb0, 0x07, 0xa7, 0x16, 0x9a, 0x2a, 0x11, 0xad, 0x53, 0x08, 0xdc, 0x2d, 0xee, - 0x57, 0x19, 0x72, 0xd6, 0x78, 0xb4, 0xb8, 0x7f, 0xaa, 0x5f, 0x31, 0xcb, 0xe8, 0x62, 0xea, 0x67, 0x13, 0x96, 0x78, - 0xed, 0xc2, 0xbd, 0x5a, 0x28, 0x50, 0x8f, 0x8e, 0x8e, 0x0a, 0x37, 0xd4, 0x4f, 0xed, 0x30, 0x2c, 0xdc, 0x60, 0x51, - 0x4e, 0xa3, 0xdd, 0x1e, 0x8f, 0x0b, 0x97, 0xe9, 0x82, 0xfd, 0x6e, 0x10, 0xee, 0x77, 0x0b, 0xf7, 0xda, 0xa8, 0x51, - 0xb8, 0x54, 0x3d, 0x65, 0x34, 0xac, 0x1d, 0x7d, 0x78, 0xd4, 0x6e, 0x17, 0xae, 0x24, 0xb4, 0x05, 0xc4, 0xe4, 0xe4, - 0x4f, 0xcf, 0x9f, 0x73, 0x30, 0x98, 0x8a, 0x5e, 0xcc, 0x9d, 0xe1, 0xae, 0xba, 0x56, 0x52, 0x7e, 0x87, 0xb1, 0x40, - 0x23, 0xfc, 0xb5, 0x99, 0x39, 0x07, 0xc4, 0x2c, 0x32, 0xe6, 0x62, 0x9d, 0x58, 0x57, 0x7b, 0x0d, 0x94, 0x25, 0x5e, - 0x7f, 0x4d, 0xe2, 0x2a, 0xa1, 0x0e, 0xf8, 0x18, 0xd4, 0x94, 0xb7, 0x9f, 0x27, 0xdb, 0xa4, 0x47, 0xf6, 0x69, 0xe9, - 0x71, 0x79, 0x1f, 0xe1, 0x91, 0xfd, 0xe1, 0xc2, 0x23, 0x31, 0x85, 0x87, 0x64, 0x1d, 0xd7, 0x9c, 0xd8, 0x41, 0x44, - 0x83, 0x8f, 0x97, 0xe9, 0x4d, 0x13, 0xb6, 0x44, 0x66, 0x0b, 0xb1, 0x72, 0xf5, 0x5b, 0x33, 0xf9, 0x75, 0x67, 0xc6, - 0x47, 0x1c, 0x85, 0x8e, 0xff, 0x26, 0x21, 0xf6, 0x1b, 0x1d, 0xd8, 0x93, 0x25, 0xe3, 0x31, 0xb1, 0xdf, 0x8c, 0xc7, - 0xb6, 0xbe, 0x1c, 0xc7, 0xe7, 0x54, 0xd4, 0x7a, 0x5d, 0x2b, 0x11, 0xb5, 0xc0, 0xd0, 0xaf, 0xca, 0xcc, 0x02, 0x95, - 0x77, 0x67, 0xe6, 0xd8, 0xa9, 0x37, 0x21, 0xcb, 0x61, 0xab, 0xc1, 0xb7, 0x25, 0xeb, 0x97, 0xf3, 0x27, 0xb5, 0x2f, - 0x29, 0x95, 0x00, 0x6f, 0xf8, 0xfc, 0xd3, 0xea, 0xcd, 0x70, 0x13, 0xaa, 0x55, 0xfc, 0x27, 0xb7, 0x2f, 0x42, 0xe7, - 0x9a, 0xa3, 0x82, 0xe5, 0x6f, 0x92, 0x95, 0x5b, 0x1f, 0x24, 0x8c, 0x84, 0x98, 0xd3, 0x2a, 0x78, 0x3a, 0x99, 0xc4, - 0xe2, 0x30, 0x49, 0xcd, 0xe0, 0x96, 0xcd, 0x07, 0xb5, 0xf9, 0x7a, 0x66, 0x43, 0xf5, 0x79, 0x0d, 0xf1, 0xbd, 0x61, - 0x79, 0x5a, 0xf8, 0x4a, 0x7d, 0x78, 0x56, 0xc4, 0x04, 0x17, 0x8a, 0xc7, 0x2f, 0xe4, 0x19, 0x53, 0x8e, 0x59, 0x28, - 0x9b, 0xb3, 0xb0, 0x28, 0xd4, 0xe9, 0xfc, 0x90, 0xe5, 0x33, 0xd0, 0x9e, 0x64, 0x4b, 0xfa, 0x29, 0x16, 0x9e, 0x5f, - 0x1b, 0xc9, 0x6d, 0xb5, 0xe5, 0x2a, 0xb4, 0x9d, 0x26, 0xb3, 0x85, 0xae, 0x79, 0x61, 0x2b, 0x93, 0x4d, 0x23, 0xd1, - 0xb6, 0x24, 0x3e, 0x65, 0xda, 0x9d, 0x31, 0x43, 0xc8, 0xfc, 0x29, 0x17, 0x44, 0xbf, 0xd2, 0x05, 0x85, 0x69, 0x65, - 0x89, 0x37, 0x12, 0x5b, 0x22, 0x55, 0x2c, 0x9f, 0xf9, 0x89, 0x36, 0xe6, 0x24, 0x3f, 0xd8, 0x5d, 0x54, 0x2b, 0x5f, - 0xd8, 0x1a, 0x6c, 0x49, 0xbc, 0xfd, 0xe3, 0x16, 0x34, 0xe8, 0x5b, 0x35, 0xd0, 0x93, 0xb5, 0x0c, 0xb3, 0xbb, 0xf3, - 0xae, 0x3f, 0x5e, 0xb8, 0xf9, 0x35, 0x76, 0xf3, 0x6b, 0xeb, 0xab, 0x45, 0xf3, 0x9a, 0x5e, 0x7e, 0x64, 0xbc, 0xc9, - 0xfd, 0x59, 0x13, 0xbc, 0xa7, 0x22, 0x33, 0x44, 0xb1, 0x67, 0xa1, 0xa3, 0x4b, 0xd3, 0xaf, 0x37, 0xcf, 0x21, 0x3d, - 0x5b, 0x98, 0x51, 0x5e, 0x92, 0x26, 0xb4, 0x57, 0x3f, 0xbe, 0x67, 0x66, 0x18, 0x6b, 0x6c, 0x8d, 0x16, 0x29, 0xa4, - 0x73, 0xf3, 0x5b, 0xaf, 0xad, 0xd8, 0x7a, 0x5b, 0xa7, 0x0f, 0xb7, 0x37, 0xd6, 0xf7, 0x14, 0x72, 0x1b, 0x42, 0x7a, - 0x65, 0xeb, 0xf9, 0xcf, 0xdb, 0xf2, 0xbb, 0x3f, 0x75, 0x98, 0x0d, 0xf2, 0x49, 0xf4, 0xff, 0xc6, 0x29, 0xc0, 0xd5, - 0x62, 0x71, 0x98, 0xed, 0x3e, 0x90, 0x79, 0xfe, 0x98, 0xd3, 0x0c, 0xdf, 0xa7, 0xe6, 0xa5, 0xb8, 0x77, 0x62, 0x01, - 0x62, 0xc6, 0xeb, 0x1c, 0xd5, 0x53, 0xb1, 0xef, 0xee, 0xfe, 0xee, 0xe9, 0x17, 0x0a, 0x47, 0xfa, 0x1e, 0x56, 0xdb, - 0xee, 0xc1, 0x46, 0x88, 0xfd, 0x5b, 0x8f, 0x25, 0x42, 0xe6, 0x5d, 0x42, 0x52, 0x48, 0x6f, 0x96, 0xaa, 0x53, 0x99, - 0x19, 0x8d, 0xc5, 0xa7, 0xd7, 0xd5, 0x52, 0xec, 0x3f, 0x9c, 0xdd, 0xe8, 0xd5, 0xe8, 0xac, 0x9c, 0xb6, 0xfc, 0x43, - 0x0f, 0x55, 0x6e, 0x3f, 0xc5, 0x59, 0x3f, 0x18, 0x78, 0x38, 0xbb, 0xe9, 0x49, 0x41, 0xdb, 0xcc, 0x24, 0x54, 0xed, - 0xd9, 0x8d, 0x79, 0xac, 0xb4, 0xea, 0xc8, 0x72, 0xf7, 0x73, 0x8b, 0xfa, 0x39, 0xed, 0xc1, 0x97, 0xa6, 0x58, 0xe0, - 0xc7, 0x4a, 0x98, 0x4f, 0x59, 0x18, 0xc6, 0xb4, 0xa7, 0xe5, 0xb5, 0xd5, 0x79, 0x08, 0xa7, 0x32, 0xcd, 0x25, 0xab, - 0xaf, 0x8a, 0x81, 0xbc, 0x12, 0x4f, 0xfe, 0x65, 0x9e, 0xc6, 0xf0, 0x9d, 0xc7, 0x8d, 0xe8, 0x54, 0xc7, 0x15, 0xdb, - 0x15, 0xf2, 0xc4, 0xef, 0xfa, 0x5c, 0x0e, 0xdb, 0x7f, 0xea, 0x89, 0x05, 0x6f, 0xf7, 0x78, 0x3a, 0xf3, 0x9a, 0xfb, - 0xf5, 0x89, 0xc0, 0xab, 0x72, 0x0a, 0x78, 0xc3, 0xb4, 0x30, 0x48, 0x2b, 0xc9, 0xa7, 0x2d, 0xb7, 0xa3, 0xca, 0x44, - 0x07, 0x60, 0x84, 0x96, 0x45, 0x45, 0x7d, 0x32, 0xff, 0x98, 0xdd, 0xf2, 0x78, 0xf3, 0x6e, 0x79, 0xac, 0x77, 0xcb, - 0xdd, 0x14, 0xfb, 0xe5, 0xb8, 0x03, 0xff, 0xf5, 0xaa, 0x09, 0x79, 0x6d, 0x6b, 0x7f, 0x76, 0x63, 0x81, 0x9e, 0xd6, - 0xec, 0xce, 0x6e, 0xe4, 0xa1, 0x5a, 0x48, 0x5c, 0x6b, 0xc3, 0x31, 0x53, 0xdc, 0xb6, 0xa0, 0x10, 0xfe, 0x6f, 0xd7, - 0x5e, 0x75, 0x0e, 0xe0, 0x1d, 0xb4, 0x3a, 0x5c, 0x7f, 0xd7, 0xbd, 0x7b, 0xd3, 0x7a, 0x49, 0xca, 0x1d, 0x4f, 0x73, - 0x63, 0xe4, 0x72, 0xff, 0xf2, 0x92, 0x86, 0xde, 0x38, 0x0d, 0xe6, 0xf9, 0x3f, 0x29, 0xf8, 0x15, 0x12, 0xef, 0xdc, - 0xd2, 0x2b, 0xfd, 0xe8, 0xa6, 0xf2, 0x88, 0xaf, 0xee, 0x61, 0x51, 0xae, 0x93, 0x97, 0x07, 0x7e, 0x4c, 0x9d, 0xae, - 0x7b, 0xb0, 0x61, 0x13, 0xfc, 0x9b, 0xac, 0xcd, 0xc6, 0xc9, 0xfc, 0x5e, 0x64, 0xdc, 0x89, 0x84, 0xcf, 0xc2, 0x81, - 0xb9, 0x86, 0xed, 0xa3, 0xcd, 0xe0, 0x0e, 0xf5, 0x48, 0x23, 0x2d, 0x14, 0x94, 0xdc, 0x09, 0xe9, 0xd8, 0x9f, 0xc7, - 0xfc, 0xee, 0x5e, 0xb7, 0x51, 0xc6, 0x5a, 0xaf, 0x77, 0x30, 0xf4, 0xaa, 0xee, 0x3d, 0xb9, 0xf4, 0x97, 0x8f, 0x0f, - 0xe0, 0x3f, 0x79, 0xf8, 0xe5, 0xb2, 0xd2, 0xd5, 0xa5, 0xd5, 0x0b, 0xba, 0xfa, 0x55, 0x4d, 0x19, 0x97, 0x22, 0x5c, - 0xe8, 0xe3, 0xf7, 0xad, 0x0d, 0x5a, 0xe5, 0xbd, 0xaa, 0x2b, 0x2d, 0xeb, 0xb3, 0x6a, 0x7f, 0x5e, 0xe7, 0xf7, 0xac, - 0x1b, 0x48, 0xcd, 0xb5, 0x5e, 0x57, 0x7d, 0x7a, 0x7e, 0xad, 0xb2, 0xc6, 0xb8, 0xa8, 0x7f, 0x45, 0x2e, 0x4b, 0x13, - 0x45, 0xa6, 0xa2, 0x82, 0x95, 0x72, 0x25, 0xad, 0x94, 0x94, 0x92, 0x8b, 0xe3, 0xc1, 0xcd, 0x34, 0xb6, 0xae, 0xe4, - 0xfd, 0x38, 0xc4, 0xee, 0xb8, 0x6d, 0xdb, 0x12, 0x4e, 0x3a, 0xf8, 0x4c, 0x97, 0xfd, 0xe1, 0xfd, 0xd7, 0xcd, 0x23, - 0x7b, 0x00, 0x9a, 0xd6, 0xd5, 0x44, 0x68, 0x76, 0x2f, 0xfd, 0x5b, 0x9a, 0x9d, 0x77, 0x95, 0x0b, 0x5e, 0xe6, 0x8b, - 0x8b, 0x32, 0xab, 0x6b, 0x5b, 0x37, 0xd3, 0x38, 0xc9, 0x89, 0x1d, 0x71, 0x3e, 0xf3, 0x5a, 0xad, 0xeb, 0xeb, 0x6b, - 0xf7, 0x7a, 0xdf, 0x4d, 0xb3, 0x49, 0xab, 0xdb, 0x6e, 0xb7, 0xe1, 0x8b, 0x1f, 0xb6, 0x75, 0xc5, 0xe8, 0xf5, 0x93, - 0xf4, 0x86, 0xd8, 0x6d, 0xab, 0x6d, 0x75, 0xba, 0x47, 0x56, 0xa7, 0x7b, 0xe0, 0x3e, 0x3c, 0xb2, 0xfb, 0x5f, 0x58, - 0xd6, 0x71, 0x48, 0xc7, 0x39, 0xfc, 0xb0, 0xac, 0x63, 0xa1, 0x78, 0xc9, 0xdf, 0x96, 0xe5, 0x06, 0x71, 0xde, 0xec, - 0x58, 0x0b, 0xf5, 0x68, 0x59, 0x70, 0x8b, 0x90, 0x67, 0x7d, 0x39, 0xee, 0x8e, 0x0f, 0xc6, 0x8f, 0x7b, 0xaa, 0xb8, - 0xf8, 0xa2, 0x56, 0x1d, 0xcb, 0x7f, 0xbb, 0x46, 0xb3, 0x9c, 0x67, 0xe9, 0x47, 0xaa, 0x5c, 0xfb, 0x16, 0x88, 0x9e, - 0x8d, 0x4d, 0xbb, 0xeb, 0x23, 0x75, 0x8e, 0x2e, 0x83, 0x71, 0xb7, 0xaa, 0x2e, 0x60, 0x6c, 0x95, 0x40, 0x1e, 0xb7, - 0x34, 0xe8, 0xc7, 0x26, 0x9a, 0x3a, 0xcd, 0x4d, 0x88, 0xea, 0xd8, 0x6a, 0x8e, 0x13, 0x3d, 0xbf, 0x63, 0x38, 0xb4, - 0xae, 0x75, 0x55, 0x01, 0x81, 0x6d, 0x85, 0xc4, 0x7e, 0xd5, 0xe9, 0x1e, 0xe1, 0x4e, 0xe7, 0xa1, 0xfb, 0xf0, 0x28, - 0x68, 0xe3, 0x03, 0xf7, 0xa0, 0xb9, 0xef, 0x3e, 0xc4, 0x47, 0xcd, 0x23, 0x7c, 0xf4, 0xfc, 0x28, 0x68, 0x1e, 0xb8, - 0x07, 0xb8, 0xdd, 0x3c, 0x82, 0xc2, 0xe6, 0x51, 0xf3, 0xe8, 0xaa, 0x79, 0x70, 0x14, 0xb4, 0x45, 0x69, 0xd7, 0x3d, - 0x3c, 0x6c, 0x76, 0xda, 0xee, 0xe1, 0x21, 0x3e, 0x74, 0x1f, 0x3e, 0x6c, 0x76, 0xf6, 0xdd, 0x87, 0x0f, 0x5f, 0x1e, - 0x1e, 0xb9, 0xfb, 0xf0, 0x6e, 0x7f, 0x3f, 0xd8, 0x77, 0x3b, 0x9d, 0x26, 0xfc, 0xc1, 0x47, 0x6e, 0x57, 0xfe, 0xe8, - 0x74, 0xdc, 0xfd, 0x0e, 0x6e, 0xc7, 0x87, 0x5d, 0xf7, 0xe1, 0x63, 0x2c, 0xfe, 0x8a, 0x6a, 0x58, 0xfc, 0x81, 0x6e, - 0xf0, 0x63, 0xb7, 0xfb, 0x50, 0xfe, 0x12, 0x1d, 0x5e, 0x1d, 0x1c, 0xfd, 0x68, 0xb7, 0xb6, 0xce, 0xa1, 0x23, 0xe7, - 0x70, 0x74, 0xe8, 0xee, 0xef, 0xe3, 0x83, 0x8e, 0x7b, 0xb4, 0x1f, 0x35, 0x0f, 0xba, 0xee, 0xc3, 0x47, 0x41, 0xb3, - 0xe3, 0x3e, 0x7a, 0x84, 0xdb, 0xcd, 0x7d, 0xb7, 0x8b, 0x3b, 0xee, 0xc1, 0xbe, 0xf8, 0xb1, 0xef, 0x76, 0xaf, 0x1e, - 0x3d, 0x76, 0x1f, 0x1e, 0x46, 0x0f, 0xdd, 0x83, 0x6f, 0x0f, 0x8e, 0xdc, 0xee, 0x7e, 0xb4, 0xff, 0xd0, 0xed, 0x3e, - 0xba, 0x7a, 0xe8, 0x1e, 0x44, 0xcd, 0xee, 0xc3, 0x3b, 0x5b, 0x76, 0xba, 0x2e, 0xe0, 0x48, 0xbc, 0x86, 0x17, 0x58, - 0xbd, 0x80, 0xff, 0x23, 0xd1, 0xf6, 0xdf, 0xb0, 0x9b, 0x7c, 0xbd, 0xe9, 0x63, 0xf7, 0xe8, 0x51, 0x20, 0xab, 0x43, - 0x41, 0x53, 0xd7, 0x80, 0x26, 0x57, 0x4d, 0x39, 0xac, 0xe8, 0xae, 0xa9, 0x3b, 0xd2, 0xff, 0xab, 0xc1, 0xae, 0x9a, - 0x30, 0xb0, 0x1c, 0xf7, 0xdf, 0xb5, 0x9f, 0x72, 0xc9, 0x8f, 0x5b, 0x13, 0x49, 0xfa, 0x93, 0xfe, 0x17, 0xf2, 0x73, - 0x3e, 0x5f, 0x5c, 0x60, 0x7f, 0x9b, 0xe3, 0x23, 0xfe, 0xb4, 0xe3, 0x23, 0xa2, 0xf7, 0xf1, 0x7c, 0xc4, 0x7f, 0xb8, - 0xe7, 0xc3, 0x5f, 0x75, 0x9b, 0xdf, 0xf0, 0x35, 0x07, 0xc7, 0xaa, 0x55, 0xfc, 0x82, 0x3b, 0xc3, 0x14, 0x3e, 0x1d, - 0x5d, 0xf4, 0x6e, 0x38, 0x89, 0xa8, 0xe9, 0x07, 0x4a, 0x81, 0xc5, 0xde, 0x70, 0xc9, 0x63, 0x83, 0x6d, 0x08, 0x09, - 0x3f, 0x8d, 0x90, 0xef, 0xee, 0x83, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, 0x60, 0xe2, 0xa3, 0xe6, 0xc9, 0x17, 0x9e, 0x06, - 0xe1, 0x29, 0x38, 0x13, 0xcf, 0x0e, 0xdc, 0x9a, 0xd1, 0xb0, 0x5b, 0xf4, 0x4a, 0x44, 0xee, 0x64, 0x70, 0xfd, 0xf9, - 0xe7, 0x04, 0x1d, 0xe4, 0x15, 0x39, 0xc4, 0x56, 0x6e, 0x99, 0x99, 0x90, 0x3a, 0xea, 0xa1, 0x14, 0x4a, 0x5d, 0xb7, - 0xed, 0xb6, 0x4b, 0x97, 0x0e, 0x5c, 0x8b, 0x44, 0x16, 0x29, 0xf7, 0xbd, 0x9d, 0x0e, 0x8e, 0xd3, 0x09, 0x5c, 0x96, - 0x24, 0x3e, 0x1f, 0x07, 0x27, 0x1e, 0x02, 0xf9, 0xe5, 0x3e, 0x48, 0x9f, 0x50, 0x8e, 0x1e, 0x3f, 0xfb, 0xf8, 0x37, - 0x08, 0x62, 0xea, 0x98, 0xc4, 0x14, 0xbc, 0x1d, 0xaf, 0x68, 0xc8, 0x7c, 0xc7, 0x76, 0x66, 0x19, 0x1d, 0xd3, 0x2c, - 0x6f, 0xd6, 0xee, 0xeb, 0x11, 0x57, 0xf5, 0x20, 0x5b, 0x41, 0x38, 0xce, 0xe0, 0x73, 0x48, 0x64, 0xa8, 0xfc, 0x8d, - 0xb6, 0x32, 0xc0, 0xec, 0x02, 0xeb, 0x92, 0x0c, 0x64, 0x6d, 0xa5, 0xb4, 0xd9, 0x52, 0x6b, 0xeb, 0xb8, 0xdd, 0x43, - 0x64, 0x89, 0x62, 0xf8, 0xd0, 0xcc, 0x0f, 0x4e, 0x73, 0xbf, 0xfd, 0x27, 0x64, 0x34, 0x2b, 0x3b, 0x1a, 0x29, 0x77, - 0x5b, 0x52, 0x7e, 0x8e, 0x70, 0x25, 0xec, 0x6a, 0x4b, 0x8a, 0xf8, 0x52, 0xce, 0xdd, 0x46, 0xbd, 0x44, 0x25, 0xcd, - 0xc9, 0x2b, 0x01, 0xc7, 0x6c, 0xe2, 0x18, 0xd7, 0x4d, 0x24, 0xf2, 0x43, 0x36, 0x70, 0x5b, 0x3d, 0x42, 0x45, 0x55, - 0x25, 0x41, 0x0b, 0x11, 0x6d, 0x61, 0x89, 0x95, 0x2c, 0x97, 0x4e, 0x02, 0x2e, 0x72, 0x62, 0xe0, 0x14, 0x9e, 0x51, - 0x0d, 0xc9, 0x09, 0x2e, 0x01, 0x12, 0x08, 0x26, 0x89, 0xfc, 0xb7, 0x2a, 0xd6, 0x3f, 0x94, 0xe3, 0xcb, 0x8d, 0xfd, - 0x64, 0x02, 0x54, 0xe8, 0x27, 0x93, 0x35, 0xb7, 0x9a, 0x0c, 0x18, 0xad, 0x94, 0x56, 0x5d, 0x55, 0xee, 0xb3, 0xfc, - 0xc9, 0xed, 0x7b, 0x75, 0xe3, 0xb5, 0x0d, 0xde, 0x69, 0x11, 0xdf, 0xa8, 0xbe, 0xce, 0xd3, 0x20, 0x0f, 0x8e, 0xa7, - 0x94, 0xfb, 0xf2, 0xb0, 0x1a, 0xe8, 0x13, 0x90, 0xcb, 0x62, 0x29, 0x6b, 0x54, 0x05, 0xf5, 0x89, 0x3c, 0xcc, 0x2f, - 0x45, 0x3d, 0xb6, 0xd4, 0x55, 0x71, 0x4d, 0xb1, 0x34, 0xa4, 0x83, 0xa5, 0x3f, 0x26, 0xf0, 0xc5, 0x71, 0x64, 0x92, - 0xa4, 0x76, 0xff, 0x41, 0x99, 0xeb, 0xb2, 0x6d, 0x11, 0x62, 0x96, 0x7c, 0x1c, 0x66, 0x34, 0xfe, 0x27, 0xf2, 0x80, - 0x05, 0x69, 0xf2, 0x60, 0x64, 0xa3, 0x1e, 0x77, 0xa3, 0x8c, 0x8e, 0xc9, 0x03, 0x90, 0xf1, 0x9e, 0xb0, 0x3e, 0x80, - 0x11, 0x36, 0x6e, 0xa6, 0x31, 0x16, 0x1a, 0xd3, 0x3d, 0x14, 0x22, 0x09, 0xae, 0xdd, 0x3d, 0xb4, 0x2d, 0x69, 0x13, - 0x8b, 0xdf, 0x7d, 0x29, 0x4e, 0x85, 0x12, 0x60, 0x75, 0xba, 0xee, 0x61, 0xd4, 0x75, 0x1f, 0x5f, 0x3d, 0x72, 0x8f, - 0xa2, 0xce, 0xa3, 0xab, 0x26, 0xfc, 0xdb, 0x75, 0x1f, 0xc7, 0xcd, 0xae, 0xfb, 0x18, 0xfe, 0xff, 0xf6, 0xc0, 0x3d, - 0x8c, 0x9a, 0x1d, 0xf7, 0xe8, 0x6a, 0xdf, 0xdd, 0x7f, 0xd9, 0xe9, 0xba, 0xfb, 0x56, 0xc7, 0x92, 0xed, 0x80, 0x5d, - 0x4b, 0xee, 0xfc, 0x60, 0x65, 0x43, 0x6c, 0x08, 0xc6, 0xc9, 0x03, 0x77, 0x36, 0x16, 0x67, 0xa4, 0xcd, 0xfd, 0xa9, - 0x9c, 0x75, 0x4f, 0xfd, 0x0c, 0xbe, 0x6c, 0x5a, 0xdf, 0xbb, 0xb5, 0x77, 0xb8, 0xc6, 0x2f, 0x36, 0x0c, 0x31, 0x13, - 0x11, 0x70, 0xf3, 0xae, 0x35, 0x2a, 0xee, 0xb0, 0x93, 0xdf, 0x82, 0x52, 0x51, 0xb0, 0x32, 0xbb, 0xc8, 0x20, 0x6b, - 0x59, 0x03, 0x12, 0x80, 0x04, 0x0d, 0xae, 0xe6, 0x8f, 0x56, 0x74, 0x9e, 0xc1, 0xfd, 0x04, 0x9a, 0x97, 0x30, 0xf1, - 0x45, 0x3e, 0x01, 0xc3, 0x8b, 0xb0, 0x58, 0x05, 0x0f, 0x8e, 0x05, 0x66, 0xa9, 0x71, 0x1b, 0x1d, 0xad, 0x72, 0x00, - 0x42, 0x06, 0xf7, 0x07, 0x16, 0x85, 0x9e, 0x59, 0xcd, 0x8b, 0x5b, 0x21, 0x51, 0xb0, 0x13, 0x9a, 0x0f, 0x6c, 0x28, - 0xb2, 0x3d, 0x5b, 0x78, 0x00, 0xed, 0xf2, 0xe3, 0xaf, 0x25, 0xdd, 0x57, 0x05, 0x58, 0x5c, 0x0e, 0x01, 0x9b, 0x1a, - 0xd0, 0x67, 0xa3, 0xbd, 0xbd, 0xad, 0xdb, 0x49, 0xe8, 0x97, 0x30, 0xb5, 0xea, 0x9b, 0x91, 0x26, 0xa7, 0xb2, 0xcd, - 0x75, 0x28, 0xfb, 0x15, 0x18, 0x46, 0x0a, 0x2d, 0x97, 0xd4, 0xe7, 0xae, 0x9f, 0xc8, 0x03, 0x06, 0x06, 0x3f, 0xc3, - 0x1d, 0xba, 0x8f, 0x8a, 0x94, 0xfb, 0x32, 0x67, 0xcc, 0x64, 0x03, 0x29, 0xf7, 0xf5, 0xdd, 0x4a, 0x3e, 0xaf, 0x9d, - 0xab, 0x8f, 0xba, 0xfd, 0x37, 0xef, 0x4f, 0x2c, 0xb9, 0x7b, 0x8f, 0x5b, 0x51, 0xb7, 0x7f, 0x2c, 0x5c, 0x2a, 0x32, - 0x2b, 0x80, 0xc8, 0xac, 0x00, 0x4b, 0x5d, 0x2a, 0x03, 0x81, 0xb6, 0xa2, 0x25, 0xa7, 0x2d, 0x4c, 0x0a, 0xe9, 0x0c, - 0x9e, 0xce, 0x63, 0xce, 0xe0, 0x9b, 0x47, 0x2d, 0x91, 0x12, 0x20, 0x52, 0x0c, 0xf4, 0x19, 0x55, 0xa5, 0x3c, 0x5e, - 0xf2, 0x44, 0xbb, 0x8e, 0xc7, 0x2c, 0xa6, 0xfa, 0x54, 0xaa, 0xea, 0xaa, 0xcc, 0x07, 0x5a, 0xaf, 0x9d, 0xcf, 0x2f, - 0x21, 0x27, 0x42, 0x67, 0x1f, 0x7d, 0x50, 0x0d, 0x8e, 0xc5, 0x50, 0x10, 0xd8, 0x97, 0x52, 0x5c, 0x7f, 0xdd, 0xb5, - 0xbe, 0xa4, 0x6a, 0xf6, 0x4a, 0x80, 0xc0, 0x4d, 0x1e, 0xd1, 0x7e, 0xbf, 0xf4, 0x26, 0x9b, 0xef, 0x8a, 0xe3, 0x56, - 0xb4, 0xdf, 0xbf, 0xf0, 0x26, 0xaa, 0xbf, 0x97, 0xe9, 0x64, 0x73, 0x5f, 0x71, 0x3a, 0x19, 0x88, 0x63, 0xf2, 0xf2, - 0xca, 0x27, 0xad, 0x1b, 0xa7, 0xb1, 0xdd, 0x3f, 0x56, 0xba, 0x82, 0x25, 0xa2, 0xee, 0xf6, 0x61, 0x5b, 0x9f, 0xbc, - 0x8f, 0xd3, 0x09, 0xec, 0x57, 0xd9, 0xc4, 0x18, 0xa4, 0xe6, 0x90, 0x8f, 0x3a, 0xfd, 0x63, 0xdf, 0x12, 0xac, 0x47, - 0xf0, 0x96, 0xdc, 0x6b, 0x41, 0xe3, 0x28, 0x9d, 0x52, 0x97, 0xa5, 0xad, 0x6b, 0x7a, 0xd9, 0xf4, 0x67, 0xac, 0xf2, - 0x7e, 0x83, 0x4e, 0x52, 0x0e, 0x99, 0xae, 0x64, 0x60, 0x75, 0x2b, 0x6f, 0xdc, 0x01, 0x98, 0x44, 0xda, 0x73, 0x27, - 0x5c, 0x76, 0x06, 0x58, 0x69, 0xff, 0xb8, 0xe5, 0xaf, 0x60, 0x44, 0x6c, 0xc5, 0x42, 0xf9, 0xe1, 0xc1, 0xee, 0xb9, - 0x14, 0xe9, 0x5f, 0x52, 0x5a, 0x68, 0x7f, 0xbd, 0x92, 0xe3, 0x85, 0xdd, 0xff, 0xd7, 0xff, 0xf1, 0xbf, 0x94, 0x0b, - 0xfe, 0xb8, 0x15, 0x75, 0x74, 0x5f, 0x2b, 0xab, 0x52, 0x1c, 0xc3, 0x3d, 0x36, 0x55, 0xcc, 0x98, 0xde, 0x34, 0x27, - 0x19, 0x0b, 0x9b, 0x91, 0x1f, 0x8f, 0xed, 0xfe, 0x76, 0x6c, 0xca, 0xf4, 0xc4, 0xa6, 0x8e, 0xb6, 0xae, 0x17, 0x01, - 0xbd, 0xfe, 0xa6, 0x4b, 0x19, 0x74, 0xc6, 0x97, 0xd8, 0xda, 0xe6, 0x15, 0x0d, 0xd5, 0xee, 0xab, 0x5d, 0xd3, 0x90, - 0xa8, 0x4f, 0x46, 0x2b, 0x06, 0x99, 0xd4, 0x6e, 0x67, 0x28, 0x6c, 0xab, 0x8c, 0x79, 0xfd, 0xdf, 0xff, 0xf9, 0x5f, - 0xfe, 0x9b, 0x7e, 0x84, 0x50, 0xd6, 0xbf, 0xfe, 0xf7, 0xff, 0xfc, 0x7f, 0xfe, 0xf7, 0x7f, 0x85, 0xf4, 0x34, 0x15, - 0xee, 0x12, 0x4c, 0xc5, 0xaa, 0x62, 0x5d, 0x92, 0xbb, 0x58, 0x70, 0xe8, 0x6d, 0xca, 0x72, 0xce, 0x82, 0xfa, 0x7d, - 0x0d, 0x67, 0x62, 0x40, 0xb1, 0x33, 0x15, 0x74, 0x62, 0x87, 0x17, 0x15, 0x41, 0xd5, 0x50, 0x2e, 0x08, 0xb7, 0x38, - 0x6e, 0x01, 0xbe, 0xef, 0x77, 0xdd, 0x8c, 0x5b, 0x2e, 0xc7, 0x42, 0x93, 0x09, 0x94, 0x14, 0x55, 0xb9, 0x05, 0xa1, - 0x97, 0x05, 0x3c, 0x7a, 0x5d, 0xa3, 0x58, 0xac, 0x5e, 0xad, 0x4d, 0xef, 0xe7, 0x79, 0xce, 0xd9, 0x18, 0x50, 0x2e, - 0xdd, 0xc8, 0x22, 0xca, 0xdd, 0x04, 0x55, 0x32, 0xbe, 0x2d, 0x44, 0x2f, 0x92, 0x40, 0x0f, 0x8e, 0xfe, 0x54, 0xfc, - 0x79, 0x0a, 0x0a, 0x9b, 0xe5, 0x4c, 0xfd, 0x1b, 0x65, 0xbd, 0x3f, 0x6c, 0xb7, 0x67, 0x37, 0x68, 0x51, 0x8d, 0x80, - 0xb7, 0x0d, 0x26, 0xe8, 0xd8, 0xec, 0x50, 0x84, 0xc7, 0x4b, 0x2f, 0x77, 0xdb, 0x02, 0x57, 0xb9, 0xd5, 0x2e, 0x8a, - 0xaf, 0x16, 0xc2, 0xd1, 0xca, 0x7e, 0x85, 0x30, 0xb6, 0xf2, 0x49, 0x5f, 0xa6, 0xe6, 0xe4, 0x16, 0x46, 0xab, 0xae, - 0x6c, 0x15, 0x75, 0xd6, 0x6f, 0x6e, 0x31, 0xc3, 0xf0, 0x66, 0x00, 0xfd, 0x00, 0x42, 0xe2, 0x51, 0x07, 0x47, 0xdd, - 0x45, 0xd9, 0x3d, 0xe7, 0xe9, 0xd4, 0x8c, 0xbb, 0x53, 0x9f, 0x06, 0x74, 0xac, 0x7d, 0xf9, 0xea, 0xbd, 0x8c, 0xa9, - 0x17, 0xd1, 0xfe, 0x86, 0xb1, 0x14, 0x48, 0x22, 0xde, 0x6e, 0xb5, 0x8b, 0x2f, 0x61, 0x07, 0x2e, 0xc6, 0x71, 0xea, - 0x73, 0x4f, 0x10, 0x6c, 0xcf, 0x8c, 0xde, 0xfb, 0xc0, 0x93, 0xd2, 0x85, 0x01, 0x4f, 0x4f, 0x56, 0x05, 0xaf, 0x7a, - 0xfd, 0x06, 0xc7, 0xc2, 0x15, 0xcd, 0xcd, 0xae, 0xa4, 0x53, 0xee, 0x3b, 0x15, 0x14, 0x7f, 0x5e, 0xf3, 0x66, 0x29, - 0x81, 0xd4, 0x45, 0x9b, 0xdf, 0x4b, 0xb1, 0x2f, 0xdf, 0x7e, 0xcf, 0x1d, 0x5b, 0x80, 0x69, 0xaf, 0xd6, 0x12, 0x85, - 0x50, 0xeb, 0x39, 0xf9, 0xae, 0xb4, 0xa8, 0xfc, 0xd9, 0x4c, 0x54, 0x44, 0xbd, 0xe3, 0x96, 0x54, 0x84, 0x81, 0x7b, - 0x88, 0x8c, 0x0f, 0x99, 0x60, 0xa1, 0x2a, 0xa9, 0xad, 0x20, 0x7f, 0xa9, 0xd4, 0x0b, 0xf8, 0x94, 0x78, 0xff, 0xff, - 0x01, 0x65, 0x21, 0x07, 0x4b, 0xe3, 0x97, 0x00, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xdb, 0x72, 0xdb, 0xc6, 0xb6, 0xe0, 0xf3, + 0xe4, 0x2b, 0x20, 0x44, 0x5b, 0x46, 0x87, 0x4d, 0xf0, 0x22, 0xc9, 0x96, 0x41, 0x35, 0xb9, 0x65, 0xd9, 0xd9, 0xf6, + 0x8e, 0x6f, 0xdb, 0xb2, 0x73, 0x53, 0xb8, 0x25, 0x08, 0x68, 0x12, 0x1d, 0x83, 0x00, 0x03, 0x34, 0x45, 0x29, 0x24, + 0x4e, 0xcd, 0x07, 0x4c, 0xd5, 0x54, 0xcd, 0xd3, 0xbc, 0x4c, 0xcd, 0x79, 0x98, 0x8f, 0x98, 0xe7, 0xf3, 0x29, 0xe7, + 0x07, 0x66, 0x3e, 0x61, 0x6a, 0xf5, 0x05, 0x68, 0xf0, 0x22, 0xcb, 0x49, 0xf6, 0x39, 0xe7, 0x61, 0x2a, 0x15, 0x99, + 0x68, 0xf4, 0x65, 0xf5, 0xea, 0xd5, 0xeb, 0xde, 0x8d, 0xe3, 0x9d, 0x30, 0x0d, 0xf8, 0xed, 0x94, 0x5a, 0x11, 0x9f, + 0xc4, 0xfd, 0x63, 0xf5, 0x97, 0xfa, 0x61, 0xff, 0x38, 0x66, 0xc9, 0x47, 0x2b, 0xa3, 0x31, 0x61, 0x41, 0x9a, 0x58, + 0x51, 0x46, 0x47, 0x24, 0xf4, 0xb9, 0xef, 0xb1, 0x89, 0x3f, 0xa6, 0x56, 0xab, 0x7f, 0x3c, 0xa1, 0xdc, 0xb7, 0x82, + 0xc8, 0xcf, 0x72, 0xca, 0xc9, 0x87, 0xf7, 0x5f, 0x37, 0x8f, 0xfa, 0xc7, 0x79, 0x90, 0xb1, 0x29, 0xb7, 0xa0, 0x4b, + 0x32, 0x49, 0xc3, 0x59, 0x4c, 0xfb, 0xad, 0xd6, 0x7c, 0x3e, 0x77, 0x7f, 0xce, 0xbf, 0x08, 0xd2, 0x24, 0xe7, 0xd6, + 0x53, 0x32, 0x67, 0x49, 0x98, 0xce, 0x71, 0xc2, 0xc9, 0x53, 0xf7, 0x2c, 0xf2, 0xc3, 0x74, 0xfe, 0x2e, 0x4d, 0xf9, + 0xde, 0x9e, 0x23, 0x1f, 0x6f, 0x4f, 0xcf, 0xce, 0x08, 0x21, 0xd7, 0x29, 0x0b, 0xad, 0xf6, 0x72, 0x59, 0x15, 0xba, + 0x89, 0xcf, 0xd9, 0x35, 0x95, 0x4d, 0xd0, 0xde, 0x9e, 0xed, 0x87, 0xe9, 0x94, 0xd3, 0xf0, 0x8c, 0xdf, 0xc6, 0xf4, + 0x2c, 0xa2, 0x94, 0xe7, 0x36, 0x4b, 0xac, 0xa7, 0x69, 0x30, 0x9b, 0xd0, 0x84, 0xbb, 0xd3, 0x2c, 0xe5, 0x29, 0x40, + 0xb2, 0xb7, 0x67, 0x67, 0x74, 0x1a, 0xfb, 0x01, 0x85, 0xf7, 0xa7, 0x67, 0x67, 0x55, 0x8b, 0xaa, 0x12, 0xce, 0x39, + 0x39, 0xbb, 0x9d, 0x5c, 0xa5, 0xb1, 0x83, 0x70, 0xc4, 0x49, 0x42, 0xe7, 0xd6, 0x77, 0xd4, 0xff, 0xf8, 0xca, 0x9f, + 0xf6, 0x82, 0xd8, 0xcf, 0x73, 0xeb, 0x84, 0x2f, 0xc4, 0x14, 0xb2, 0x59, 0xc0, 0xd3, 0xcc, 0xe1, 0x98, 0x62, 0x86, + 0x16, 0x6c, 0xe4, 0xf0, 0x88, 0xe5, 0xee, 0xc5, 0x6e, 0x90, 0xe7, 0xef, 0x68, 0x3e, 0x8b, 0xf9, 0x2e, 0xd9, 0x69, + 0x63, 0xb6, 0x43, 0x48, 0xce, 0x11, 0x8f, 0xb2, 0x74, 0x6e, 0x3d, 0xcb, 0xb2, 0x34, 0x73, 0xec, 0xd3, 0xb3, 0x33, + 0x59, 0xc3, 0x62, 0xb9, 0x95, 0xa4, 0xdc, 0x2a, 0xfb, 0xf3, 0xaf, 0x62, 0xea, 0x5a, 0x1f, 0x72, 0x6a, 0x5d, 0xce, + 0x92, 0xdc, 0x1f, 0xd1, 0xd3, 0xb3, 0xb3, 0x4b, 0x2b, 0xcd, 0xac, 0xcb, 0x20, 0xcf, 0x2f, 0x2d, 0x96, 0xe4, 0x9c, + 0xfa, 0xa1, 0x6b, 0xa3, 0x9e, 0x18, 0x2c, 0xc8, 0xf3, 0xf7, 0xf4, 0x86, 0x13, 0x8e, 0xc5, 0x23, 0x27, 0xb4, 0x18, + 0x53, 0x6e, 0xe5, 0xe5, 0xbc, 0x1c, 0xb4, 0x88, 0x29, 0xb7, 0x38, 0x11, 0xef, 0xd3, 0x9e, 0xc4, 0x3d, 0x95, 0x8f, + 0xbc, 0xc7, 0x46, 0x4e, 0xc2, 0xf7, 0xf6, 0x78, 0x89, 0x67, 0x24, 0xa7, 0x66, 0x31, 0x42, 0x77, 0x74, 0xd9, 0xde, + 0x1e, 0x75, 0x63, 0x9a, 0x8c, 0x79, 0x44, 0x08, 0xe9, 0xf4, 0xd8, 0xde, 0x9e, 0xc3, 0x49, 0xc4, 0xdd, 0x31, 0xe5, + 0x0e, 0x45, 0x08, 0x57, 0xad, 0xf7, 0xf6, 0x1c, 0x89, 0x84, 0x94, 0x48, 0xc4, 0xd5, 0x70, 0x8c, 0x5c, 0x85, 0xfd, + 0xb3, 0xdb, 0x24, 0x70, 0x4c, 0xf8, 0x11, 0x66, 0x7b, 0x7b, 0x11, 0x77, 0x73, 0xe8, 0x11, 0x73, 0x84, 0x8a, 0x8c, + 0xf2, 0x59, 0x96, 0x58, 0xbc, 0xe0, 0xe9, 0x19, 0xcf, 0x58, 0x32, 0x76, 0xd0, 0x42, 0x97, 0x19, 0x0d, 0x8b, 0x42, + 0x82, 0xfb, 0x8e, 0x93, 0x9c, 0xf4, 0x61, 0xc4, 0x13, 0xee, 0xc0, 0x2a, 0xa6, 0x23, 0x2b, 0x27, 0xc4, 0xce, 0x45, + 0x5b, 0x7b, 0x90, 0x7b, 0x79, 0xc3, 0xb6, 0xb1, 0x84, 0x12, 0xe7, 0x1c, 0xe1, 0x8f, 0xc4, 0xc9, 0xb1, 0xeb, 0xba, + 0x1c, 0x91, 0xfe, 0x42, 0x63, 0x25, 0x37, 0xe6, 0x39, 0xc8, 0xcf, 0xdb, 0x43, 0x8f, 0xbb, 0x19, 0x0d, 0x67, 0x01, + 0x75, 0x1c, 0x86, 0x13, 0x9c, 0x21, 0xd2, 0x67, 0x0d, 0x27, 0x25, 0x7d, 0x58, 0xee, 0xb4, 0xbe, 0xd6, 0x84, 0xec, + 0xb4, 0x91, 0x82, 0x31, 0xd5, 0x00, 0x02, 0x86, 0x15, 0x3c, 0x29, 0x21, 0x76, 0x32, 0x9b, 0x5c, 0xd1, 0xcc, 0x2e, + 0xab, 0xf5, 0x6a, 0x64, 0x31, 0xcb, 0xa9, 0x15, 0xe4, 0xb9, 0x35, 0x9a, 0x25, 0x01, 0x67, 0x69, 0x62, 0xd9, 0x8d, + 0xb4, 0x61, 0x4b, 0x72, 0x28, 0xa9, 0xc1, 0x46, 0x05, 0x72, 0x12, 0xd4, 0xc8, 0xcf, 0xb3, 0x46, 0x67, 0x88, 0x01, + 0x4a, 0xd4, 0x53, 0xfd, 0x29, 0x04, 0x50, 0x9c, 0xc3, 0x1c, 0x0b, 0xfc, 0x84, 0xc3, 0x2c, 0xc5, 0x14, 0x13, 0x3e, + 0xc8, 0xdd, 0xf5, 0x8d, 0x42, 0xb8, 0x3b, 0xf1, 0xa7, 0x0e, 0x25, 0x7d, 0x2a, 0x88, 0xcb, 0x4f, 0x02, 0x80, 0xb5, + 0xb6, 0x6e, 0x03, 0xea, 0x51, 0xb7, 0x22, 0x29, 0xe4, 0x71, 0x77, 0x94, 0x66, 0xcf, 0xfc, 0x20, 0x82, 0x76, 0x25, + 0xc1, 0x84, 0x7a, 0xbf, 0x05, 0x19, 0xf5, 0x39, 0x7d, 0x16, 0x53, 0x78, 0x72, 0x6c, 0xd1, 0xd2, 0x46, 0x38, 0x21, + 0x4f, 0xdd, 0x98, 0xf1, 0xd7, 0x69, 0x12, 0xd0, 0x5e, 0x62, 0x50, 0x17, 0x83, 0x75, 0x3f, 0xe1, 0x3c, 0x63, 0x57, + 0x33, 0x4e, 0x1d, 0x3b, 0x81, 0x1a, 0x36, 0x4e, 0x10, 0x66, 0x2e, 0xa7, 0x37, 0xfc, 0x34, 0x4d, 0x38, 0x4d, 0x38, + 0xa1, 0x1a, 0xa9, 0x38, 0x77, 0xfd, 0xe9, 0x94, 0x26, 0xe1, 0x69, 0xc4, 0xe2, 0xd0, 0x61, 0xa8, 0x40, 0x05, 0x0e, + 0x38, 0x81, 0x39, 0x92, 0x7e, 0xee, 0xc1, 0x9f, 0xed, 0xb3, 0x71, 0x38, 0xe9, 0x8b, 0x4d, 0x41, 0x89, 0x6d, 0xf7, + 0x46, 0x69, 0xe6, 0xa8, 0x19, 0x58, 0xe9, 0xc8, 0xe2, 0x30, 0xc6, 0xbb, 0x59, 0x4c, 0x73, 0x44, 0x1b, 0x84, 0x95, + 0xcb, 0xa8, 0x10, 0xfc, 0x0e, 0x28, 0xbe, 0x40, 0x4e, 0x8e, 0xbc, 0xbc, 0x77, 0xed, 0x67, 0xd6, 0x8f, 0x6a, 0x47, + 0xfd, 0xac, 0xb9, 0x59, 0xc8, 0xc9, 0xcf, 0x2e, 0xcf, 0x66, 0x39, 0xa7, 0xe1, 0xfb, 0xdb, 0x29, 0xcd, 0xf1, 0x73, + 0x4e, 0x42, 0x3e, 0x08, 0xb9, 0x4b, 0x27, 0x53, 0x7e, 0x7b, 0x26, 0x18, 0xa3, 0x67, 0xdb, 0x78, 0x06, 0x35, 0x33, + 0xea, 0x07, 0xc0, 0xcc, 0x14, 0xb6, 0xde, 0xa6, 0xf1, 0xed, 0x88, 0xc5, 0xf1, 0xd9, 0x6c, 0x3a, 0x4d, 0x33, 0x8e, + 0x39, 0x27, 0x0b, 0x9e, 0x56, 0xb8, 0x81, 0xc5, 0x5c, 0xe4, 0x73, 0xc6, 0x83, 0xc8, 0xe1, 0x68, 0x11, 0xf8, 0x39, + 0xb5, 0x9e, 0xa4, 0x69, 0x4c, 0xfd, 0xc4, 0xcb, 0x49, 0x3e, 0x78, 0xce, 0xbd, 0x64, 0x16, 0xc7, 0xbd, 0xab, 0x8c, + 0xfa, 0x1f, 0x7b, 0xe2, 0xf5, 0x9b, 0xab, 0x9f, 0x69, 0xc0, 0x3d, 0xf1, 0xfb, 0x24, 0xcb, 0xfc, 0x5b, 0xa8, 0x48, + 0x08, 0x54, 0x1b, 0xe4, 0xde, 0x5f, 0xcf, 0xde, 0xbc, 0x76, 0xe5, 0x2e, 0x61, 0xa3, 0x5b, 0x27, 0x2f, 0x77, 0x5e, + 0x5e, 0xe0, 0x51, 0x96, 0x4e, 0x56, 0x86, 0x96, 0x68, 0xcb, 0x7b, 0x5b, 0x40, 0xa0, 0x24, 0xdf, 0x91, 0x5d, 0x9b, + 0x10, 0xbc, 0x16, 0x44, 0x0f, 0x2f, 0x89, 0x1a, 0x17, 0xfe, 0x78, 0xb2, 0xd8, 0xc9, 0xd1, 0xdd, 0xd0, 0xf2, 0xec, + 0x76, 0x41, 0x89, 0x80, 0x73, 0x0a, 0x22, 0x06, 0x60, 0x0c, 0x7c, 0x1e, 0x44, 0x0b, 0x2a, 0x3a, 0x2b, 0x34, 0xc4, + 0xb4, 0x28, 0xf0, 0xb3, 0x92, 0xe0, 0x39, 0xb0, 0x5d, 0xc1, 0xa9, 0x08, 0x5f, 0x2e, 0x73, 0x42, 0x72, 0x84, 0xff, + 0x4a, 0x16, 0xbe, 0x9e, 0x8f, 0xb7, 0xd3, 0xc6, 0xb0, 0x31, 0x3d, 0xc9, 0x5e, 0x70, 0x90, 0x26, 0xd7, 0x34, 0xe3, + 0x34, 0xf3, 0x38, 0xc7, 0x19, 0x1d, 0xc5, 0x00, 0xc6, 0x4e, 0x07, 0x47, 0x7e, 0x7e, 0x1a, 0xf9, 0xc9, 0x98, 0x86, + 0xde, 0x33, 0x5e, 0x60, 0xca, 0x89, 0x3d, 0x62, 0x89, 0x1f, 0xb3, 0x5f, 0x69, 0x68, 0x2b, 0x81, 0xf0, 0xcc, 0xa2, + 0x37, 0x9c, 0x26, 0x61, 0x6e, 0x3d, 0x7f, 0xff, 0xea, 0xa5, 0x5a, 0xca, 0x9a, 0x8c, 0x40, 0x8b, 0x7c, 0x36, 0xa5, + 0x99, 0x83, 0xb0, 0x92, 0x11, 0xcf, 0x98, 0xe0, 0x8f, 0xaf, 0xfc, 0xa9, 0x2c, 0x61, 0xf9, 0x87, 0x69, 0xe8, 0x73, + 0xfa, 0x96, 0x26, 0x21, 0x4b, 0xc6, 0x64, 0xa7, 0x23, 0xcb, 0x23, 0x5f, 0xbd, 0x08, 0xcb, 0xa2, 0x8b, 0xdd, 0x67, + 0xb1, 0x98, 0x79, 0xf9, 0x38, 0x73, 0x50, 0x91, 0x73, 0x9f, 0xb3, 0xc0, 0xf2, 0xc3, 0xf0, 0x45, 0xc2, 0x38, 0x13, + 0x00, 0x66, 0xb0, 0x40, 0x40, 0xa5, 0x54, 0x4a, 0x0b, 0x0d, 0xb8, 0x83, 0xb0, 0xe3, 0x28, 0x19, 0x10, 0x21, 0xb5, + 0x62, 0x7b, 0x7b, 0x15, 0xc7, 0x1f, 0x50, 0x4f, 0xbe, 0x24, 0xe7, 0x43, 0xe4, 0x4e, 0x67, 0x39, 0x2c, 0xb5, 0x1e, + 0x02, 0x04, 0x4c, 0x7a, 0x95, 0xd3, 0xec, 0x9a, 0x86, 0x25, 0x79, 0xe4, 0x0e, 0x5a, 0xac, 0x8c, 0xa1, 0x76, 0x06, + 0x27, 0xe7, 0xc3, 0x9e, 0xc9, 0xba, 0xa9, 0x22, 0xf5, 0x2c, 0x9d, 0xd2, 0x8c, 0x33, 0x9a, 0x97, 0xdc, 0xc4, 0x01, + 0x41, 0x5a, 0x72, 0x94, 0x84, 0xe8, 0xf9, 0x4d, 0x1d, 0x86, 0x29, 0xaa, 0xf1, 0x0c, 0x2d, 0x6b, 0x9f, 0x5d, 0x0b, + 0xa1, 0x91, 0x60, 0x86, 0x30, 0x97, 0x90, 0x26, 0x08, 0x15, 0x08, 0x73, 0x0d, 0xae, 0xe4, 0x46, 0x6a, 0xb4, 0x5b, + 0x90, 0xd6, 0xe4, 0xaf, 0x42, 0x5a, 0x03, 0x4f, 0xf3, 0x39, 0xdd, 0xdb, 0x73, 0xa8, 0x5b, 0x92, 0x05, 0xd9, 0xe9, + 0xa8, 0x35, 0x32, 0x90, 0xb5, 0x05, 0x6c, 0x18, 0x98, 0x63, 0x8a, 0xf0, 0x0e, 0x75, 0x93, 0xf4, 0x24, 0x08, 0x68, + 0x9e, 0xa7, 0xd9, 0xde, 0xde, 0x8e, 0xa8, 0x5f, 0x2a, 0x14, 0xb0, 0x86, 0x6f, 0xe6, 0x49, 0x05, 0x01, 0xaa, 0x84, + 0xac, 0x12, 0x0d, 0x1c, 0x44, 0x95, 0xd0, 0x39, 0xec, 0x81, 0xd6, 0x3d, 0x3c, 0xfb, 0xe2, 0xc2, 0x6e, 0x70, 0xac, + 0xd0, 0x30, 0xa6, 0x7a, 0xe8, 0xdb, 0xa7, 0x54, 0x6a, 0x57, 0x42, 0xf7, 0x58, 0xc3, 0x8c, 0xdc, 0x41, 0x6e, 0x48, + 0x47, 0x2c, 0x31, 0xa6, 0x5d, 0x03, 0x09, 0x73, 0x9c, 0xa0, 0xc2, 0x58, 0xd0, 0x8d, 0x5d, 0x0b, 0xb5, 0x46, 0xae, + 0xdc, 0x62, 0x2c, 0x54, 0x09, 0x63, 0x19, 0xcf, 0xe9, 0xb0, 0xc0, 0x02, 0xf5, 0x7a, 0x36, 0x99, 0x00, 0xf4, 0x9c, + 0x0f, 0x7b, 0xea, 0x3d, 0x49, 0x24, 0xe6, 0x32, 0xfa, 0xcb, 0x8c, 0xe6, 0x5c, 0xd2, 0xb1, 0xc3, 0x71, 0x86, 0x19, + 0xf0, 0xeb, 0x34, 0x19, 0xb1, 0xf1, 0x2c, 0x03, 0x8d, 0x07, 0x36, 0x23, 0x4d, 0x66, 0x13, 0xaa, 0x9f, 0x36, 0xc1, + 0xf6, 0x66, 0x0a, 0x32, 0x31, 0x07, 0x9a, 0xbe, 0x9b, 0x9c, 0x00, 0x56, 0x8e, 0x96, 0xcb, 0xbf, 0xea, 0x4e, 0xaa, + 0xa5, 0x2c, 0xb5, 0xb4, 0x95, 0x35, 0xa1, 0x1c, 0x29, 0x99, 0xbc, 0xd3, 0x51, 0xe0, 0xf3, 0x21, 0xd9, 0x69, 0x97, + 0x34, 0xac, 0xb0, 0x2a, 0xc1, 0x91, 0x48, 0x7c, 0x23, 0xbb, 0x42, 0x42, 0xc4, 0xd7, 0xc8, 0xc5, 0x8d, 0xd6, 0x28, + 0x35, 0x22, 0xe7, 0xa0, 0x6c, 0xb8, 0xd1, 0x70, 0x1b, 0x39, 0x69, 0x7e, 0xe0, 0xf0, 0xf5, 0x77, 0x15, 0xdb, 0xb8, + 0xae, 0xb3, 0x8d, 0x95, 0x69, 0xd8, 0xd3, 0xb2, 0x89, 0x5d, 0x52, 0x99, 0xda, 0xe8, 0xd5, 0x2b, 0xcc, 0x04, 0x30, + 0xd5, 0x94, 0x8c, 0x2e, 0x5e, 0xfb, 0x13, 0x9a, 0x3b, 0x14, 0xe1, 0x6d, 0x15, 0x24, 0x79, 0x42, 0x95, 0xa1, 0x21, + 0x3b, 0x13, 0x90, 0x9d, 0x0c, 0x49, 0xd5, 0xac, 0xbe, 0xe1, 0x12, 0x4c, 0xcf, 0x93, 0x61, 0xa5, 0xd1, 0x19, 0x93, + 0x17, 0x42, 0x39, 0x27, 0xb5, 0xed, 0x26, 0xcb, 0x24, 0xd2, 0x84, 0xe6, 0x90, 0x23, 0xbc, 0xd3, 0x5e, 0x5d, 0x49, + 0x5d, 0xab, 0x9a, 0xe3, 0xf9, 0x10, 0xd6, 0x41, 0x88, 0x0c, 0x97, 0xe5, 0xe2, 0xdf, 0xda, 0x4e, 0x03, 0xb4, 0x9d, + 0x01, 0x61, 0xb8, 0xa3, 0xd8, 0xe7, 0x4e, 0xa7, 0xd5, 0x06, 0x75, 0xf4, 0x9a, 0x82, 0x44, 0x41, 0x68, 0x7d, 0x2a, + 0xd4, 0x9d, 0x25, 0x79, 0xc4, 0x46, 0xdc, 0x09, 0xb8, 0x60, 0x29, 0x34, 0xce, 0xa9, 0xc5, 0x6b, 0x4a, 0xb1, 0x60, + 0x37, 0x01, 0x10, 0x5b, 0xa9, 0x81, 0x51, 0x0d, 0xa9, 0x60, 0x5b, 0xc0, 0x1d, 0x2a, 0x85, 0xba, 0xe2, 0x32, 0xba, + 0x36, 0x03, 0xa5, 0xb1, 0x33, 0x90, 0x3d, 0x7a, 0x8a, 0x19, 0x30, 0x43, 0x6f, 0x65, 0x9e, 0xc9, 0x21, 0x54, 0x21, + 0x77, 0x79, 0xfa, 0x32, 0x9d, 0xd3, 0xec, 0xd4, 0x07, 0xe0, 0x3d, 0xd9, 0xbc, 0x90, 0x82, 0x40, 0xf0, 0x7b, 0xde, + 0xd3, 0xf4, 0x72, 0x21, 0x26, 0xfe, 0x36, 0x4b, 0x27, 0x2c, 0xa7, 0xa0, 0xae, 0x49, 0xfc, 0x27, 0xb0, 0xcf, 0xc4, + 0x86, 0x04, 0x61, 0x43, 0x4b, 0xfa, 0x3a, 0x79, 0x59, 0xa7, 0xaf, 0x8b, 0xdd, 0x67, 0x63, 0xcd, 0x00, 0xeb, 0xdb, + 0x18, 0x61, 0x47, 0x19, 0x15, 0x86, 0x9c, 0x73, 0x23, 0xa4, 0x44, 0xfc, 0x72, 0xc9, 0x0d, 0xdb, 0xad, 0xa6, 0x30, + 0x52, 0xb9, 0x6d, 0x50, 0xe1, 0x87, 0x21, 0xa8, 0x76, 0x59, 0x1a, 0xc7, 0x86, 0xa8, 0xc2, 0xac, 0x57, 0x0a, 0xa7, + 0x8b, 0xdd, 0x67, 0x67, 0x77, 0xc9, 0x27, 0x78, 0x6f, 0x8a, 0x28, 0x0d, 0x68, 0x12, 0xd2, 0x0c, 0x6c, 0x49, 0x63, + 0xb5, 0x94, 0x94, 0x3d, 0x4d, 0x93, 0x84, 0x06, 0x9c, 0x86, 0x60, 0xaa, 0x30, 0xc2, 0xdd, 0x28, 0xcd, 0x79, 0x59, + 0x58, 0x41, 0xcf, 0x0c, 0xe8, 0x99, 0x1b, 0xf8, 0x71, 0xec, 0x48, 0xb3, 0x64, 0x92, 0x5e, 0xd3, 0x0d, 0x50, 0xf7, + 0x6a, 0x20, 0x97, 0xdd, 0x50, 0xa3, 0x1b, 0xea, 0xe6, 0xd3, 0x98, 0x05, 0xb4, 0x14, 0x5d, 0x67, 0x2e, 0x4b, 0x42, + 0x7a, 0x03, 0x7c, 0x04, 0xf5, 0xfb, 0xfd, 0x36, 0xee, 0xa0, 0x42, 0x22, 0x7c, 0xb1, 0x86, 0xd8, 0x3b, 0x84, 0x26, + 0x10, 0x19, 0xe9, 0x2f, 0x36, 0xb2, 0x35, 0x64, 0x48, 0x4a, 0xa6, 0xcd, 0x2b, 0xc9, 0x9d, 0x11, 0x0e, 0x69, 0x4c, + 0x39, 0xd5, 0xdc, 0x1c, 0x94, 0x68, 0xb9, 0x75, 0xdf, 0x95, 0xf8, 0x2b, 0xc9, 0x49, 0xef, 0x32, 0xbd, 0xe6, 0x79, + 0x69, 0xae, 0x57, 0xcb, 0x53, 0x61, 0x7b, 0xc0, 0xe5, 0xf2, 0xf8, 0x9c, 0xfb, 0x41, 0x24, 0xed, 0x74, 0x67, 0x6d, + 0x4a, 0x55, 0x1f, 0x8a, 0xb3, 0x97, 0x9b, 0xe8, 0x89, 0x06, 0x73, 0x13, 0x0a, 0xce, 0x14, 0x53, 0xa0, 0x60, 0xfa, + 0xc9, 0x65, 0x3b, 0xf5, 0xe3, 0xf8, 0xca, 0x0f, 0x3e, 0xd6, 0xa9, 0xbf, 0x22, 0x03, 0xb2, 0xca, 0x8d, 0x8d, 0x57, + 0x06, 0xcb, 0x32, 0xe7, 0xad, 0xb9, 0x74, 0x6d, 0xa3, 0x38, 0x3b, 0xed, 0x8a, 0xec, 0xeb, 0x0b, 0xbd, 0x95, 0xda, + 0x05, 0x44, 0x4c, 0xcd, 0xcc, 0x01, 0x2e, 0xf0, 0x49, 0x8a, 0xd3, 0xfc, 0x40, 0xd1, 0x1d, 0x18, 0x1c, 0xc5, 0x0a, + 0x20, 0x1c, 0x2d, 0x8a, 0x90, 0xe5, 0xdb, 0x31, 0xf0, 0x87, 0x40, 0xf9, 0xd4, 0x18, 0xe1, 0xbe, 0x80, 0x96, 0x3c, + 0x4e, 0x69, 0xcd, 0x25, 0x64, 0x4a, 0x9f, 0xd0, 0x8c, 0xe6, 0x1b, 0xd0, 0x5d, 0x04, 0xbd, 0xbf, 0x91, 0xaf, 0x40, + 0x2b, 0x03, 0x28, 0x92, 0x9e, 0xa9, 0x4e, 0xd4, 0x28, 0x40, 0xf1, 0x54, 0x26, 0x44, 0x6e, 0x56, 0xb3, 0x20, 0x95, + 0xc6, 0x2e, 0x8d, 0x70, 0xc5, 0x72, 0x53, 0xe2, 0x38, 0x4e, 0x02, 0x46, 0x9c, 0xd6, 0xed, 0xab, 0x49, 0x24, 0x6b, + 0x93, 0x48, 0x5c, 0xc3, 0xd0, 0x42, 0x15, 0x2d, 0x1b, 0xcd, 0x3d, 0xce, 0x91, 0x59, 0x0b, 0xf4, 0x55, 0x17, 0x18, + 0x34, 0x2a, 0xf9, 0x6d, 0x4c, 0x38, 0x4e, 0x95, 0x95, 0xa3, 0x48, 0x0d, 0x38, 0x46, 0xd5, 0x24, 0x43, 0x72, 0x6f, + 0xd4, 0x4c, 0xde, 0x0c, 0xa7, 0x68, 0x45, 0xb9, 0x2f, 0x0a, 0x85, 0x24, 0x8a, 0xd4, 0xe2, 0xd4, 0xb4, 0x62, 0x03, + 0x2d, 0x38, 0x23, 0x89, 0xd4, 0x84, 0xa5, 0xe2, 0xb3, 0x8a, 0x9c, 0xb2, 0xdf, 0x1d, 0x42, 0xb2, 0x0a, 0x37, 0x89, + 0xbb, 0x41, 0xb7, 0xca, 0x10, 0x8e, 0xb4, 0x52, 0x9a, 0x56, 0x13, 0x27, 0xc4, 0xd6, 0x3e, 0x09, 0x7b, 0xb0, 0xa8, + 0xd9, 0x85, 0x9e, 0x51, 0xad, 0xf0, 0x80, 0xa7, 0xa6, 0x9b, 0xf0, 0xbd, 0x89, 0x68, 0x6a, 0xfd, 0x18, 0x18, 0x4f, + 0x6b, 0x18, 0x37, 0x50, 0x9b, 0x49, 0xde, 0x95, 0x0d, 0x49, 0x54, 0x6f, 0xec, 0x50, 0x9c, 0xca, 0x85, 0x58, 0xc3, + 0xe2, 0xaa, 0xf2, 0x29, 0x88, 0x10, 0xcc, 0xd8, 0x04, 0xd4, 0x3b, 0x53, 0x42, 0x38, 0x00, 0x3c, 0x5b, 0x2e, 0xd7, + 0xc8, 0x6e, 0xa3, 0x0e, 0x8a, 0xdc, 0xca, 0x32, 0x5c, 0x2e, 0x9f, 0x71, 0xe4, 0x28, 0xed, 0x17, 0x53, 0x34, 0xd0, + 0x3c, 0xf7, 0xe4, 0x25, 0xd4, 0x12, 0xca, 0x68, 0x55, 0x52, 0x9a, 0x0d, 0x75, 0xaa, 0xad, 0x2f, 0x14, 0x37, 0x18, + 0xf7, 0xe9, 0x1a, 0xff, 0x12, 0x85, 0x4a, 0x50, 0x57, 0x53, 0x3e, 0x55, 0x5d, 0x33, 0x84, 0x90, 0x97, 0x08, 0x4b, + 0x66, 0x67, 0x93, 0x71, 0xb9, 0xb7, 0x97, 0x18, 0x1d, 0x5d, 0x94, 0x8c, 0xe2, 0x67, 0x07, 0x84, 0x72, 0x7e, 0x9b, + 0x08, 0xed, 0xe5, 0x67, 0x2d, 0x86, 0xd6, 0x4c, 0xd3, 0x76, 0x0f, 0x6c, 0x72, 0x7f, 0xee, 0x33, 0x6e, 0x95, 0xbd, + 0x48, 0x9b, 0xdc, 0xa1, 0x68, 0xa1, 0x94, 0x0d, 0x37, 0xa3, 0xa0, 0x3e, 0x02, 0x57, 0xd0, 0x4a, 0xb4, 0x24, 0xfc, + 0x20, 0xa2, 0xe0, 0x0f, 0xd6, 0x7a, 0x44, 0x69, 0x1b, 0xee, 0x28, 0x39, 0xa2, 0x3a, 0xde, 0x0c, 0x7b, 0xb1, 0xda, + 0xbc, 0x66, 0x0b, 0x4c, 0x69, 0x36, 0x4a, 0xb3, 0x89, 0x7e, 0x57, 0xac, 0x3c, 0x2b, 0xde, 0xc8, 0x46, 0xce, 0xc6, + 0xbe, 0x95, 0x05, 0xd0, 0x5b, 0x31, 0xbc, 0x2b, 0x93, 0xbd, 0x26, 0x4c, 0x4b, 0xf9, 0x2b, 0xdd, 0x82, 0x9a, 0x32, + 0x13, 0xd3, 0xc4, 0x57, 0x3e, 0xd5, 0x9e, 0x74, 0x9b, 0xec, 0x74, 0x7a, 0xa5, 0xdd, 0xa7, 0xa9, 0xa1, 0x27, 0xdd, + 0x1b, 0x4a, 0xa8, 0xa6, 0xb3, 0x38, 0x54, 0xc0, 0x32, 0x84, 0xa9, 0xa2, 0xa3, 0x39, 0x8b, 0xe3, 0xaa, 0xf4, 0x73, + 0x38, 0x7b, 0xa2, 0x38, 0x7b, 0xa6, 0x39, 0x3b, 0xb0, 0x0a, 0xe0, 0xec, 0xb2, 0xbb, 0xaa, 0x79, 0xb6, 0xb6, 0x3d, + 0x33, 0xc9, 0xd3, 0x13, 0x61, 0x4b, 0xc3, 0x78, 0x33, 0x0d, 0x01, 0x2a, 0x75, 0xaf, 0x8f, 0x8e, 0x72, 0xc5, 0x80, + 0x11, 0x28, 0x3d, 0x99, 0xd4, 0x74, 0x53, 0x7c, 0x74, 0x10, 0x4e, 0x0a, 0x5a, 0x52, 0xf6, 0xc9, 0x33, 0xf0, 0xd5, + 0x19, 0xd3, 0x01, 0x31, 0x26, 0x8a, 0x3f, 0x4b, 0x8d, 0xd2, 0xb3, 0x63, 0x6a, 0x76, 0x89, 0x9e, 0x1d, 0xf0, 0xfa, + 0x6a, 0x76, 0xe1, 0xdd, 0xdc, 0x5e, 0x4c, 0x8f, 0x95, 0xd3, 0xab, 0xd6, 0x7b, 0xb9, 0x74, 0x56, 0x4a, 0xc0, 0x8d, + 0xaf, 0x8c, 0x94, 0xac, 0xec, 0x1d, 0x78, 0x80, 0x89, 0x19, 0x28, 0x28, 0xe4, 0xa4, 0x4b, 0x21, 0xf7, 0xf2, 0x53, + 0x4e, 0x1e, 0xe1, 0xad, 0x97, 0xed, 0x4f, 0xd3, 0xc9, 0x14, 0xf4, 0xb1, 0x15, 0x92, 0x1e, 0x53, 0x35, 0x60, 0xf5, + 0xbe, 0xd8, 0x50, 0x56, 0x6b, 0x23, 0xf6, 0x63, 0x8d, 0x9a, 0x4a, 0x9b, 0x79, 0xa7, 0x5d, 0xcc, 0xca, 0xa2, 0x92, + 0x71, 0x6c, 0x72, 0xac, 0x9c, 0xae, 0xba, 0x65, 0xf4, 0x8b, 0x37, 0x0e, 0x93, 0x7c, 0x98, 0x01, 0xaf, 0x33, 0xd8, + 0x8f, 0x26, 0x77, 0x73, 0xfd, 0x8b, 0x0a, 0x39, 0x8b, 0x62, 0x05, 0x7d, 0x8b, 0xa2, 0x78, 0xa6, 0xec, 0x6c, 0xfc, + 0x6c, 0xbb, 0x41, 0x5c, 0xbd, 0x53, 0xf6, 0xe2, 0xf9, 0x10, 0x3f, 0x5b, 0xd7, 0x1e, 0xc9, 0x62, 0x92, 0x86, 0xd4, + 0xb3, 0xd3, 0x29, 0x4d, 0xec, 0x02, 0xbc, 0xab, 0x6a, 0xf1, 0x67, 0xdc, 0x59, 0xbc, 0xab, 0xbb, 0x59, 0xbd, 0x67, + 0x05, 0xb8, 0xc0, 0x7e, 0x5c, 0x77, 0xc0, 0x7e, 0x4b, 0xb3, 0x5c, 0xe8, 0xa2, 0xa5, 0x5a, 0xfb, 0x63, 0x25, 0x98, + 0x7e, 0xf4, 0xb6, 0xd6, 0xaf, 0xac, 0x10, 0xbb, 0xe3, 0x3e, 0x74, 0xf7, 0x6d, 0x24, 0xdc, 0xc3, 0xdf, 0xa8, 0x1d, + 0xff, 0x8b, 0x76, 0x0f, 0x9f, 0x91, 0x5f, 0xea, 0xde, 0xe1, 0x29, 0x27, 0x67, 0x83, 0x33, 0x6d, 0x34, 0xa7, 0x31, + 0x0b, 0x6e, 0x1d, 0x3b, 0x66, 0xbc, 0x09, 0x21, 0x38, 0x1b, 0x2f, 0xe4, 0x0b, 0xf0, 0x2b, 0x0a, 0xb7, 0x76, 0xa1, + 0xcd, 0x3d, 0xcc, 0x38, 0xb1, 0x77, 0x63, 0xc6, 0x77, 0x6d, 0xbc, 0x4b, 0x2e, 0xe1, 0xc7, 0xee, 0xc2, 0x79, 0xe5, + 0xf3, 0xc8, 0xcd, 0xfc, 0x24, 0x4c, 0x27, 0x0e, 0x6a, 0xd8, 0x36, 0x72, 0x73, 0x61, 0x72, 0x3c, 0x46, 0xc5, 0xee, + 0x25, 0x3e, 0xe3, 0xc4, 0x1e, 0xd8, 0x8d, 0x5d, 0xfc, 0x8a, 0x93, 0xcb, 0xe3, 0xdd, 0xc5, 0x19, 0x2f, 0xfa, 0x97, + 0xf8, 0xa4, 0xf4, 0xdc, 0xe3, 0xd7, 0xc4, 0x41, 0xa4, 0x7f, 0xa2, 0xa0, 0x39, 0x4d, 0x27, 0xd2, 0x83, 0x6f, 0x23, + 0xfc, 0x0e, 0xe2, 0x2b, 0x79, 0xc5, 0x6e, 0x54, 0x88, 0x65, 0x87, 0xd8, 0xa9, 0xf0, 0x12, 0xd8, 0x7b, 0x7b, 0x46, + 0x59, 0xa9, 0x2c, 0xe0, 0x53, 0x4e, 0x6a, 0x36, 0x39, 0x7e, 0x29, 0x22, 0x35, 0xa7, 0xdc, 0xc9, 0x91, 0xee, 0xc6, + 0xd1, 0xee, 0x68, 0xb5, 0x37, 0xf3, 0x73, 0xe9, 0x64, 0x70, 0x19, 0xa7, 0x99, 0xcf, 0xd3, 0x6c, 0x88, 0x4c, 0x05, + 0x04, 0xff, 0x8d, 0x5c, 0x9e, 0x5b, 0xff, 0xe9, 0x8b, 0x9f, 0x46, 0x3f, 0x65, 0xc3, 0x4b, 0xfc, 0x81, 0xb4, 0x8e, + 0x9d, 0x81, 0xe7, 0xec, 0x34, 0x9b, 0xcb, 0x9f, 0x5a, 0xe7, 0x7f, 0xf7, 0x9b, 0xbf, 0x9e, 0x34, 0x7f, 0x1c, 0xa2, + 0xa5, 0xf3, 0x53, 0x6b, 0x70, 0xae, 0x9e, 0xce, 0xff, 0xde, 0xff, 0x29, 0x1f, 0x7e, 0x25, 0x0b, 0x77, 0x11, 0x6a, + 0x8d, 0xf1, 0x98, 0x93, 0x56, 0xb3, 0xd9, 0x6f, 0x8d, 0xf1, 0x84, 0x93, 0x16, 0xfc, 0x3b, 0x27, 0xef, 0xe8, 0xf8, + 0xd9, 0xcd, 0xd4, 0xb9, 0xec, 0x2f, 0x77, 0x17, 0x7f, 0x2b, 0xa0, 0xd7, 0xf3, 0xbf, 0xff, 0xf4, 0x53, 0x6e, 0x3f, + 0xe8, 0x93, 0xd6, 0xb0, 0x81, 0x1c, 0x28, 0xfd, 0x8a, 0x88, 0xbf, 0xce, 0xc0, 0x3b, 0xff, 0xbb, 0x82, 0xc2, 0x7e, + 0xf0, 0xd3, 0xe5, 0x71, 0x9f, 0x0c, 0x97, 0x8e, 0xbd, 0x7c, 0x80, 0x96, 0x08, 0x2d, 0x77, 0xd1, 0x25, 0xb6, 0xc7, + 0x36, 0xc2, 0x17, 0x9c, 0xb4, 0x1e, 0xb4, 0xc6, 0x78, 0xc4, 0x49, 0xcb, 0x6e, 0x8d, 0xf1, 0x1b, 0x4e, 0x5a, 0x7f, + 0x77, 0x06, 0x9e, 0x74, 0xb3, 0x2d, 0x85, 0x87, 0x63, 0x09, 0x41, 0x0e, 0x3f, 0xa3, 0xfe, 0x92, 0x33, 0x1e, 0x53, + 0xb4, 0xdb, 0x62, 0xf8, 0xa3, 0x40, 0x93, 0xc3, 0xc1, 0x0f, 0x03, 0xe6, 0x9d, 0xb3, 0xb8, 0x80, 0xc5, 0x06, 0x9a, + 0xd9, 0xf5, 0x20, 0xba, 0x03, 0xae, 0x80, 0xdc, 0xe3, 0xf8, 0xda, 0x8f, 0x67, 0x34, 0xf7, 0x68, 0x81, 0x70, 0x4c, + 0x3e, 0x72, 0xa7, 0x83, 0xf0, 0x0b, 0x0e, 0x3f, 0xba, 0x08, 0x9f, 0xaa, 0x40, 0x26, 0xec, 0x64, 0x49, 0x54, 0x49, + 0x2a, 0x55, 0x16, 0x1b, 0xe1, 0xf1, 0x86, 0x97, 0x3c, 0x02, 0x07, 0x03, 0xc2, 0xd7, 0xb5, 0xb0, 0x27, 0xbe, 0x21, + 0x9a, 0x24, 0xde, 0x67, 0x94, 0x7e, 0xe7, 0xc7, 0x1f, 0x69, 0xe6, 0x9c, 0xe0, 0x4e, 0xf7, 0x31, 0x16, 0x7e, 0xe8, + 0x9d, 0x0e, 0xea, 0x95, 0x31, 0xab, 0xb7, 0x5c, 0x86, 0x0a, 0x40, 0xca, 0xd6, 0xdd, 0x31, 0xb0, 0xe2, 0x3b, 0xeb, + 0x3e, 0xab, 0xcc, 0x9f, 0xdb, 0xa8, 0x1e, 0x1f, 0x65, 0xc9, 0xb5, 0x1f, 0xb3, 0xd0, 0xe2, 0x74, 0x32, 0x8d, 0x7d, + 0x4e, 0x2d, 0x35, 0x5f, 0xcb, 0x87, 0x8e, 0xec, 0x52, 0x67, 0x98, 0x1a, 0x36, 0xe7, 0x54, 0x07, 0x9e, 0x60, 0xaf, + 0x38, 0x10, 0xa5, 0x52, 0x7a, 0xc7, 0xd3, 0x2a, 0x08, 0xb6, 0x1a, 0xe7, 0x6b, 0x76, 0xc0, 0x17, 0x36, 0x14, 0xf2, + 0x39, 0xc1, 0x19, 0x01, 0x29, 0xda, 0x1d, 0xd8, 0xc7, 0xf9, 0xf5, 0xb8, 0x6f, 0x43, 0x8c, 0x26, 0x25, 0x1f, 0x84, + 0x6b, 0x08, 0x2a, 0x44, 0xa4, 0xdd, 0x8b, 0x8e, 0x69, 0x2f, 0x6a, 0x34, 0xb4, 0x16, 0xed, 0x93, 0xfc, 0x3c, 0x92, + 0xcd, 0x03, 0x1c, 0xe2, 0x19, 0x69, 0x76, 0xf0, 0x94, 0xb4, 0x45, 0x93, 0xde, 0xf4, 0xd8, 0x57, 0xc3, 0xec, 0xed, + 0x39, 0xa9, 0x1b, 0xfb, 0x39, 0x7f, 0x01, 0xf6, 0x3e, 0x99, 0xe2, 0x90, 0xa4, 0x2e, 0xbd, 0xa1, 0x81, 0xe3, 0x23, + 0x1c, 0x2a, 0x4e, 0x83, 0x7a, 0x68, 0x4a, 0x8c, 0x6a, 0x60, 0x46, 0x90, 0x0f, 0x83, 0xf0, 0xbc, 0x33, 0x24, 0x84, + 0xd8, 0x3b, 0xcd, 0xa6, 0x3d, 0x48, 0xc9, 0x98, 0x7b, 0x50, 0x62, 0x28, 0xcb, 0x64, 0x02, 0x45, 0x5d, 0xa3, 0xc8, + 0x79, 0xc3, 0x5d, 0x4e, 0x73, 0xee, 0x40, 0x31, 0x78, 0x00, 0x12, 0x4d, 0xd8, 0xf6, 0x71, 0xcb, 0x6e, 0x40, 0xa9, + 0x20, 0x4e, 0x84, 0x53, 0x32, 0x47, 0x5e, 0x78, 0xbe, 0x3f, 0x34, 0x05, 0x80, 0x28, 0x84, 0xc1, 0xe7, 0x83, 0xf0, + 0xbc, 0x2d, 0x06, 0xef, 0xdb, 0x03, 0x27, 0x25, 0xc9, 0x8e, 0x8a, 0xde, 0x78, 0x1f, 0xc4, 0x54, 0x91, 0xa7, 0x80, + 0x53, 0xe3, 0xce, 0x48, 0xb3, 0xeb, 0x39, 0x33, 0x73, 0x12, 0x4d, 0x18, 0x4c, 0x61, 0x01, 0x07, 0x04, 0xea, 0xe3, + 0x94, 0xc0, 0x88, 0x55, 0xb3, 0xb9, 0xa7, 0x9e, 0x1f, 0xd8, 0x0f, 0x06, 0x23, 0xee, 0x5d, 0x70, 0x39, 0xfc, 0x88, + 0x2f, 0x97, 0xf0, 0xef, 0x05, 0x1f, 0xa4, 0x64, 0x2e, 0x8a, 0xc6, 0xaa, 0x68, 0x02, 0x45, 0x1f, 0x3c, 0x00, 0x15, + 0x27, 0xa5, 0x96, 0x25, 0xd7, 0x64, 0x42, 0x04, 0xec, 0x7b, 0x7b, 0xf9, 0x79, 0xd4, 0xe8, 0x0c, 0xc1, 0xc9, 0x9f, + 0xf1, 0xfc, 0x3b, 0xc6, 0x23, 0xc7, 0x6e, 0xf5, 0x6d, 0x34, 0xb0, 0x2d, 0x58, 0xda, 0x5e, 0xd6, 0x20, 0x12, 0xc3, + 0x7e, 0xe3, 0x15, 0xf7, 0x66, 0x7d, 0xd2, 0x1e, 0x38, 0x4c, 0xb9, 0xf4, 0x10, 0xf6, 0x15, 0xe3, 0x6c, 0xe3, 0x19, + 0x6a, 0x30, 0xde, 0xd0, 0xcf, 0x33, 0xd4, 0xd8, 0x6d, 0x4c, 0x90, 0xe7, 0x37, 0x76, 0x1b, 0xce, 0x8c, 0x10, 0xd2, + 0xec, 0x96, 0xcd, 0xb4, 0xf8, 0x8b, 0x90, 0x37, 0xd1, 0xfe, 0xce, 0x73, 0xb1, 0x1d, 0xb2, 0x86, 0x03, 0x2e, 0x96, + 0xe5, 0xd2, 0x3e, 0x1e, 0xf4, 0x6d, 0xd4, 0x70, 0x34, 0xa1, 0xb5, 0x34, 0xa5, 0x21, 0x84, 0xd9, 0xb0, 0x50, 0xf1, + 0xa4, 0x27, 0xb5, 0xd8, 0xd1, 0xa2, 0xda, 0xec, 0x06, 0x0f, 0xa0, 0x45, 0x69, 0xc8, 0x48, 0x85, 0x75, 0x0a, 0xd3, + 0xd4, 0xc4, 0x9c, 0x91, 0x36, 0x4e, 0x89, 0x76, 0x5f, 0x47, 0x84, 0x57, 0x04, 0xef, 0x93, 0xaa, 0x3a, 0x3e, 0x0f, + 0x70, 0x38, 0x24, 0x4f, 0xa5, 0x41, 0xd2, 0xd3, 0xce, 0x71, 0x1a, 0x93, 0x27, 0x2b, 0x51, 0xdc, 0x00, 0x02, 0x2c, + 0x37, 0x6e, 0x30, 0xcb, 0x32, 0x9a, 0xf0, 0xd7, 0x69, 0xa8, 0xf4, 0x34, 0x1a, 0x83, 0xa9, 0x04, 0xe1, 0x59, 0x0c, + 0x4a, 0x5a, 0x57, 0xef, 0x8c, 0xd9, 0xda, 0xeb, 0x29, 0x99, 0x49, 0xfd, 0x49, 0x04, 0x6d, 0x7b, 0x53, 0x65, 0x19, + 0x3b, 0x08, 0xcf, 0x54, 0x34, 0xd7, 0x71, 0x5d, 0x77, 0xea, 0x06, 0xf0, 0x1a, 0x06, 0xc8, 0x51, 0x21, 0xf6, 0x91, + 0x93, 0x90, 0x1b, 0x37, 0xa1, 0x37, 0x62, 0x54, 0x07, 0x55, 0x92, 0x59, 0x6f, 0xaf, 0xe3, 0xa8, 0x27, 0xd8, 0x4d, + 0xe2, 0x26, 0x69, 0x48, 0x01, 0x3d, 0x10, 0xbf, 0x57, 0x45, 0x91, 0x9f, 0x9b, 0x41, 0xaa, 0x0a, 0xbe, 0x73, 0xd3, + 0x7f, 0x3d, 0x05, 0xa7, 0xaf, 0xb0, 0x88, 0xcb, 0xca, 0xd2, 0x13, 0x8e, 0x10, 0x1b, 0x39, 0x53, 0x17, 0x82, 0x7b, + 0x82, 0x84, 0x18, 0xd8, 0x72, 0x53, 0x93, 0xa8, 0x76, 0xcb, 0x3e, 0x27, 0x24, 0x3c, 0x4f, 0x1b, 0x0d, 0xe1, 0x88, + 0x9e, 0x49, 0x92, 0x98, 0x22, 0x3c, 0x29, 0xf7, 0x96, 0xae, 0xf7, 0x96, 0xd4, 0x47, 0x72, 0x26, 0x75, 0x87, 0x6e, + 0x83, 0x71, 0x24, 0x7c, 0x85, 0xdc, 0xd9, 0x45, 0xf8, 0x82, 0xb4, 0x9c, 0x73, 0x77, 0xf0, 0xe7, 0x21, 0x1a, 0x38, + 0xee, 0x57, 0xa8, 0x25, 0x19, 0xc7, 0x04, 0xf5, 0x7c, 0x39, 0xc4, 0x42, 0x44, 0x31, 0x3b, 0x58, 0xf8, 0x12, 0xbd, + 0x0c, 0x27, 0xfe, 0x84, 0x7a, 0x17, 0xb0, 0xc7, 0x35, 0xdd, 0xbc, 0xc5, 0x40, 0x47, 0xde, 0x85, 0xe2, 0x24, 0xae, + 0x3d, 0xf8, 0x85, 0x97, 0x4f, 0x03, 0x7b, 0xf0, 0x75, 0xf5, 0xf4, 0x67, 0x7b, 0xf0, 0x2d, 0xf7, 0xbe, 0x2d, 0x94, + 0xbb, 0xbb, 0x36, 0xc4, 0x43, 0x3d, 0x44, 0x21, 0x17, 0xc6, 0xc0, 0xdc, 0x0c, 0x25, 0x6b, 0x8e, 0x8e, 0x29, 0x2a, + 0xd8, 0xa8, 0x64, 0x45, 0x89, 0xcb, 0xfd, 0x31, 0xa0, 0xd4, 0x58, 0x81, 0xc4, 0x8c, 0xee, 0x57, 0x13, 0x06, 0x42, + 0xd1, 0xd4, 0x0a, 0xa8, 0x9c, 0xf6, 0xdb, 0x68, 0x51, 0xab, 0x2b, 0x34, 0xa6, 0x7a, 0x34, 0xbd, 0xe4, 0xd2, 0x13, + 0xd2, 0xee, 0x4d, 0x8e, 0xa7, 0xbd, 0x49, 0xa3, 0x81, 0x12, 0x4d, 0x58, 0xb3, 0xf3, 0xc9, 0x10, 0xbf, 0x06, 0xaf, + 0x9e, 0x49, 0x49, 0xb8, 0x36, 0xbd, 0xae, 0x9a, 0x5e, 0xa3, 0x91, 0x15, 0xa8, 0x67, 0x34, 0x9d, 0xca, 0xa6, 0x45, + 0x21, 0x71, 0xb2, 0x4a, 0x68, 0x47, 0x48, 0x94, 0x40, 0x4a, 0x14, 0x21, 0xe4, 0x8c, 0xa3, 0x8d, 0xbd, 0x42, 0x9f, + 0xd0, 0x5c, 0xec, 0x58, 0x60, 0x9e, 0x52, 0x46, 0x38, 0x80, 0x05, 0x68, 0x5a, 0xba, 0x82, 0x77, 0xf1, 0xac, 0xd1, + 0x11, 0x44, 0xde, 0xec, 0xf4, 0xea, 0x7d, 0x3d, 0xaa, 0xfa, 0xc2, 0xb3, 0x06, 0xd9, 0x2d, 0xb1, 0x54, 0x64, 0x8d, + 0x46, 0x51, 0x8f, 0x77, 0xea, 0x7d, 0x5b, 0x8b, 0x40, 0x9c, 0xac, 0xa6, 0x66, 0x68, 0xf9, 0x5a, 0x49, 0x54, 0xe6, + 0xb2, 0x24, 0xa1, 0x19, 0xc8, 0x50, 0xc2, 0x31, 0x2b, 0x8a, 0x52, 0xae, 0xbf, 0x01, 0x21, 0x8a, 0x29, 0xc9, 0x81, + 0xef, 0x08, 0xb3, 0x0b, 0x67, 0x38, 0xc5, 0x91, 0xe0, 0x1a, 0x84, 0x90, 0x53, 0x9d, 0xd4, 0xc2, 0x05, 0x07, 0xf2, + 0x09, 0x33, 0x24, 0x52, 0x42, 0xa8, 0x7b, 0xb1, 0x7b, 0x9a, 0xde, 0x69, 0x92, 0x9d, 0xb3, 0xa1, 0x27, 0xaa, 0xc5, + 0x8a, 0x6f, 0x05, 0xe4, 0x9d, 0xc3, 0x51, 0x19, 0x1e, 0x71, 0x05, 0xfb, 0x7b, 0xca, 0x32, 0x2a, 0x34, 0xf0, 0x5d, + 0x6d, 0xf6, 0xf9, 0x75, 0xf5, 0xd1, 0x37, 0x9d, 0x37, 0x80, 0xc8, 0x00, 0x7c, 0x3b, 0x19, 0x59, 0xab, 0x76, 0xb1, + 0x7b, 0xf2, 0x66, 0x93, 0x09, 0xbc, 0x5c, 0x2a, 0xe3, 0xd7, 0x07, 0xcd, 0x06, 0x07, 0x15, 0xa4, 0xbe, 0xfa, 0xe1, + 0x39, 0xbe, 0x50, 0x90, 0x02, 0x27, 0x07, 0x2a, 0xba, 0xd8, 0x3d, 0x79, 0xef, 0xe4, 0xc2, 0xb5, 0x84, 0xb0, 0x39, + 0x6d, 0x27, 0x25, 0x4e, 0x44, 0x28, 0x92, 0x73, 0x2f, 0x19, 0x57, 0x6a, 0x88, 0x6f, 0x2f, 0x12, 0x2f, 0xc1, 0x7e, + 0x38, 0x67, 0x43, 0xe2, 0x2b, 0x0c, 0x10, 0x1f, 0x61, 0xbf, 0x66, 0x96, 0x11, 0x58, 0x00, 0x31, 0xd6, 0x19, 0xac, + 0x84, 0x2b, 0x15, 0x3f, 0x84, 0x7d, 0x31, 0x2a, 0x2f, 0xa4, 0xe8, 0xf8, 0x79, 0x2d, 0x37, 0xad, 0xb2, 0x46, 0xbf, + 0x05, 0xcb, 0x49, 0x3f, 0xbc, 0x56, 0x5d, 0x97, 0x05, 0x4f, 0x75, 0x12, 0xd9, 0xc5, 0xee, 0xc9, 0x2b, 0x95, 0x47, + 0x36, 0xf5, 0x35, 0xb7, 0x5f, 0xb3, 0x30, 0x4f, 0x5e, 0xb9, 0xd5, 0x5b, 0x51, 0xf9, 0x62, 0xf7, 0xe4, 0xc3, 0xa6, + 0x6a, 0x50, 0x5e, 0xcc, 0x2a, 0x13, 0x5f, 0xc0, 0xb7, 0xa0, 0xb1, 0xb7, 0x50, 0xa2, 0xc1, 0x63, 0x05, 0x16, 0xe2, + 0xc8, 0x4b, 0x8a, 0xd2, 0x33, 0xf2, 0x14, 0x67, 0x44, 0xc4, 0x81, 0xea, 0xab, 0xa6, 0x94, 0x3c, 0x96, 0x26, 0x67, + 0x41, 0x3a, 0xa5, 0x5b, 0x82, 0x43, 0x27, 0xc8, 0x65, 0x13, 0x48, 0xa0, 0x11, 0xa0, 0x33, 0xbc, 0xd3, 0x46, 0xbd, + 0xba, 0xf0, 0xca, 0x04, 0x91, 0xa6, 0x35, 0xc9, 0x82, 0x23, 0xd2, 0xc6, 0x3e, 0x69, 0xe3, 0x80, 0x24, 0xe7, 0x6d, + 0x29, 0x1e, 0x7a, 0x41, 0xd9, 0xaf, 0x14, 0x32, 0x90, 0x1b, 0x16, 0xc8, 0xdd, 0x2a, 0xc5, 0x6f, 0xd8, 0x0b, 0x84, + 0xeb, 0x51, 0x48, 0xf4, 0x50, 0x1a, 0xad, 0x4e, 0x8a, 0x53, 0xd1, 0xf1, 0x19, 0xbb, 0x8a, 0x21, 0xbb, 0x04, 0x66, + 0x85, 0x39, 0xf2, 0xca, 0xaa, 0x1d, 0x55, 0x35, 0x70, 0xc5, 0x3a, 0xa5, 0x38, 0x70, 0x81, 0x71, 0xe3, 0x40, 0x25, + 0xe3, 0xe4, 0xeb, 0x4d, 0x1e, 0xee, 0xed, 0x39, 0xb2, 0xd1, 0x77, 0xdc, 0x49, 0xf5, 0xfb, 0x2a, 0x74, 0xf7, 0xad, + 0xe4, 0x15, 0x21, 0x12, 0xf0, 0x37, 0x1a, 0xfe, 0xb0, 0x80, 0x38, 0xb4, 0x13, 0xd4, 0x31, 0xa8, 0x81, 0x17, 0x9a, + 0x5e, 0x7d, 0xfa, 0x8d, 0x46, 0x19, 0xa6, 0xad, 0x63, 0xeb, 0x04, 0x67, 0xc5, 0xb5, 0x53, 0xe6, 0xff, 0xb4, 0xd7, + 0xb2, 0xa6, 0x34, 0x08, 0x88, 0x99, 0x34, 0xcb, 0xf4, 0x64, 0x8c, 0x2d, 0xc1, 0xa0, 0xde, 0x0b, 0x95, 0xb8, 0x80, + 0x45, 0x8e, 0x95, 0xaa, 0xa4, 0xd9, 0x59, 0x17, 0x79, 0xba, 0x12, 0x84, 0xa5, 0xa0, 0x52, 0xa3, 0x50, 0xe4, 0xfd, + 0x6a, 0x3d, 0xf3, 0x12, 0x27, 0x48, 0xf9, 0xb8, 0x04, 0x14, 0x02, 0x59, 0xdd, 0x12, 0x29, 0xcf, 0xc9, 0x78, 0x3b, + 0xc9, 0x9f, 0x18, 0x24, 0xff, 0x84, 0x50, 0x83, 0xfc, 0xa5, 0x87, 0xc3, 0x4d, 0x95, 0x6b, 0x21, 0xd1, 0xaf, 0x4e, + 0xa7, 0x04, 0x7c, 0x68, 0x75, 0x8c, 0x26, 0x66, 0x5c, 0x71, 0x0b, 0x43, 0x31, 0x77, 0x88, 0xf0, 0x42, 0x62, 0x1d, + 0x04, 0x76, 0xaa, 0xa8, 0x1a, 0x0c, 0xbd, 0xc9, 0xa5, 0x67, 0x72, 0xc0, 0x93, 0x0f, 0x77, 0x07, 0x44, 0x4f, 0xa7, + 0xeb, 0x3b, 0xd7, 0xc8, 0x00, 0x85, 0x59, 0x1b, 0x1b, 0xb7, 0x9e, 0x0f, 0x0a, 0xe3, 0x97, 0x81, 0xec, 0x3a, 0xf3, + 0x59, 0xd9, 0x84, 0x5a, 0xfe, 0x01, 0xb4, 0x9d, 0x8e, 0xa8, 0x41, 0x8d, 0x6e, 0x81, 0x1f, 0xc9, 0x3c, 0x54, 0x3f, + 0xdb, 0xc2, 0x3e, 0x4e, 0x44, 0x05, 0x9a, 0x84, 0x9b, 0x5f, 0x3f, 0x29, 0x14, 0x99, 0x48, 0xd0, 0xd0, 0x02, 0xf8, + 0x9f, 0x24, 0x79, 0xa0, 0x1b, 0x21, 0x17, 0x00, 0x41, 0x63, 0x81, 0xa7, 0x0a, 0x61, 0xb6, 0x5d, 0x39, 0xdf, 0x9f, + 0xef, 0x10, 0x32, 0xae, 0x9c, 0x8f, 0xef, 0xaa, 0xec, 0x2b, 0x20, 0x0b, 0xe4, 0x81, 0xf1, 0x58, 0x16, 0xc8, 0xf8, + 0xe5, 0xa9, 0xae, 0x2e, 0x0c, 0x48, 0xb7, 0xd2, 0xb7, 0x8d, 0xd8, 0xa6, 0xf0, 0xca, 0xc9, 0xf7, 0x1a, 0x0d, 0x2b, + 0x6f, 0x77, 0xe1, 0xed, 0x4b, 0x2e, 0x60, 0x84, 0xe7, 0xf7, 0xa2, 0xb6, 0xee, 0xb7, 0xf8, 0xb8, 0x9a, 0xc2, 0xb2, + 0xb2, 0x28, 0x2e, 0x4b, 0x72, 0x9a, 0xf1, 0x27, 0x74, 0x94, 0x66, 0x10, 0xb2, 0x28, 0x71, 0x82, 0x8a, 0x5d, 0xc3, + 0x6d, 0x27, 0xe6, 0x67, 0xc4, 0x09, 0x56, 0x26, 0x28, 0x7e, 0x7d, 0x14, 0x51, 0xeb, 0x8b, 0xd5, 0x56, 0xe3, 0xbd, + 0xbd, 0x77, 0x15, 0x9a, 0x14, 0x94, 0x02, 0x0a, 0x83, 0x69, 0x49, 0x95, 0x46, 0x85, 0x72, 0x77, 0x9d, 0xd2, 0x05, + 0xa0, 0x19, 0x86, 0xc9, 0x7b, 0x9e, 0x13, 0x5e, 0x8c, 0x57, 0x59, 0xbc, 0x72, 0x4d, 0x30, 0xd3, 0x6c, 0x01, 0x0e, + 0x0f, 0x86, 0xb6, 0xf4, 0x15, 0x25, 0x55, 0x4a, 0x6c, 0x09, 0xc3, 0x29, 0x20, 0xcb, 0x49, 0xc0, 0x08, 0x31, 0x28, + 0x30, 0xd9, 0x64, 0x94, 0xbc, 0x05, 0xbd, 0x32, 0xc2, 0x89, 0x1b, 0x41, 0x12, 0x6c, 0x6d, 0xcb, 0x22, 0x84, 0x13, + 0x61, 0xd0, 0x18, 0xb9, 0x04, 0x27, 0xcf, 0x37, 0x79, 0x94, 0x35, 0x51, 0x53, 0x21, 0x75, 0xa0, 0x46, 0x86, 0xca, + 0x06, 0xee, 0xb5, 0xc3, 0x94, 0xe2, 0x56, 0xc6, 0xcd, 0xe8, 0xdc, 0xfa, 0x99, 0x3b, 0x32, 0x16, 0x05, 0x32, 0x23, + 0x75, 0x67, 0x4e, 0x6d, 0xe8, 0x5e, 0x2a, 0x9a, 0x61, 0x85, 0xb8, 0xc8, 0x44, 0x53, 0x2a, 0xe2, 0x7a, 0xa7, 0x15, + 0x2f, 0xbd, 0x96, 0x79, 0xd4, 0x5c, 0x73, 0xc1, 0x2a, 0x93, 0xc4, 0x98, 0xfe, 0xb5, 0x4c, 0x8d, 0x2e, 0x2b, 0x61, + 0x2a, 0xc0, 0x78, 0x22, 0xd6, 0x80, 0x16, 0x40, 0x5f, 0x8b, 0x53, 0x6e, 0xac, 0xa8, 0xf6, 0x61, 0x8b, 0x31, 0x0d, + 0xa9, 0xff, 0x0e, 0x72, 0x5d, 0x56, 0xf7, 0xfc, 0x73, 0x21, 0x0b, 0x19, 0x4e, 0x6a, 0x8c, 0x3d, 0x13, 0x8c, 0x1d, + 0x81, 0x9e, 0xa6, 0xd3, 0xbf, 0x07, 0x2a, 0xe5, 0x45, 0xe5, 0x2e, 0x3a, 0x8a, 0xc4, 0x5e, 0x97, 0xe1, 0x72, 0xe3, + 0xf7, 0xca, 0x6a, 0x78, 0x8c, 0x40, 0x1a, 0x10, 0x56, 0x9c, 0x3d, 0x43, 0x38, 0x69, 0x34, 0x7a, 0xc9, 0x31, 0xad, + 0x5c, 0x24, 0x15, 0x8c, 0x0c, 0x22, 0xba, 0x40, 0xf0, 0x35, 0x19, 0x9a, 0x20, 0x5c, 0xe6, 0xa1, 0x27, 0xe0, 0x6a, + 0x3f, 0x79, 0xe7, 0x98, 0x5c, 0xcd, 0xac, 0x5b, 0x06, 0x4d, 0x61, 0x3e, 0x4e, 0x15, 0x6f, 0x79, 0x7b, 0x77, 0x86, + 0x07, 0xc0, 0xbd, 0xd3, 0xc1, 0x90, 0x8d, 0x86, 0x7a, 0x5c, 0xb2, 0x84, 0x72, 0xf7, 0xf5, 0x50, 0x95, 0x98, 0x68, + 0x0e, 0xd6, 0xe3, 0x95, 0x29, 0xcb, 0x49, 0x52, 0x14, 0x39, 0xad, 0xe2, 0xfb, 0x2b, 0x19, 0x98, 0x42, 0xb8, 0xac, + 0x3b, 0xdb, 0x4f, 0xa7, 0x84, 0x63, 0x83, 0x50, 0xdf, 0x6e, 0x0b, 0x7d, 0x54, 0x60, 0xc2, 0xbe, 0x56, 0x42, 0xf1, + 0xdb, 0x4d, 0x42, 0x11, 0x67, 0x6a, 0xcb, 0x0b, 0x81, 0xd8, 0xb9, 0x87, 0x40, 0x54, 0x4e, 0x76, 0x2d, 0x13, 0x41, + 0x1d, 0xa9, 0xc9, 0xc4, 0xa4, 0x2e, 0x13, 0x33, 0xcc, 0xd4, 0x6a, 0xf4, 0xbb, 0xcb, 0x25, 0x3b, 0x6f, 0x83, 0x13, + 0xc9, 0xb6, 0xe1, 0x67, 0x47, 0xfe, 0x34, 0x38, 0xb1, 0x74, 0x02, 0x3b, 0xac, 0x34, 0x59, 0x90, 0x0b, 0x69, 0xce, + 0x8e, 0xc8, 0xca, 0x12, 0x34, 0xad, 0x28, 0x48, 0x11, 0x38, 0x61, 0x65, 0x94, 0x09, 0x20, 0x16, 0xb2, 0x42, 0x19, + 0x90, 0xce, 0xc6, 0xf4, 0x3f, 0x6d, 0x5e, 0x7e, 0x5a, 0x13, 0xad, 0xc9, 0x15, 0xa9, 0x3e, 0xd4, 0x12, 0x0e, 0x14, + 0x04, 0x4a, 0x3f, 0xdc, 0x11, 0x26, 0x68, 0x25, 0xca, 0x91, 0x29, 0x87, 0x70, 0x1b, 0x5c, 0x68, 0x3b, 0xef, 0x64, + 0x80, 0x77, 0x83, 0x34, 0xc1, 0xa9, 0x41, 0xd7, 0xcf, 0x09, 0xaf, 0xb1, 0x92, 0x88, 0x28, 0x4b, 0x09, 0x07, 0x82, + 0x4c, 0x39, 0xc9, 0xce, 0xdb, 0x43, 0x50, 0x40, 0x7b, 0xfe, 0x71, 0x56, 0x99, 0xc0, 0x7e, 0xa3, 0x81, 0x02, 0x3d, + 0x6a, 0x74, 0xce, 0x1a, 0xfe, 0x10, 0x53, 0xec, 0x4b, 0xc3, 0xe4, 0x74, 0x6f, 0xcf, 0x09, 0xaa, 0x71, 0xcf, 0xfd, + 0x21, 0xc2, 0xe9, 0x72, 0xe9, 0x08, 0xb0, 0x02, 0xb4, 0x5c, 0x06, 0x26, 0x58, 0xe2, 0x35, 0x34, 0x1b, 0x0f, 0x38, + 0x19, 0x0b, 0x01, 0x38, 0x06, 0x08, 0x1b, 0xc4, 0x09, 0x94, 0x73, 0x2f, 0x00, 0x67, 0x54, 0x23, 0x3b, 0xf7, 0x1b, + 0x9d, 0xa1, 0xc1, 0xb8, 0xce, 0xfd, 0x21, 0x09, 0x8a, 0x74, 0x6f, 0x6f, 0x27, 0x51, 0x22, 0xf2, 0x67, 0x10, 0x65, + 0x3f, 0x0b, 0xc9, 0x22, 0x3b, 0x34, 0x57, 0x63, 0xd5, 0x19, 0x50, 0x52, 0x94, 0x5a, 0x56, 0x5d, 0xaf, 0x96, 0x05, + 0x51, 0x56, 0xc2, 0x2a, 0x16, 0x3c, 0x00, 0xcb, 0xbe, 0x24, 0xf3, 0x5f, 0x78, 0x99, 0x66, 0xfd, 0xed, 0xc6, 0xe4, + 0x6a, 0xd7, 0x75, 0xfd, 0x6c, 0x2c, 0x22, 0x19, 0x3a, 0x63, 0x52, 0x10, 0xff, 0xbe, 0x02, 0xd3, 0x18, 0xf8, 0xbc, + 0x1c, 0x6b, 0x48, 0x24, 0xf8, 0x5a, 0xb5, 0xd1, 0x27, 0x4a, 0x7e, 0xdd, 0xe8, 0x65, 0x90, 0x90, 0x7c, 0xfd, 0x5b, + 0x21, 0x39, 0x50, 0x90, 0x48, 0xf2, 0x58, 0xc1, 0xd9, 0x16, 0x5c, 0xfc, 0xca, 0x57, 0x70, 0xb6, 0x1d, 0xb7, 0x25, + 0x43, 0xd8, 0x06, 0x9f, 0xc1, 0x1b, 0x24, 0xa0, 0x55, 0x81, 0x01, 0xe5, 0xe1, 0xaa, 0xee, 0x25, 0x59, 0x29, 0x08, + 0x53, 0x4e, 0x1c, 0x56, 0xdf, 0x00, 0x95, 0x36, 0x6a, 0x18, 0xbe, 0xcc, 0x1b, 0x23, 0xc3, 0x25, 0x50, 0x4f, 0x5d, + 0x01, 0x72, 0x52, 0xbe, 0x76, 0x48, 0x45, 0xd8, 0x91, 0x4a, 0x9c, 0x1b, 0xf8, 0x53, 0x3e, 0xcb, 0x40, 0x95, 0x4a, + 0xf4, 0x6f, 0x28, 0x86, 0xb3, 0x20, 0xa2, 0x0c, 0x7e, 0x40, 0xc1, 0xd4, 0xcf, 0x73, 0x76, 0x2d, 0xcb, 0xd4, 0x6f, + 0x9c, 0x12, 0x4d, 0xca, 0x89, 0xd4, 0x09, 0x33, 0xd4, 0xcb, 0x14, 0x9d, 0xd6, 0xd1, 0xf6, 0xec, 0x9a, 0x26, 0xfc, + 0x25, 0xcb, 0x39, 0x4d, 0x60, 0xfa, 0x15, 0xc5, 0xc1, 0x8c, 0x12, 0x04, 0x1b, 0xb6, 0xd6, 0xca, 0x0f, 0xc3, 0x3b, + 0x9b, 0xf0, 0xba, 0x0e, 0x14, 0xf9, 0x49, 0x18, 0xcb, 0x41, 0xcc, 0x84, 0x46, 0x9d, 0xc4, 0x59, 0xd6, 0x34, 0xf3, + 0x69, 0x2a, 0x65, 0x43, 0x70, 0x77, 0x87, 0x11, 0x2d, 0x09, 0xb4, 0xf4, 0xbc, 0x53, 0x6b, 0x81, 0x80, 0xf7, 0x96, + 0x45, 0x30, 0x67, 0x82, 0xb9, 0xc1, 0x51, 0xdd, 0x3a, 0x9c, 0x9a, 0x6e, 0xbe, 0xdb, 0x78, 0xb0, 0x6d, 0x93, 0x70, + 0x10, 0x74, 0xf2, 0x70, 0xbb, 0x65, 0xf5, 0x4a, 0x4b, 0x0e, 0x2d, 0x2d, 0xd8, 0x7d, 0x19, 0x33, 0x5a, 0x68, 0xf2, + 0x42, 0x7a, 0x2b, 0xde, 0x72, 0xf2, 0x0b, 0x9c, 0x1c, 0x7a, 0xce, 0x27, 0xf1, 0xca, 0x01, 0x99, 0xde, 0x6d, 0xa9, + 0xfd, 0xdf, 0x72, 0xe7, 0x09, 0x7e, 0x05, 0x61, 0xdd, 0x6f, 0xaa, 0xea, 0xeb, 0xe1, 0xdc, 0x6f, 0x2a, 0x04, 0x7d, + 0xe3, 0xad, 0xd5, 0x33, 0xc2, 0xb8, 0x5d, 0xf7, 0xc8, 0x6d, 0xdb, 0x5a, 0x5b, 0xfa, 0x51, 0x06, 0x91, 0x64, 0xaa, + 0xa5, 0xd8, 0x0f, 0xb8, 0x4a, 0x54, 0x83, 0x84, 0xb9, 0xba, 0x85, 0x44, 0x55, 0x8a, 0xa1, 0xd4, 0xe1, 0xb7, 0x2d, + 0x8f, 0x92, 0x31, 0x99, 0xb4, 0x33, 0xde, 0xfa, 0x19, 0xdf, 0x85, 0x5d, 0x96, 0xae, 0x9d, 0xc6, 0x8b, 0x08, 0x78, + 0xd0, 0xee, 0x37, 0x44, 0x75, 0x16, 0x60, 0x90, 0xc8, 0xc3, 0x40, 0x66, 0xff, 0x24, 0xd5, 0xba, 0x5b, 0xdd, 0xca, + 0x78, 0x0d, 0xf6, 0x3f, 0xc2, 0x91, 0x3e, 0x22, 0x47, 0x15, 0x07, 0xa6, 0xde, 0xa2, 0x28, 0x9d, 0x02, 0xa9, 0x54, + 0xde, 0x72, 0x84, 0xd3, 0x42, 0x84, 0xb7, 0xbf, 0xc7, 0x3f, 0x28, 0x96, 0x38, 0x2a, 0x39, 0xce, 0xb3, 0xfb, 0x72, + 0x44, 0x09, 0x7e, 0x19, 0xbd, 0x07, 0x3a, 0x16, 0x14, 0x5a, 0x68, 0x2a, 0x7a, 0x9a, 0xaa, 0x89, 0x6c, 0xcd, 0x4b, + 0xc5, 0xb4, 0xcc, 0xa8, 0x11, 0xc3, 0x6c, 0x48, 0xe4, 0xd4, 0x56, 0x36, 0x2f, 0x77, 0x55, 0x6d, 0x5c, 0xb4, 0x05, + 0x8b, 0x55, 0x60, 0x71, 0xb9, 0x74, 0xea, 0xa8, 0x26, 0xcc, 0x88, 0x63, 0x20, 0xcc, 0x8c, 0x84, 0x8a, 0x9a, 0x66, + 0x2d, 0xdb, 0x38, 0x68, 0x35, 0x9f, 0x48, 0xeb, 0xe6, 0x35, 0x38, 0x4c, 0x17, 0x82, 0x6c, 0x6e, 0xfa, 0x14, 0xb0, + 0x9c, 0x5d, 0x39, 0x90, 0x81, 0xa1, 0x1f, 0xcb, 0x5c, 0xd9, 0x2a, 0xa9, 0x75, 0x03, 0x7e, 0xd1, 0x1d, 0xd9, 0xb2, + 0x0a, 0x75, 0xeb, 0xef, 0x8d, 0x5c, 0xa3, 0xa7, 0xe9, 0xb6, 0x5c, 0xa3, 0x9a, 0xb6, 0xbb, 0xd3, 0x46, 0x77, 0xe7, + 0xa5, 0xca, 0xb1, 0x36, 0x57, 0xf9, 0x0d, 0xc3, 0x75, 0x80, 0x36, 0x25, 0x9a, 0x35, 0x57, 0x39, 0x2d, 0x8a, 0x51, + 0x79, 0x9a, 0x40, 0xa4, 0xee, 0x8c, 0x24, 0xfd, 0x2b, 0xab, 0x51, 0x1c, 0xca, 0x75, 0xbe, 0x27, 0xe3, 0x38, 0xbd, + 0xf2, 0xe3, 0xf7, 0x30, 0x5e, 0xf5, 0xf2, 0xf9, 0x6d, 0x98, 0xf9, 0x9c, 0x2a, 0xee, 0x52, 0xc1, 0xf0, 0xbd, 0x01, + 0xc3, 0xf7, 0x92, 0x4f, 0x57, 0xed, 0xf1, 0xe2, 0x65, 0xd9, 0x81, 0x37, 0x2a, 0x34, 0xcb, 0xd8, 0xe5, 0x9b, 0xc7, + 0x58, 0x65, 0x61, 0xbb, 0x25, 0x0b, 0xdb, 0xe5, 0xce, 0x6a, 0x57, 0x8e, 0xf3, 0xc3, 0xe6, 0x5e, 0xd6, 0x39, 0xdb, + 0x0f, 0xd5, 0xc6, 0xff, 0xc1, 0xbb, 0xb3, 0x8d, 0xc1, 0xe5, 0xf6, 0xdd, 0x7d, 0x91, 0xac, 0x22, 0x41, 0x7e, 0x09, + 0x49, 0x07, 0x9c, 0xf4, 0x8d, 0x43, 0x07, 0x95, 0x9c, 0xd2, 0x79, 0x40, 0x4e, 0x30, 0xcb, 0x79, 0x3a, 0x51, 0x7d, + 0xe6, 0xea, 0xa4, 0x91, 0x78, 0x09, 0xae, 0x68, 0x11, 0x6b, 0xf7, 0xea, 0x67, 0xb9, 0x16, 0x1f, 0x59, 0x12, 0x7a, + 0x09, 0x56, 0x52, 0x24, 0xf7, 0xb2, 0x82, 0xe8, 0x6c, 0xe3, 0xf5, 0x77, 0x78, 0xc4, 0x12, 0x96, 0x47, 0x34, 0x73, + 0x52, 0xb4, 0xd8, 0x36, 0x58, 0x0a, 0x01, 0x19, 0x39, 0x18, 0xfe, 0x6b, 0x75, 0xea, 0xcf, 0x85, 0xde, 0xc0, 0x0f, + 0x34, 0xa1, 0x3c, 0x4a, 0x43, 0x48, 0x4b, 0x71, 0xc3, 0xf2, 0x50, 0xd3, 0xde, 0xde, 0x8e, 0x63, 0x0b, 0xb7, 0x04, + 0x1c, 0x00, 0x37, 0xdf, 0xa0, 0xc1, 0x02, 0xce, 0xe7, 0x54, 0x43, 0x53, 0xb4, 0xa0, 0xab, 0x47, 0x59, 0xb8, 0xfb, + 0x91, 0xde, 0xe2, 0x1c, 0x15, 0x85, 0x27, 0xa1, 0xb6, 0x47, 0x8c, 0xc6, 0xa1, 0x8d, 0x3f, 0xd2, 0x5b, 0xaf, 0x3c, + 0x33, 0x2e, 0x8e, 0x38, 0x8b, 0x05, 0xb4, 0xd3, 0x79, 0x62, 0xe3, 0x6a, 0x10, 0x6f, 0x51, 0xe0, 0x34, 0x63, 0x63, + 0x20, 0xce, 0x6f, 0xe8, 0xad, 0x27, 0xfb, 0x63, 0xc6, 0x79, 0x3d, 0xb4, 0xd0, 0xa8, 0x77, 0x8d, 0x62, 0x73, 0x19, + 0x94, 0x41, 0x71, 0x2e, 0xda, 0x0e, 0x49, 0xad, 0x5e, 0x65, 0x1e, 0x22, 0x54, 0xdc, 0x77, 0x2a, 0xf8, 0x1b, 0x53, + 0xb4, 0xf1, 0x5a, 0xe6, 0xeb, 0x4a, 0x23, 0x0a, 0x0d, 0xaa, 0x4c, 0x0f, 0xc8, 0xe8, 0x58, 0x68, 0xf6, 0x2a, 0x9a, + 0x1b, 0x8e, 0xb0, 0x6f, 0xb8, 0xea, 0xd4, 0xfb, 0xab, 0x4c, 0x08, 0xa9, 0x22, 0x49, 0x2f, 0xaa, 0x76, 0xd6, 0xad, + 0x03, 0x78, 0x87, 0x84, 0x16, 0x5f, 0x9c, 0xc9, 0x2c, 0x74, 0xb6, 0xe8, 0xdf, 0x38, 0x71, 0x16, 0x7a, 0x0a, 0x5e, + 0x6e, 0x62, 0x91, 0x17, 0x40, 0x85, 0x8a, 0xbe, 0x64, 0x02, 0x20, 0x1b, 0x39, 0x6c, 0x4d, 0x6a, 0x66, 0x42, 0x6a, + 0xba, 0x06, 0xc6, 0xb7, 0x48, 0x49, 0x2a, 0x90, 0x21, 0x94, 0x48, 0x21, 0xf4, 0xd4, 0xe2, 0x2a, 0x12, 0x32, 0x17, + 0xb4, 0x3c, 0x41, 0x27, 0xd7, 0x3c, 0xab, 0x81, 0xe5, 0x88, 0x7e, 0x50, 0xe1, 0xc1, 0x94, 0xa8, 0xac, 0x50, 0x68, + 0x77, 0x4e, 0xae, 0xd3, 0x5b, 0x9d, 0xd4, 0xd5, 0xd3, 0x22, 0x1a, 0x25, 0x4e, 0x84, 0x16, 0xb9, 0x13, 0xe1, 0x0c, + 0xd2, 0x11, 0xd3, 0xa2, 0x84, 0x9f, 0x9a, 0xab, 0x51, 0x4b, 0x56, 0xde, 0x7c, 0xca, 0x0f, 0x94, 0x79, 0x0e, 0x29, + 0x9a, 0x38, 0xd7, 0x3c, 0x25, 0x77, 0xc4, 0x71, 0x3b, 0x63, 0xd9, 0xbe, 0x57, 0x09, 0x3a, 0x0a, 0xb0, 0xbf, 0x71, + 0x67, 0x61, 0xcc, 0xc2, 0x3c, 0xd1, 0xad, 0x4e, 0xfd, 0xa9, 0x60, 0x5f, 0x95, 0x43, 0xea, 0x24, 0x64, 0x45, 0xe2, + 0xdc, 0x9d, 0x6a, 0xf9, 0xcb, 0x8c, 0x66, 0xb7, 0x67, 0x14, 0x52, 0x9d, 0x53, 0x38, 0xf0, 0x5b, 0x2d, 0x43, 0x95, + 0xa7, 0x3e, 0xc8, 0x84, 0xb2, 0x52, 0xd4, 0xcf, 0x01, 0xae, 0x9e, 0x12, 0x2c, 0x44, 0xb4, 0xd1, 0x70, 0xc4, 0xc8, + 0xdd, 0x42, 0xb7, 0x9e, 0x9f, 0xa4, 0x3d, 0x06, 0xfe, 0xb5, 0x0a, 0xd3, 0x2a, 0x58, 0x80, 0x53, 0xf3, 0x4c, 0xea, + 0x79, 0x32, 0x5c, 0xf5, 0xca, 0x40, 0x11, 0x84, 0xef, 0xb2, 0xed, 0x53, 0xdd, 0x94, 0x34, 0xbb, 0x7d, 0xaa, 0xb5, + 0xa0, 0x9f, 0x48, 0xf8, 0xc1, 0x6a, 0x9c, 0xf2, 0x04, 0x33, 0x2b, 0x0a, 0x54, 0x00, 0x78, 0x7f, 0xe9, 0x39, 0xce, + 0x5f, 0x54, 0xca, 0xa0, 0x0b, 0xb1, 0xd8, 0xb3, 0x38, 0xd5, 0x4c, 0xbc, 0x1a, 0xff, 0x2f, 0x6b, 0xe3, 0xff, 0xc5, + 0x38, 0x75, 0x0a, 0xa6, 0xd1, 0x38, 0xa1, 0xa1, 0x66, 0x9d, 0x48, 0x12, 0xa0, 0xd0, 0xdb, 0x32, 0x4e, 0x3e, 0x5e, + 0x7a, 0xa0, 0x71, 0x2d, 0x46, 0x69, 0xc2, 0x9b, 0x23, 0x7f, 0xc2, 0xe2, 0x5b, 0x6f, 0xc6, 0x9a, 0x93, 0x34, 0x49, + 0xf3, 0xa9, 0x1f, 0x50, 0x9c, 0xdf, 0xe6, 0x9c, 0x4e, 0x9a, 0x33, 0x86, 0x9f, 0xd3, 0xf8, 0x9a, 0x72, 0x16, 0xf8, + 0xd8, 0x3e, 0xc9, 0x98, 0x1f, 0x5b, 0xaf, 0xfd, 0x2c, 0x4b, 0xe7, 0x36, 0x7e, 0x97, 0x5e, 0xa5, 0x3c, 0xc5, 0x6f, + 0x6e, 0x6e, 0xc7, 0x34, 0xc1, 0x1f, 0xae, 0x66, 0x09, 0x9f, 0xe1, 0xdc, 0x4f, 0xf2, 0x66, 0x4e, 0x33, 0x36, 0xea, + 0x05, 0x69, 0x9c, 0x66, 0x4d, 0xc8, 0xd8, 0x9e, 0x50, 0x2f, 0x66, 0xe3, 0x88, 0x5b, 0xa1, 0x9f, 0x7d, 0xec, 0x35, + 0x9b, 0xd3, 0x8c, 0x4d, 0xfc, 0xec, 0xb6, 0x29, 0x6a, 0x78, 0x5f, 0xb6, 0xf7, 0xfd, 0xc7, 0xa3, 0x83, 0x1e, 0xcf, + 0xfc, 0x24, 0x67, 0xb0, 0x4c, 0x9e, 0x1f, 0xc7, 0xd6, 0xfe, 0x61, 0x7b, 0x92, 0xef, 0xc8, 0x40, 0x9e, 0x9f, 0xf0, + 0xe2, 0x12, 0xbf, 0x07, 0xb8, 0xdd, 0x2b, 0x9e, 0xe0, 0xab, 0x19, 0xe7, 0x69, 0xb2, 0x08, 0x66, 0x59, 0x9e, 0x66, + 0xde, 0x34, 0x65, 0x09, 0xa7, 0x59, 0xef, 0x2a, 0xcd, 0x42, 0x9a, 0x35, 0x33, 0x3f, 0x64, 0xb3, 0xdc, 0x3b, 0x98, + 0xde, 0xf4, 0x40, 0xb3, 0x18, 0x67, 0xe9, 0x2c, 0x09, 0xd5, 0x58, 0x2c, 0x89, 0x68, 0xc6, 0xb8, 0xf9, 0x42, 0x5c, + 0x64, 0xe2, 0xc5, 0x2c, 0xa1, 0x7e, 0xd6, 0x1c, 0x43, 0x63, 0x30, 0x8b, 0xda, 0x21, 0x1d, 0xe3, 0x6c, 0x7c, 0xe5, + 0x3b, 0x9d, 0xee, 0x23, 0xac, 0xff, 0x77, 0x0f, 0x91, 0xd5, 0xde, 0x5c, 0xdc, 0x69, 0xb7, 0xff, 0x84, 0x7a, 0x2b, + 0xa3, 0x08, 0x80, 0xbc, 0xce, 0xf4, 0xc6, 0xca, 0x53, 0xc8, 0x68, 0xdb, 0xd4, 0xb2, 0x37, 0xf5, 0x43, 0xc8, 0x07, + 0xf6, 0xba, 0xd3, 0x9b, 0x02, 0x66, 0xe7, 0xc9, 0x14, 0x53, 0x35, 0x49, 0xf5, 0xb4, 0xf8, 0xad, 0x10, 0x1f, 0x6d, + 0x86, 0xb8, 0xab, 0x21, 0xae, 0xb0, 0xde, 0x0c, 0x67, 0x99, 0x88, 0xad, 0x7a, 0x9d, 0x5c, 0x02, 0x12, 0xa5, 0xd7, + 0x34, 0xd3, 0x70, 0x88, 0x87, 0xdf, 0x0c, 0x46, 0x77, 0x33, 0x18, 0x47, 0x9f, 0x02, 0x23, 0x4b, 0xc2, 0x45, 0x7d, + 0x5d, 0x3b, 0x19, 0x9d, 0xf4, 0x22, 0x0a, 0xf4, 0xe4, 0x75, 0xe1, 0xf7, 0x9c, 0x85, 0x3c, 0x92, 0x3f, 0x05, 0x39, + 0xcf, 0xe5, 0xbb, 0xc3, 0x76, 0x5b, 0x3e, 0xe7, 0xec, 0x57, 0xea, 0x75, 0x5c, 0xa8, 0x50, 0x5c, 0xe2, 0x1f, 0xca, + 0xd3, 0xbc, 0x75, 0xee, 0x89, 0xff, 0x62, 0x1e, 0xf3, 0x35, 0x52, 0x14, 0xab, 0x43, 0xd1, 0x38, 0xd5, 0xb2, 0x52, + 0x0a, 0x1f, 0x70, 0xdb, 0x09, 0xee, 0x48, 0x58, 0xbf, 0x3c, 0xc6, 0xc9, 0x06, 0x7f, 0x91, 0x79, 0x17, 0x1e, 0x44, + 0x3a, 0x8c, 0x54, 0xc3, 0xb4, 0x97, 0xf5, 0x49, 0xbb, 0x97, 0x35, 0x9b, 0xc8, 0x49, 0x09, 0x9c, 0x16, 0x90, 0xc9, + 0x79, 0x0e, 0x1b, 0xa4, 0xc2, 0xd8, 0x4e, 0x90, 0x97, 0xc2, 0x59, 0xd3, 0xe5, 0x32, 0xa9, 0x12, 0x32, 0xc4, 0x69, + 0x8d, 0x1f, 0xb8, 0xaa, 0x80, 0x13, 0x83, 0x93, 0xfb, 0xfa, 0x7a, 0x97, 0x5c, 0xf3, 0x8a, 0x38, 0x0d, 0x04, 0xe6, + 0xdc, 0xa9, 0xcf, 0x23, 0xf0, 0x52, 0x94, 0xe2, 0xa7, 0x4a, 0x61, 0xb2, 0x5b, 0x36, 0x1a, 0xe4, 0x65, 0x7e, 0x1b, + 0xe4, 0xf1, 0xe5, 0x05, 0xf4, 0x72, 0xc5, 0x09, 0xf4, 0x58, 0xf5, 0xff, 0x81, 0x1b, 0x92, 0x3a, 0x77, 0x59, 0x12, + 0xc4, 0xb3, 0x90, 0xe6, 0xa2, 0x87, 0x4a, 0x9c, 0xc3, 0xdd, 0x10, 0x65, 0x2d, 0xd1, 0x04, 0x7a, 0x17, 0xd9, 0x3c, + 0x50, 0x11, 0x6e, 0x51, 0x29, 0x9f, 0x9b, 0xe2, 0xb9, 0x6a, 0xfb, 0xba, 0x4a, 0x16, 0x85, 0x96, 0xee, 0x2c, 0x61, + 0xbf, 0xcc, 0xe8, 0x05, 0x0b, 0x8d, 0x93, 0xbb, 0x34, 0x09, 0xd2, 0x90, 0x7e, 0x78, 0xf7, 0x02, 0xb2, 0xdd, 0xd3, + 0x04, 0x48, 0x4c, 0xf9, 0xbb, 0x70, 0x42, 0x40, 0x23, 0xbc, 0x66, 0x01, 0x1d, 0x5c, 0xee, 0x2e, 0x36, 0x56, 0x94, + 0xaf, 0x51, 0xd1, 0xba, 0x14, 0x49, 0x7f, 0x02, 0xca, 0xcb, 0xdd, 0xc5, 0x15, 0x2f, 0x5a, 0xbb, 0x8b, 0xdc, 0x0d, + 0xd3, 0x89, 0xcf, 0x12, 0xf8, 0x9d, 0x14, 0xbb, 0x0b, 0x06, 0x3f, 0x78, 0x71, 0x59, 0x54, 0x89, 0xa2, 0x25, 0x44, + 0xc6, 0x14, 0x14, 0xee, 0x3a, 0xc8, 0xfd, 0x39, 0x65, 0x89, 0x28, 0xba, 0xab, 0x67, 0xaa, 0x7b, 0x05, 0x24, 0xff, + 0x4a, 0xa4, 0xc1, 0xac, 0xcd, 0xe5, 0xd1, 0x7d, 0xcd, 0x65, 0x9a, 0x70, 0x26, 0xd2, 0xe2, 0x75, 0x38, 0x27, 0xf2, + 0xf3, 0x8b, 0x40, 0x9e, 0x44, 0xcd, 0xab, 0x53, 0x17, 0xbe, 0x40, 0xac, 0xb4, 0x80, 0x69, 0x26, 0x8c, 0x7d, 0xba, + 0xfd, 0xa8, 0x64, 0x7e, 0x97, 0xf1, 0x57, 0x52, 0x55, 0x9e, 0xce, 0xb2, 0x00, 0x62, 0xbd, 0x4a, 0xa5, 0x58, 0xf7, + 0x8a, 0xd9, 0x42, 0x7f, 0xb3, 0x31, 0x37, 0x92, 0x6c, 0x39, 0x9c, 0xe9, 0xab, 0xae, 0xed, 0xa0, 0x22, 0x9e, 0x08, + 0x6b, 0xc6, 0xc4, 0xea, 0x5d, 0xb0, 0x10, 0x02, 0x2f, 0x2c, 0x54, 0x09, 0x8b, 0xb5, 0x49, 0x82, 0x8a, 0x14, 0x8a, + 0x0c, 0x52, 0xb8, 0x6c, 0x27, 0xad, 0x56, 0x01, 0x84, 0x1f, 0xd2, 0x2e, 0xf9, 0x66, 0x67, 0x6f, 0x2f, 0xa9, 0x4e, + 0xb4, 0x31, 0x85, 0xf3, 0xe5, 0x92, 0x53, 0x27, 0x91, 0xa7, 0x6e, 0x22, 0x02, 0xca, 0x18, 0xc3, 0xf2, 0x8d, 0x97, + 0xe2, 0xb2, 0x27, 0x2f, 0x29, 0x7a, 0x91, 0x40, 0xa2, 0x44, 0x19, 0xd1, 0x48, 0x3d, 0xd1, 0x2a, 0x19, 0x36, 0x5f, + 0x97, 0x07, 0xf9, 0x6b, 0x58, 0x6f, 0xaf, 0x2c, 0x8e, 0xb4, 0xaa, 0xa2, 0xd5, 0xd2, 0x3c, 0xcd, 0xb8, 0xe3, 0xf8, + 0x38, 0x40, 0xa4, 0xef, 0x8b, 0xd9, 0x1f, 0xcb, 0x7c, 0x8f, 0x41, 0xb3, 0xe3, 0x75, 0x4a, 0x7f, 0x48, 0xed, 0x7c, + 0xb5, 0xcc, 0x36, 0x53, 0x67, 0x74, 0x01, 0x4f, 0xb8, 0xfc, 0xad, 0xd0, 0x57, 0x15, 0xc8, 0xd9, 0x55, 0xcf, 0xe5, + 0x24, 0xb1, 0x62, 0x68, 0x52, 0x19, 0x70, 0x6a, 0x50, 0x9d, 0x67, 0x43, 0xcc, 0xb6, 0x8c, 0x8d, 0x8a, 0x0a, 0x11, + 0xe5, 0xe6, 0xbe, 0x94, 0x4a, 0xd0, 0x85, 0x41, 0xdd, 0x97, 0x4c, 0xbb, 0xf1, 0xea, 0x74, 0x57, 0x28, 0x14, 0x19, + 0x9c, 0x61, 0x53, 0x35, 0x09, 0xcb, 0x2d, 0xc9, 0x37, 0x12, 0xaf, 0x2b, 0x1f, 0xa9, 0xa4, 0x8d, 0xcd, 0x55, 0x44, + 0x32, 0xe4, 0x26, 0xc0, 0xc0, 0x31, 0x90, 0x73, 0x3d, 0x05, 0xe0, 0x31, 0x23, 0x0a, 0x27, 0x95, 0x14, 0xc7, 0xc1, + 0x0b, 0xa9, 0xdd, 0x7b, 0xf6, 0xdb, 0x37, 0x67, 0xef, 0x6d, 0x0c, 0x57, 0x9d, 0xd1, 0x2c, 0xf7, 0x16, 0xb6, 0xca, + 0x31, 0x6c, 0x42, 0xbc, 0xda, 0xf6, 0x6c, 0x7f, 0x0a, 0x87, 0xb6, 0x05, 0x53, 0x6d, 0xdd, 0x34, 0xe7, 0xf3, 0x79, + 0x13, 0x4e, 0x94, 0x35, 0x67, 0x59, 0x2c, 0xd9, 0x4d, 0x68, 0x17, 0x05, 0x72, 0x79, 0x44, 0x93, 0xf2, 0x32, 0xa4, + 0x34, 0xa6, 0x6e, 0x9c, 0x8e, 0xe5, 0x79, 0xd8, 0x55, 0xf7, 0x44, 0x7c, 0x79, 0x2c, 0x2e, 0xf9, 0xea, 0x1f, 0x73, + 0x79, 0xbd, 0x1a, 0xcf, 0xe0, 0x67, 0x1f, 0x82, 0x57, 0xc7, 0x2d, 0x1e, 0x89, 0x87, 0x33, 0xd8, 0x4d, 0xe2, 0x69, + 0x77, 0xb1, 0x46, 0x75, 0x03, 0xe8, 0x22, 0xea, 0xcb, 0xa9, 0xe5, 0xa2, 0xd6, 0xa5, 0x17, 0x5f, 0x5e, 0x16, 0xc7, + 0x2d, 0xe8, 0xab, 0xa5, 0xfb, 0xbd, 0x4a, 0xc3, 0x5b, 0xdd, 0xbe, 0xa4, 0x44, 0xb8, 0xec, 0x29, 0x27, 0x7d, 0xe8, + 0x02, 0xc6, 0x0d, 0xfb, 0x02, 0x67, 0x8a, 0x85, 0x9e, 0x57, 0x0f, 0xc5, 0xd0, 0x02, 0x86, 0x59, 0x40, 0x09, 0x90, + 0x1b, 0x74, 0x1e, 0x96, 0x0d, 0xc4, 0x6e, 0x97, 0x45, 0xdb, 0x00, 0x94, 0x15, 0xab, 0xfd, 0x23, 0xdd, 0xdc, 0x15, + 0x59, 0x68, 0x88, 0x43, 0x13, 0xf8, 0x4b, 0x04, 0xff, 0x0a, 0xc0, 0x8f, 0x5b, 0x12, 0x4d, 0x97, 0xe6, 0xb5, 0x33, + 0xf2, 0x42, 0x88, 0x12, 0x99, 0xe7, 0x19, 0xc7, 0xef, 0x39, 0xfe, 0x78, 0x29, 0xaa, 0x6a, 0x2d, 0x01, 0xd4, 0x57, + 0xd0, 0xa6, 0xda, 0x5a, 0x1d, 0x0c, 0xd2, 0x38, 0xf6, 0xa7, 0x39, 0xf5, 0xf4, 0x0f, 0xa5, 0x30, 0x80, 0xde, 0xb1, + 0xae, 0xa1, 0xa9, 0xbc, 0xa7, 0x53, 0xd0, 0xe3, 0xd6, 0xd5, 0xc7, 0x6b, 0x3f, 0x73, 0x9a, 0xcd, 0xa0, 0x79, 0x35, + 0x46, 0x05, 0x8f, 0x16, 0xa6, 0xba, 0xf1, 0xb0, 0xdd, 0xee, 0x41, 0x92, 0x6a, 0xd3, 0x8f, 0xd9, 0x38, 0xf1, 0x62, + 0x3a, 0xe2, 0x05, 0x87, 0xd3, 0x83, 0x0b, 0xad, 0xdf, 0xb9, 0xdd, 0xc3, 0x8c, 0x4e, 0x2c, 0x17, 0xfe, 0xde, 0x3d, + 0x70, 0xc1, 0x43, 0x2f, 0xe1, 0x51, 0x53, 0x24, 0x43, 0xc3, 0x51, 0x0e, 0x1e, 0xd5, 0x9e, 0x17, 0xc6, 0x40, 0x01, + 0x05, 0xdd, 0xb7, 0xe0, 0x99, 0xc5, 0x23, 0xcc, 0x33, 0xb3, 0x5e, 0x82, 0x16, 0x6b, 0x33, 0x58, 0x57, 0xc1, 0xf6, + 0x51, 0x91, 0x0b, 0x8b, 0x65, 0xb1, 0x86, 0x17, 0x43, 0x95, 0x2e, 0x58, 0x32, 0x9d, 0xf1, 0x73, 0xe1, 0xf9, 0xcf, + 0xe0, 0x0c, 0xc9, 0x10, 0x1b, 0x25, 0x00, 0xcf, 0x50, 0xb5, 0x0f, 0xfc, 0x38, 0x70, 0xa0, 0x13, 0xab, 0x69, 0x1d, + 0x65, 0x74, 0x82, 0x7a, 0x13, 0x96, 0x34, 0xe5, 0xbb, 0x43, 0x43, 0x77, 0x73, 0x1f, 0xc1, 0x53, 0xe1, 0x8a, 0xde, + 0xb0, 0x48, 0xf0, 0xdd, 0x30, 0xaf, 0xcb, 0x61, 0x51, 0xf4, 0x52, 0xee, 0x9c, 0xbf, 0x70, 0xd0, 0x10, 0xff, 0x6a, + 0x5c, 0x62, 0x63, 0x6b, 0xaa, 0xb6, 0x71, 0x17, 0x6d, 0xa9, 0x62, 0xd2, 0xa5, 0xa8, 0xf6, 0x2b, 0x81, 0x8a, 0x2f, + 0x1d, 0x9b, 0xe6, 0xd3, 0xa6, 0x64, 0x3f, 0x4d, 0x41, 0x3e, 0x36, 0x34, 0x45, 0xca, 0x9d, 0x4d, 0xe9, 0x42, 0x70, + 0x16, 0x75, 0x8e, 0x45, 0x7a, 0x5c, 0x86, 0xe5, 0xb9, 0x27, 0xf5, 0x6c, 0x9e, 0x74, 0x42, 0xb5, 0xad, 0x7f, 0x79, + 0x52, 0x67, 0x53, 0x20, 0xff, 0xcb, 0xbb, 0xfe, 0xfc, 0x38, 0x86, 0x01, 0x2f, 0xb5, 0xd2, 0x60, 0x5e, 0x8d, 0x72, + 0xce, 0x87, 0x0e, 0x2a, 0xd4, 0x9e, 0x79, 0x22, 0xf4, 0x6e, 0xe3, 0x82, 0xc1, 0x1d, 0xae, 0x23, 0x6a, 0xf2, 0x04, + 0x33, 0x83, 0x9c, 0x80, 0x5a, 0xee, 0x78, 0xaf, 0x62, 0x33, 0x52, 0x6b, 0xb7, 0xc4, 0x84, 0x88, 0x9d, 0x25, 0xa1, + 0x6d, 0xfd, 0x39, 0x88, 0x59, 0xf0, 0x91, 0xd8, 0xbb, 0x0b, 0x07, 0xad, 0x1f, 0x0d, 0x15, 0x3b, 0x54, 0xf3, 0x5c, + 0x54, 0x8f, 0x36, 0x64, 0xae, 0xc1, 0x4e, 0xe5, 0xed, 0x41, 0x76, 0x1f, 0x54, 0x9b, 0xe3, 0x96, 0x1c, 0xa7, 0x7f, + 0x59, 0x5c, 0x54, 0xb7, 0x82, 0x55, 0x50, 0x00, 0x9a, 0x65, 0xb9, 0x25, 0xe8, 0x8f, 0xd8, 0x72, 0x0b, 0xd5, 0x2c, + 0x40, 0x6c, 0xd2, 0x3e, 0xb2, 0x2d, 0xc9, 0x60, 0x00, 0x4e, 0xae, 0x78, 0x8d, 0x6d, 0xfd, 0xb9, 0x2c, 0xa3, 0xa5, + 0xdb, 0x47, 0xe4, 0xad, 0x10, 0x1b, 0xc6, 0x02, 0x5b, 0xdf, 0x0d, 0x29, 0xf7, 0x59, 0x2c, 0x9b, 0xf4, 0xb4, 0x97, + 0x62, 0x65, 0x46, 0xcb, 0x65, 0x52, 0x9f, 0x0b, 0xab, 0x63, 0x50, 0xcc, 0xec, 0xb8, 0x55, 0xc1, 0x2d, 0x66, 0x26, + 0xf6, 0x87, 0x19, 0x3f, 0xad, 0x66, 0x28, 0xdf, 0x59, 0x7f, 0x0e, 0xc4, 0xc9, 0x2a, 0x00, 0x30, 0x55, 0x00, 0x42, + 0x64, 0x5f, 0x2a, 0x21, 0x8e, 0x4f, 0x52, 0x97, 0xfb, 0xd9, 0x98, 0xf2, 0x15, 0xc4, 0xfa, 0x32, 0x91, 0xb7, 0xa7, + 0xa3, 0xf8, 0x6b, 0xd0, 0x06, 0x75, 0x68, 0x41, 0xcf, 0x2d, 0x06, 0xa0, 0xaa, 0x92, 0x8d, 0x1a, 0x6f, 0x84, 0x40, + 0xf6, 0x89, 0xc5, 0x49, 0x04, 0xb7, 0x4f, 0x05, 0xb7, 0x97, 0x71, 0x38, 0x4b, 0x8c, 0x25, 0x40, 0x2c, 0x6c, 0x6b, + 0x20, 0x21, 0xa7, 0xa1, 0x84, 0x99, 0x64, 0xa2, 0x55, 0x5a, 0x1c, 0xb7, 0x64, 0x6d, 0xc9, 0x8e, 0x65, 0x25, 0x40, + 0x82, 0xd8, 0xa7, 0x15, 0x0e, 0x20, 0xf9, 0xdb, 0xc4, 0x43, 0xc8, 0xae, 0x4b, 0x62, 0x13, 0x67, 0xcc, 0xfa, 0xc7, + 0xb1, 0x7f, 0x45, 0xe3, 0xfe, 0xee, 0x22, 0x5b, 0x2e, 0xdb, 0xc5, 0x71, 0x4b, 0x3e, 0x5a, 0xc7, 0x82, 0x6f, 0xc8, + 0xbb, 0x41, 0xc5, 0x12, 0xc3, 0xc1, 0x4d, 0x48, 0x89, 0xd5, 0xb9, 0x60, 0x9e, 0xea, 0xa0, 0xb0, 0x2d, 0x91, 0x85, + 0x22, 0x2a, 0x95, 0x3a, 0x4d, 0x61, 0x5b, 0x2c, 0x5c, 0x2f, 0xcb, 0x39, 0x9d, 0x42, 0x69, 0xb4, 0x5c, 0x76, 0x0a, + 0xdb, 0x9a, 0xb0, 0x04, 0x9e, 0xb2, 0xe5, 0x52, 0x9c, 0x89, 0x9c, 0xb0, 0xc4, 0x69, 0x03, 0xd9, 0xda, 0xd6, 0xc4, + 0xbf, 0x11, 0x13, 0xd6, 0x6f, 0xfc, 0x1b, 0xa7, 0xa3, 0x5e, 0xb9, 0x25, 0x7e, 0x12, 0xa0, 0xb8, 0x6a, 0x45, 0x7d, + 0xb5, 0xa2, 0x21, 0x9e, 0xc9, 0xd3, 0x5e, 0xc4, 0x09, 0x89, 0xbf, 0x79, 0x45, 0x43, 0xbd, 0xa2, 0xb3, 0x2d, 0x2b, + 0x3a, 0xbb, 0x63, 0x45, 0x03, 0xb5, 0x7a, 0x56, 0x89, 0xbb, 0x74, 0xb9, 0xec, 0xb4, 0x2b, 0xec, 0x1d, 0xb7, 0x42, + 0x76, 0x0d, 0xab, 0x01, 0x9a, 0x1a, 0x67, 0x13, 0xba, 0x99, 0x28, 0xeb, 0x28, 0xa6, 0x9f, 0x85, 0xc9, 0x0a, 0x0b, + 0x59, 0x1d, 0x0b, 0x26, 0x5d, 0x97, 0x81, 0xc9, 0x3f, 0x92, 0xb2, 0x19, 0xe0, 0x21, 0x01, 0x3c, 0x44, 0xfa, 0xae, + 0x50, 0xc7, 0x7e, 0x6f, 0x63, 0xdb, 0xb2, 0x35, 0x59, 0x5f, 0x16, 0x17, 0x20, 0x23, 0xc4, 0xfc, 0xee, 0x45, 0x8b, + 0x50, 0xdb, 0xee, 0x6f, 0xa7, 0x39, 0xc8, 0x21, 0x98, 0xa7, 0x59, 0x68, 0x7b, 0xb2, 0xea, 0x67, 0xa1, 0x6a, 0xc2, + 0x12, 0x95, 0x91, 0xb6, 0x95, 0xd6, 0xaa, 0xf7, 0x26, 0xc5, 0x75, 0x0f, 0x0f, 0x65, 0x8d, 0xa9, 0xcf, 0x39, 0xcd, + 0x12, 0x45, 0xb9, 0xb6, 0xfd, 0x1f, 0x82, 0x0a, 0x37, 0xf0, 0x95, 0x40, 0x2f, 0x80, 0x26, 0x40, 0xa5, 0x73, 0x2b, + 0x9e, 0x2f, 0xc5, 0xd3, 0x4e, 0xa5, 0x6c, 0xde, 0x22, 0x53, 0xef, 0x97, 0x45, 0x60, 0x86, 0xcc, 0x26, 0x34, 0xbc, + 0x10, 0x0c, 0x7a, 0x10, 0x5f, 0x2a, 0xe5, 0x71, 0x45, 0xdc, 0x55, 0x0d, 0xb0, 0xfd, 0xd3, 0xac, 0xfb, 0xe8, 0xe0, + 0xd4, 0xc6, 0x92, 0xc7, 0xa7, 0xa3, 0x91, 0x8d, 0x0a, 0xeb, 0x7e, 0xcd, 0x3a, 0x07, 0x3f, 0xcd, 0xbe, 0x7e, 0xd6, + 0xfe, 0xba, 0x6c, 0x9c, 0x00, 0x11, 0xa9, 0x24, 0x08, 0x2d, 0xaa, 0x0c, 0x78, 0xf5, 0x8c, 0x46, 0x7e, 0xb2, 0x7d, + 0x3a, 0xe7, 0xe6, 0x74, 0xf2, 0x29, 0xa5, 0x21, 0x10, 0x27, 0x5e, 0x2b, 0xbd, 0x88, 0xe9, 0x35, 0xd5, 0x37, 0x34, + 0x6e, 0x18, 0x6c, 0x43, 0x8b, 0x20, 0x9d, 0x25, 0x5c, 0x65, 0x83, 0x28, 0x56, 0x6b, 0x4c, 0xe9, 0x52, 0xcc, 0xc1, + 0x54, 0xe7, 0x6f, 0xa5, 0x9c, 0xab, 0x4b, 0xaf, 0xe2, 0x12, 0xdb, 0x06, 0x00, 0x5b, 0x21, 0x1b, 0x6c, 0x29, 0xf7, + 0xda, 0xb8, 0xbd, 0x0d, 0x36, 0xdc, 0x41, 0x9e, 0x6d, 0x0f, 0x35, 0x9e, 0x84, 0x43, 0xb7, 0x76, 0xa9, 0xc6, 0x56, + 0x7c, 0x7d, 0x12, 0x03, 0x57, 0x19, 0x74, 0x96, 0xd0, 0x3c, 0xdf, 0x8a, 0x80, 0x72, 0x11, 0xb1, 0x5d, 0xd5, 0xb6, + 0xb7, 0xf4, 0x82, 0xdb, 0x18, 0x76, 0x98, 0x00, 0xb8, 0x56, 0x45, 0xa8, 0x1b, 0x17, 0x70, 0xee, 0xe9, 0x3e, 0x03, + 0x55, 0xb5, 0xb7, 0xf5, 0x82, 0x3b, 0x87, 0x07, 0x78, 0xff, 0x51, 0x5b, 0x0d, 0xa5, 0x23, 0xd8, 0xaa, 0x1e, 0x1d, + 0x8d, 0x68, 0x50, 0xba, 0xde, 0x21, 0x16, 0x39, 0x62, 0x31, 0x87, 0x90, 0x9c, 0x88, 0x95, 0xd9, 0xaf, 0xd3, 0x84, + 0xda, 0x48, 0x67, 0xd7, 0x2a, 0x54, 0x29, 0x55, 0x63, 0x33, 0x44, 0xb2, 0xc7, 0x3a, 0x34, 0x6a, 0x94, 0xe5, 0x52, + 0x7b, 0x86, 0x6a, 0xe5, 0xf5, 0x35, 0x4b, 0x85, 0xeb, 0x67, 0xdb, 0x5e, 0xbd, 0xdf, 0x8e, 0x5c, 0x74, 0xbe, 0x3e, + 0xec, 0xb4, 0x0b, 0x1b, 0xdb, 0xd0, 0xdd, 0x7d, 0x37, 0xa4, 0x68, 0xb5, 0x0f, 0xad, 0x66, 0xc9, 0xe7, 0xb4, 0xeb, + 0x76, 0x1e, 0x77, 0x6c, 0x2c, 0xaf, 0x75, 0x40, 0x45, 0xc9, 0x77, 0x02, 0x70, 0x46, 0xff, 0xee, 0xa9, 0xd4, 0x3b, + 0xbf, 0x1f, 0x3c, 0x0f, 0x3b, 0x6d, 0x1b, 0xdb, 0x39, 0x4f, 0xa7, 0x9f, 0x31, 0x85, 0x7d, 0xa0, 0xa6, 0x38, 0xcd, + 0xa9, 0x39, 0x07, 0xa9, 0x39, 0xff, 0xfe, 0x49, 0x48, 0x88, 0xa6, 0x19, 0xcd, 0x73, 0xcb, 0xec, 0x5f, 0x91, 0xd2, + 0x27, 0x78, 0xf3, 0x46, 0x8a, 0xcb, 0x29, 0x17, 0x78, 0x91, 0x37, 0x2e, 0x98, 0x54, 0x25, 0xcb, 0xd6, 0x88, 0x4d, + 0x48, 0x9b, 0x92, 0x87, 0x4a, 0x45, 0xee, 0x93, 0x23, 0x6f, 0xd8, 0x7c, 0x72, 0x60, 0x19, 0xa3, 0x5f, 0x1f, 0xa0, + 0x56, 0x32, 0x61, 0xc9, 0xc5, 0x86, 0x52, 0xff, 0x66, 0x43, 0x29, 0x68, 0x87, 0x25, 0x74, 0xea, 0x36, 0xa0, 0x4f, + 0x63, 0xbd, 0xd2, 0xb1, 0x4c, 0x10, 0x43, 0xe1, 0xea, 0xfc, 0x04, 0xa4, 0xc6, 0x32, 0x88, 0x1e, 0x7e, 0xfb, 0x70, + 0x50, 0xf2, 0x39, 0xc3, 0x95, 0xbd, 0xfc, 0xbe, 0x19, 0x42, 0x69, 0x13, 0xe2, 0x09, 0xf1, 0x67, 0xcd, 0x95, 0xde, + 0x7c, 0x9a, 0xe0, 0x0c, 0x05, 0xee, 0x77, 0x2c, 0xbd, 0xba, 0x55, 0x60, 0x75, 0xed, 0x37, 0x14, 0x2b, 0x1d, 0xab, + 0x5c, 0xff, 0x20, 0x66, 0x93, 0x8a, 0x04, 0xd6, 0xc1, 0x14, 0xca, 0x15, 0x24, 0x97, 0x99, 0x9d, 0x48, 0x2d, 0x4b, + 0x30, 0x7d, 0xb8, 0x95, 0x64, 0x96, 0xd1, 0x8b, 0x38, 0x9d, 0xaf, 0xde, 0xb3, 0xb6, 0xbd, 0x72, 0xc4, 0xc6, 0x91, + 0x71, 0x0e, 0x8e, 0x92, 0x72, 0x11, 0xee, 0x1c, 0xa0, 0xf8, 0x97, 0x7f, 0x76, 0xdd, 0x7f, 0xf9, 0xe7, 0x4f, 0x56, + 0x85, 0xee, 0x8b, 0x4b, 0xcc, 0xab, 0x6e, 0xb7, 0xef, 0xae, 0xcd, 0x23, 0xd5, 0x71, 0xbe, 0xb9, 0xce, 0xda, 0x22, + 0x08, 0x19, 0xb8, 0xba, 0x04, 0x6b, 0x85, 0x72, 0xf7, 0x59, 0xbf, 0x05, 0x30, 0x98, 0xd7, 0x27, 0x21, 0x83, 0x4a, + 0xbf, 0x0b, 0xb4, 0x4b, 0xe4, 0xdd, 0x6b, 0x45, 0x7e, 0x3b, 0x86, 0x3f, 0x35, 0x87, 0xdf, 0x09, 0xbe, 0x72, 0x85, + 0xc4, 0x97, 0x97, 0x65, 0xc2, 0xa3, 0xd9, 0x14, 0xae, 0x53, 0x18, 0xac, 0x95, 0x28, 0xc5, 0xc3, 0x6b, 0xa3, 0xbe, + 0x38, 0xae, 0x49, 0xe2, 0xcb, 0x57, 0x70, 0x87, 0xd2, 0xf1, 0x55, 0xa6, 0xfd, 0xba, 0x77, 0x08, 0x07, 0xe8, 0xa2, + 0x3e, 0x2b, 0xd1, 0xe9, 0x9a, 0x64, 0x80, 0x52, 0xb0, 0x6c, 0x00, 0x4c, 0x1c, 0x5f, 0x2a, 0xc3, 0xf6, 0x54, 0x7a, + 0x7c, 0xbc, 0x55, 0xd2, 0x56, 0x9e, 0xa0, 0x1a, 0xd2, 0xb1, 0xf5, 0x5e, 0xe0, 0x4b, 0x54, 0xa6, 0x95, 0x23, 0x41, + 0x78, 0xd5, 0xc0, 0x64, 0x29, 0xd9, 0xcf, 0xb5, 0x1f, 0x5f, 0xdf, 0x8f, 0xf1, 0x6d, 0x17, 0xa8, 0x4b, 0x6b, 0xf9, + 0x8f, 0x56, 0x09, 0x96, 0xcd, 0xe5, 0x26, 0x7d, 0x60, 0xee, 0x73, 0x9a, 0x5d, 0x44, 0x90, 0x73, 0x95, 0x7d, 0x82, + 0x39, 0xc1, 0x4a, 0x63, 0x2a, 0xfe, 0x32, 0xa2, 0xee, 0xac, 0xfe, 0x07, 0x71, 0x2a, 0x06, 0x09, 0x93, 0x30, 0x94, + 0xb1, 0x08, 0xff, 0x9f, 0x6f, 0xfd, 0x87, 0xe1, 0x5b, 0x77, 0x0f, 0x51, 0x3b, 0x8e, 0xfd, 0xd9, 0x0b, 0xf9, 0x1f, + 0x9b, 0xdd, 0x25, 0x82, 0xdd, 0xfd, 0x06, 0x46, 0x97, 0xfc, 0x63, 0x18, 0x9d, 0x30, 0xc7, 0x35, 0xa7, 0x5b, 0x8b, + 0x6a, 0xdf, 0xba, 0xfe, 0xdc, 0xbf, 0xad, 0xf6, 0x55, 0x7c, 0x79, 0x32, 0xf7, 0x6f, 0xab, 0x45, 0xd8, 0xce, 0x2e, + 0x56, 0xfb, 0x18, 0xd8, 0x6f, 0x5e, 0xdb, 0x9e, 0xfd, 0xe6, 0xeb, 0xaf, 0x6d, 0x7c, 0x99, 0x53, 0x3e, 0x80, 0x42, + 0xb2, 0xbb, 0xd8, 0x59, 0xad, 0x08, 0x1e, 0x1b, 0x98, 0xa2, 0x88, 0xb0, 0x41, 0x7e, 0xa3, 0xf1, 0x9e, 0xe5, 0x17, + 0x69, 0x62, 0x42, 0xf3, 0x16, 0x9c, 0x08, 0x9f, 0x0b, 0x8e, 0xe8, 0x65, 0x0d, 0x1e, 0x51, 0xba, 0x0a, 0x90, 0x28, + 0xac, 0x41, 0x54, 0xdd, 0x4e, 0x74, 0x37, 0xff, 0xaf, 0x6e, 0x60, 0x90, 0x17, 0x8b, 0x44, 0x83, 0xf8, 0xf2, 0x73, + 0xc4, 0x87, 0x1c, 0xac, 0x72, 0x0e, 0x6a, 0xcf, 0xaa, 0x5f, 0xec, 0x2e, 0xa2, 0xbd, 0x3d, 0x36, 0xb0, 0xb1, 0xb8, + 0x12, 0xaa, 0xd8, 0x24, 0x5c, 0x12, 0xf8, 0x93, 0xc1, 0x9f, 0xb4, 0x62, 0xd4, 0x2c, 0x19, 0x65, 0x7e, 0x46, 0xc3, + 0xed, 0x4c, 0xba, 0xbc, 0x4a, 0x49, 0x91, 0x86, 0xcc, 0xf5, 0xce, 0x2f, 0x44, 0x96, 0xd3, 0x84, 0x81, 0x3e, 0xba, + 0x63, 0x7e, 0x30, 0x48, 0xdd, 0xbd, 0x56, 0x7e, 0x6f, 0xc0, 0x44, 0x38, 0x25, 0x49, 0x99, 0x56, 0x01, 0x17, 0x78, + 0xaa, 0x44, 0x14, 0x6c, 0x23, 0xe1, 0xe0, 0x0f, 0x49, 0x5f, 0x64, 0x58, 0xbc, 0x48, 0xb8, 0x13, 0xba, 0x3c, 0x63, + 0x13, 0x07, 0xe1, 0x4e, 0x1b, 0x21, 0xed, 0x6c, 0x08, 0x49, 0x7f, 0x87, 0xe5, 0xaf, 0xfd, 0xd7, 0x4e, 0x28, 0xee, + 0xfc, 0x12, 0x5f, 0x09, 0x82, 0xf3, 0x98, 0x4f, 0x66, 0xa3, 0x11, 0xcd, 0x1c, 0x7d, 0xd6, 0xf0, 0xab, 0x03, 0x38, + 0xce, 0x0c, 0x6f, 0x9f, 0xfa, 0xdc, 0xff, 0x96, 0xd1, 0xb9, 0x93, 0xa2, 0x5e, 0x56, 0xdd, 0x03, 0x19, 0xe2, 0x19, + 0x22, 0xfd, 0x08, 0x72, 0xf0, 0x5f, 0x24, 0x7c, 0xbf, 0xeb, 0xcc, 0xbe, 0x3a, 0xc0, 0x21, 0xdc, 0xae, 0xa1, 0x13, + 0xc8, 0xe5, 0xb5, 0x28, 0x1f, 0x58, 0xc2, 0x8f, 0xe4, 0x89, 0xcf, 0x14, 0x29, 0x4f, 0x65, 0x99, 0x7c, 0x63, 0xf9, + 0x65, 0x87, 0x21, 0xe9, 0x07, 0x0d, 0x22, 0xcf, 0x7f, 0x8a, 0x0b, 0x7d, 0x4f, 0x23, 0x3f, 0x3b, 0x85, 0xb3, 0xe5, + 0x00, 0xe8, 0x15, 0x4f, 0x7d, 0x27, 0x28, 0x3f, 0x1a, 0xe5, 0xb4, 0x7e, 0x6a, 0xb4, 0xc6, 0x58, 0xe4, 0xdf, 0x54, + 0x45, 0x2d, 0x28, 0xba, 0x30, 0x8b, 0x48, 0x63, 0xb7, 0x85, 0x61, 0x0f, 0xf6, 0x36, 0xba, 0x83, 0xf5, 0xd2, 0x35, + 0xe7, 0x99, 0x3f, 0x2d, 0x43, 0x14, 0xa7, 0x7e, 0x96, 0x31, 0x9a, 0x59, 0xce, 0xf3, 0x5f, 0x91, 0xf7, 0x2f, 0xff, + 0xbc, 0x39, 0x54, 0xa1, 0xa2, 0x13, 0x16, 0xe4, 0xb1, 0x34, 0x45, 0xe6, 0x37, 0xb1, 0x03, 0xd9, 0xd0, 0xd6, 0x91, + 0x95, 0xfd, 0xa3, 0x76, 0xbb, 0xad, 0xa2, 0x0f, 0x1d, 0xf9, 0x13, 0xc2, 0x0d, 0xf0, 0x13, 0x1e, 0x44, 0x00, 0x9b, + 0xd8, 0x32, 0x16, 0x7a, 0xd4, 0x9e, 0xde, 0xd8, 0x7d, 0xd8, 0x0e, 0x0a, 0x8a, 0x77, 0x74, 0x4a, 0x7d, 0xfe, 0x59, + 0xe3, 0x67, 0xa2, 0x49, 0x39, 0x7c, 0x47, 0x0f, 0x5d, 0x8d, 0xbb, 0x32, 0xe8, 0xe1, 0xea, 0xa0, 0xef, 0xd9, 0x44, + 0xdc, 0x12, 0xb5, 0x6d, 0x54, 0xe1, 0x14, 0xaf, 0x8d, 0xc9, 0x65, 0x0b, 0xdb, 0x12, 0x18, 0x8f, 0xd2, 0x38, 0xa4, + 0x19, 0xb1, 0xa9, 0x3b, 0x76, 0xad, 0xc7, 0xed, 0x76, 0x1b, 0x37, 0x0f, 0x0e, 0xdb, 0x6d, 0x7c, 0xf8, 0xb0, 0x8d, + 0x9b, 0xf0, 0xc7, 0x75, 0xdd, 0x15, 0x18, 0xee, 0x0a, 0x10, 0x77, 0xda, 0x19, 0x9d, 0x28, 0x00, 0xef, 0x8c, 0x60, + 0x56, 0x7b, 0x02, 0xee, 0xb2, 0x56, 0xfb, 0x5e, 0x4a, 0x36, 0x75, 0x97, 0x82, 0xca, 0x7c, 0x15, 0xae, 0xc9, 0xb4, + 0x8a, 0xcf, 0x52, 0x79, 0xc7, 0xe0, 0x0b, 0x45, 0x08, 0x9e, 0x75, 0x0a, 0x17, 0xa5, 0x8a, 0xd0, 0x2c, 0x64, 0x1d, + 0xc1, 0xb7, 0xd8, 0xb8, 0xcf, 0x12, 0xf8, 0x4c, 0x97, 0x0e, 0xd0, 0x6a, 0x46, 0x95, 0xae, 0xe4, 0xf7, 0x3e, 0x90, + 0x11, 0xf0, 0x4d, 0x04, 0x31, 0x7c, 0x80, 0xb0, 0x7f, 0x9f, 0x06, 0x6a, 0x05, 0xa1, 0x7e, 0x70, 0x9f, 0xfa, 0x1a, + 0xfb, 0xc3, 0x07, 0x22, 0x0f, 0x6a, 0x27, 0x5a, 0x2e, 0x77, 0xfc, 0xe5, 0x72, 0x27, 0xb8, 0xff, 0x0c, 0xe5, 0xf2, + 0xea, 0x03, 0x17, 0x70, 0xc9, 0xa8, 0x04, 0xfa, 0x05, 0x94, 0x7b, 0x11, 0x96, 0x20, 0xc9, 0x27, 0x1f, 0xab, 0x01, + 0xe5, 0x63, 0x50, 0xac, 0x20, 0x25, 0x24, 0x91, 0xb4, 0xcf, 0x97, 0x4b, 0x45, 0xfc, 0x78, 0x46, 0xfc, 0xb2, 0xa8, + 0x63, 0xe3, 0x29, 0x09, 0xca, 0x47, 0x5b, 0x80, 0x3c, 0x55, 0x5c, 0xaa, 0x82, 0x78, 0xee, 0x67, 0x89, 0x09, 0xf0, + 0xeb, 0xd4, 0x52, 0xc3, 0x5a, 0xd3, 0x2c, 0xbd, 0x66, 0x90, 0x67, 0xb3, 0x32, 0xf0, 0x84, 0xc0, 0x1d, 0x63, 0x3d, + 0x33, 0xea, 0x6e, 0x74, 0xf0, 0x5e, 0xf3, 0x59, 0xb8, 0xd0, 0xb2, 0x9c, 0xa0, 0x17, 0xaa, 0xb9, 0x79, 0x33, 0x3d, + 0xad, 0x77, 0xfe, 0xdc, 0x9b, 0xea, 0x87, 0x67, 0x32, 0xa5, 0xc7, 0x9b, 0x94, 0x87, 0x78, 0xde, 0x92, 0xd7, 0x10, + 0x66, 0xb2, 0x35, 0xdf, 0x86, 0x2b, 0x3d, 0x25, 0x8f, 0x7b, 0xf7, 0xf2, 0x8c, 0xfa, 0x59, 0x10, 0xbd, 0xf5, 0x33, + 0x7f, 0x92, 0xf7, 0x2e, 0xf4, 0x85, 0x61, 0x9a, 0x02, 0x2e, 0x46, 0x22, 0xa9, 0x2a, 0x09, 0x6e, 0x6d, 0x1c, 0x22, + 0x5c, 0xbd, 0x97, 0x10, 0x48, 0x97, 0xba, 0x8d, 0x67, 0xe6, 0x2b, 0x58, 0x67, 0x1b, 0x4f, 0x10, 0x96, 0xb9, 0x4a, + 0x6f, 0xff, 0xc8, 0x2c, 0x25, 0x0c, 0x69, 0x35, 0xde, 0x85, 0x5b, 0x7d, 0x50, 0x4f, 0xe7, 0x2d, 0xbd, 0x5f, 0xc9, + 0x5b, 0xda, 0x80, 0x46, 0x2b, 0xa3, 0xf9, 0x34, 0x4d, 0x72, 0x6a, 0xe3, 0xf7, 0xd0, 0x4e, 0xde, 0xfa, 0x6c, 0x36, + 0x5c, 0xa3, 0xb9, 0xb2, 0xa9, 0x78, 0x23, 0xdb, 0x41, 0xfc, 0xe8, 0xfd, 0xf7, 0x65, 0xca, 0x80, 0x0e, 0x25, 0x89, + 0x9c, 0x77, 0x46, 0xb7, 0xa4, 0xe5, 0x26, 0xf4, 0x93, 0x69, 0xb9, 0xf1, 0xbd, 0xd2, 0x72, 0x13, 0xfa, 0x47, 0xa7, + 0xe5, 0x32, 0x6a, 0xa4, 0xe5, 0x82, 0x9c, 0xfb, 0xfa, 0x5e, 0xd9, 0x9d, 0x3a, 0xe9, 0x2e, 0x9d, 0xe7, 0xa4, 0xa3, + 0xc2, 0x2d, 0x71, 0x3a, 0x86, 0xd4, 0xce, 0x7f, 0x7c, 0xa6, 0x66, 0x9c, 0x8e, 0xcd, 0x3c, 0x4d, 0xf8, 0x06, 0x0a, + 0x90, 0x1d, 0xce, 0xc8, 0xc2, 0xfe, 0xe9, 0xa6, 0xf3, 0xe4, 0xbc, 0xd3, 0xdb, 0xef, 0x4c, 0x6c, 0xcf, 0x06, 0xa7, + 0xa3, 0x28, 0x68, 0xf7, 0xf6, 0xf7, 0xa1, 0x60, 0x6e, 0x14, 0x74, 0xa1, 0x80, 0x19, 0x05, 0x87, 0x50, 0x10, 0x18, + 0x05, 0x0f, 0xa1, 0x20, 0x34, 0x0a, 0x1e, 0x41, 0xc1, 0xb5, 0x5d, 0x9c, 0xb3, 0x32, 0xf7, 0xf8, 0x11, 0x12, 0x97, + 0x25, 0xee, 0x64, 0xf5, 0x83, 0xe2, 0x11, 0xd1, 0x55, 0x1e, 0x95, 0x97, 0x4c, 0x34, 0x0f, 0xf4, 0x9d, 0x88, 0x97, + 0x5f, 0x5c, 0x02, 0x6b, 0x85, 0x3b, 0x5f, 0x30, 0x84, 0x3f, 0x65, 0xcd, 0x7d, 0xfd, 0xda, 0xf6, 0xca, 0x04, 0xdd, + 0x36, 0xee, 0xea, 0x14, 0x5d, 0xcf, 0x46, 0x82, 0x2f, 0xc9, 0x17, 0x87, 0x8d, 0x50, 0x75, 0x0b, 0xd7, 0x0d, 0x56, + 0x77, 0x7d, 0xee, 0x23, 0x3c, 0xd1, 0x0a, 0x10, 0x75, 0xe0, 0x5b, 0x0f, 0xef, 0xd9, 0x84, 0xea, 0xfd, 0xa2, 0x07, + 0xb0, 0x44, 0x12, 0x73, 0x2f, 0xaa, 0x14, 0xa3, 0xb7, 0xf8, 0xa2, 0xba, 0x5e, 0xf6, 0x3d, 0x91, 0xd7, 0xf5, 0x65, + 0x58, 0x46, 0xd4, 0xa6, 0x98, 0xfb, 0x63, 0x0f, 0xb2, 0x35, 0x21, 0x39, 0xc5, 0xbb, 0x20, 0x84, 0xb4, 0x07, 0x33, + 0xef, 0x2d, 0x9e, 0x47, 0x34, 0xf1, 0x26, 0x45, 0xaf, 0x5c, 0x7f, 0x99, 0x3d, 0xfa, 0xbe, 0xbc, 0x93, 0x5c, 0xd0, + 0x44, 0xf5, 0x56, 0x42, 0xd9, 0x2c, 0x69, 0x67, 0x4b, 0x7a, 0xa1, 0xa1, 0xec, 0x8c, 0xe2, 0x74, 0xde, 0x04, 0x71, + 0xbf, 0x31, 0xe5, 0x10, 0xe6, 0x56, 0xa6, 0x1c, 0xbe, 0x04, 0x58, 0xcb, 0xa7, 0xf7, 0xfe, 0xb8, 0xfc, 0xfd, 0x8a, + 0xe6, 0xb9, 0x3f, 0x56, 0x35, 0xb7, 0xa7, 0x18, 0x0a, 0x10, 0xcd, 0xf4, 0x42, 0x0d, 0x04, 0xe4, 0x01, 0x02, 0x42, + 0x20, 0x76, 0xac, 0xd2, 0x02, 0x61, 0xe6, 0xf5, 0x8c, 0x42, 0x81, 0xaa, 0x7a, 0x11, 0xf7, 0xc7, 0x55, 0xc1, 0xf1, + 0x34, 0xa3, 0x2a, 0x57, 0x11, 0xb0, 0x58, 0x1c, 0xb7, 0xa0, 0x40, 0xbe, 0xde, 0x92, 0x39, 0xa8, 0xb9, 0xcb, 0xf6, + 0xfc, 0x41, 0x4b, 0x67, 0x0e, 0x9a, 0x87, 0x60, 0xca, 0x13, 0x30, 0xeb, 0xc9, 0x7f, 0x5f, 0x76, 0x02, 0xf8, 0x4f, + 0x9d, 0xf1, 0xf8, 0x72, 0x34, 0x1a, 0xdd, 0x99, 0x49, 0xf8, 0x65, 0x38, 0xa2, 0x5d, 0x7a, 0xd8, 0x83, 0x03, 0x12, + 0x4d, 0x95, 0xf3, 0xd6, 0x29, 0x04, 0xee, 0x16, 0xf7, 0xab, 0x0c, 0xe9, 0x71, 0x3c, 0x5a, 0xdc, 0x3f, 0xab, 0xb0, + 0x98, 0x66, 0x74, 0x31, 0xf1, 0xb3, 0x31, 0x4b, 0xbc, 0x76, 0xe1, 0x5e, 0x2f, 0x14, 0xa8, 0x47, 0x47, 0x47, 0x85, + 0x1b, 0xea, 0xa7, 0x76, 0x18, 0x16, 0x6e, 0xb0, 0x28, 0xa7, 0xd1, 0x6e, 0x8f, 0x46, 0x85, 0xcb, 0x74, 0xc1, 0x7e, + 0x37, 0x08, 0xf7, 0xbb, 0x85, 0x3b, 0x37, 0x6a, 0x14, 0x2e, 0x55, 0x4f, 0x19, 0x0d, 0x6b, 0xa7, 0x2c, 0x1e, 0xb5, + 0xdb, 0x85, 0x2b, 0x09, 0x6d, 0x01, 0x31, 0x39, 0xf9, 0xd3, 0xf3, 0x67, 0x1c, 0x0c, 0xa6, 0xa2, 0x17, 0x73, 0xe7, + 0xfc, 0x56, 0xdd, 0x60, 0x29, 0x3f, 0xf9, 0x58, 0xa0, 0x21, 0xfe, 0xda, 0x4c, 0xd2, 0x03, 0x62, 0x16, 0xc9, 0x79, + 0xb1, 0xce, 0xe1, 0xab, 0xbd, 0x06, 0xca, 0x12, 0xaf, 0xbf, 0x26, 0x71, 0x95, 0xbb, 0x07, 0x7c, 0x0c, 0x6a, 0xca, + 0x8b, 0xd6, 0xf3, 0x6d, 0xd2, 0x23, 0xfb, 0xb4, 0xf4, 0xb8, 0xba, 0x8f, 0xf0, 0xc8, 0xfe, 0x70, 0xe1, 0x91, 0x9b, + 0xc2, 0x43, 0xb2, 0x8e, 0x39, 0x27, 0x76, 0x10, 0xd1, 0xe0, 0xe3, 0x55, 0x7a, 0xd3, 0x84, 0x2d, 0x91, 0xd9, 0x42, + 0xac, 0x5c, 0xff, 0xd6, 0x43, 0x03, 0xba, 0x33, 0xe3, 0x7b, 0x91, 0x42, 0xc7, 0x7f, 0x93, 0x10, 0xfb, 0x8d, 0x0e, + 0xec, 0xc9, 0x92, 0xd1, 0x88, 0xd8, 0x6f, 0x46, 0x23, 0x5b, 0xdf, 0xc3, 0xe3, 0x73, 0x2a, 0x6a, 0xbd, 0xae, 0x95, + 0x88, 0x5a, 0x60, 0xe8, 0x57, 0x65, 0x66, 0x81, 0x4a, 0xf1, 0x33, 0xd3, 0xf9, 0xd4, 0x9b, 0x90, 0xe5, 0xb0, 0xd5, + 0xe0, 0x33, 0x96, 0xf5, 0xef, 0x00, 0xe4, 0xb5, 0x8f, 0x36, 0x95, 0x00, 0x6f, 0xf8, 0xd2, 0xd4, 0xea, 0x25, 0x74, + 0x63, 0xaa, 0x55, 0xfc, 0x27, 0xb7, 0x2f, 0x42, 0x67, 0xce, 0x51, 0xc1, 0xf2, 0x37, 0xc9, 0xca, 0x05, 0x13, 0x12, + 0x46, 0x42, 0xcc, 0x69, 0x15, 0x3c, 0x1d, 0x8f, 0x63, 0x71, 0x6e, 0xa5, 0x66, 0x70, 0xcb, 0xe6, 0x83, 0xda, 0x7c, + 0x3d, 0xb3, 0xa1, 0xfa, 0x92, 0x87, 0xf8, 0xb4, 0xb1, 0x3c, 0x98, 0x7c, 0xad, 0xbe, 0x71, 0x2b, 0x62, 0x82, 0x0b, + 0xc5, 0xe3, 0x17, 0xf2, 0x38, 0x2b, 0xc7, 0x2c, 0x94, 0xcd, 0x59, 0x58, 0x14, 0xea, 0x22, 0x80, 0x90, 0xe5, 0x53, + 0xd0, 0x9e, 0x64, 0x4b, 0xfa, 0x29, 0x16, 0x9e, 0xcf, 0x8d, 0x3c, 0xba, 0xda, 0x72, 0x15, 0xda, 0x4e, 0x93, 0x89, + 0x49, 0x73, 0x5e, 0xd8, 0xca, 0x64, 0xd3, 0x48, 0xb4, 0x2d, 0x89, 0x4f, 0x99, 0xe1, 0x67, 0xcc, 0x10, 0x92, 0x8c, + 0xca, 0x05, 0xd1, 0xaf, 0x74, 0x41, 0x61, 0x5a, 0x59, 0xe2, 0x8d, 0xc4, 0x96, 0xc8, 0x4a, 0xcb, 0xa7, 0x7e, 0xa2, + 0x8d, 0x39, 0xc9, 0x0f, 0x76, 0x17, 0xd5, 0xca, 0x17, 0xb6, 0x06, 0x5b, 0x12, 0x6f, 0xff, 0xb8, 0x05, 0x0d, 0xfa, + 0x56, 0x0d, 0xf4, 0x64, 0x2d, 0x99, 0xed, 0xee, 0x14, 0xef, 0x8f, 0x97, 0x6e, 0x3e, 0xc7, 0x6e, 0x3e, 0xb7, 0xbe, + 0x5a, 0x34, 0xe7, 0xf4, 0xea, 0x23, 0xe3, 0x4d, 0xee, 0x4f, 0x9b, 0xe0, 0x3d, 0x15, 0x49, 0x28, 0x8a, 0x3d, 0x0b, + 0x1d, 0x5d, 0x9a, 0x7e, 0xbd, 0x59, 0x0e, 0x99, 0xe0, 0xc2, 0x8c, 0xf2, 0x92, 0x34, 0xa1, 0xbd, 0xfa, 0x49, 0x41, + 0x33, 0x99, 0x59, 0x63, 0x6b, 0xb8, 0x48, 0x21, 0x73, 0x9c, 0xdf, 0x7a, 0x6d, 0xc5, 0xd6, 0xdb, 0x3a, 0x53, 0xb9, + 0xbd, 0xb1, 0xbe, 0xa7, 0x90, 0xdb, 0x10, 0xd2, 0x2b, 0x5b, 0x4f, 0xb5, 0xde, 0x96, 0x4a, 0xfe, 0xa9, 0x73, 0x73, + 0x90, 0xba, 0xa2, 0xff, 0x37, 0x0e, 0x1c, 0xae, 0x16, 0x8b, 0x73, 0x73, 0xf7, 0x81, 0xcc, 0xf3, 0x47, 0x9c, 0x66, + 0xf8, 0x3e, 0x35, 0xaf, 0xc4, 0x15, 0x17, 0x0b, 0x10, 0x33, 0x5e, 0xe7, 0xa8, 0x9e, 0xf5, 0x7d, 0x77, 0xf7, 0x77, + 0x4f, 0xbf, 0x50, 0x38, 0xd2, 0x57, 0xbe, 0xda, 0x76, 0x0f, 0x36, 0x42, 0xec, 0xdf, 0x7a, 0x2c, 0x11, 0x32, 0xef, + 0x0a, 0x92, 0x42, 0x7a, 0xd3, 0x54, 0x1d, 0x00, 0xcd, 0x68, 0x2c, 0xbe, 0xf2, 0xae, 0x96, 0x62, 0xff, 0xe1, 0xf4, + 0x46, 0xaf, 0x46, 0x67, 0xe5, 0x60, 0xe7, 0x1f, 0x7a, 0x7e, 0x73, 0xfb, 0x81, 0xd1, 0xfa, 0x19, 0xc4, 0xc3, 0xe9, + 0x4d, 0x4f, 0x0a, 0xda, 0x66, 0x26, 0xa1, 0x6a, 0x4f, 0x6f, 0xcc, 0x13, 0xac, 0x55, 0x47, 0x96, 0xbb, 0x9f, 0x5b, + 0xd4, 0xcf, 0x69, 0x0f, 0x3e, 0x6a, 0xc5, 0x02, 0x3f, 0x56, 0xc2, 0x7c, 0xc2, 0xc2, 0x30, 0xa6, 0x3d, 0x2d, 0xaf, + 0xad, 0xce, 0x43, 0x38, 0x00, 0x6a, 0x2e, 0x59, 0x7d, 0x55, 0x0c, 0xe4, 0x95, 0x78, 0xf2, 0xaf, 0xf2, 0x34, 0x86, + 0x4f, 0x4a, 0x6e, 0x44, 0xa7, 0x3a, 0x19, 0xd9, 0xae, 0x90, 0x27, 0x7e, 0xd7, 0xe7, 0x72, 0xd8, 0xfe, 0x53, 0x4f, + 0x2c, 0x78, 0xbb, 0xc7, 0xd3, 0xa9, 0xd7, 0xdc, 0xaf, 0x4f, 0x04, 0x5e, 0x95, 0x53, 0xc0, 0x1b, 0xa6, 0x85, 0x41, + 0x5a, 0x49, 0x3e, 0x6d, 0xb9, 0x1d, 0x55, 0x26, 0x3a, 0x00, 0x23, 0xb4, 0x2c, 0x2a, 0xea, 0x93, 0xf9, 0xc7, 0xec, + 0x96, 0xc7, 0x9b, 0x77, 0xcb, 0x63, 0xbd, 0x5b, 0xee, 0xa6, 0xd8, 0x2f, 0x47, 0x1d, 0xf8, 0xaf, 0x57, 0x4d, 0xc8, + 0x6b, 0x5b, 0xfb, 0xd3, 0x1b, 0x0b, 0xf4, 0xb4, 0x66, 0x77, 0x7a, 0x23, 0xcf, 0xef, 0x42, 0x8e, 0x5c, 0x1b, 0x4e, + 0xb4, 0xe2, 0xb6, 0x05, 0x85, 0xf0, 0x7f, 0xbb, 0xf6, 0xaa, 0x73, 0x00, 0xef, 0xa0, 0xd5, 0xe1, 0xfa, 0xbb, 0xee, + 0xdd, 0x9b, 0xd6, 0x4b, 0x52, 0xee, 0x78, 0x9a, 0x1b, 0x23, 0x97, 0xfb, 0x57, 0x57, 0x34, 0xf4, 0x46, 0x69, 0x30, + 0xcb, 0xff, 0x49, 0xc1, 0xaf, 0x90, 0x78, 0xe7, 0x96, 0x5e, 0xe9, 0x47, 0x37, 0x95, 0xa7, 0x89, 0x75, 0x0f, 0x8b, + 0x72, 0x9d, 0xbc, 0x3c, 0xf0, 0x63, 0xea, 0x74, 0xdd, 0x83, 0x0d, 0x9b, 0xe0, 0xdf, 0x64, 0x6d, 0x36, 0x4e, 0xe6, + 0xf7, 0x22, 0xe3, 0x4e, 0x24, 0x7c, 0x16, 0x0e, 0xcc, 0x35, 0x6c, 0x1f, 0x6d, 0x06, 0xf7, 0x5c, 0x8f, 0x34, 0xd4, + 0x42, 0x41, 0xc9, 0x9d, 0x90, 0x8e, 0xfc, 0x59, 0xcc, 0xef, 0xee, 0x75, 0x1b, 0x65, 0xac, 0xf5, 0x7a, 0x07, 0x43, + 0xaf, 0xea, 0xde, 0x93, 0x4b, 0x7f, 0xf9, 0xf8, 0x00, 0xfe, 0x93, 0xe7, 0x6c, 0xae, 0x2a, 0x5d, 0x5d, 0x5a, 0xbd, + 0xa0, 0xab, 0x5f, 0xd7, 0x94, 0x71, 0x29, 0xc2, 0x85, 0x3e, 0x7e, 0xdf, 0xda, 0xa0, 0x55, 0xde, 0xab, 0xba, 0xd2, + 0xb2, 0x3e, 0xab, 0xf6, 0xe7, 0x75, 0x7e, 0xcf, 0xba, 0x81, 0xd4, 0x5c, 0xeb, 0x75, 0xd5, 0x57, 0xee, 0xd7, 0x2a, + 0x6b, 0x8c, 0x8b, 0xfa, 0xd7, 0xe4, 0xaa, 0x34, 0x51, 0x64, 0xd6, 0x2b, 0x58, 0x29, 0xd7, 0xd2, 0x4a, 0x49, 0x29, + 0xb9, 0x3c, 0x1e, 0xdc, 0x4c, 0x62, 0xeb, 0x5a, 0x5e, 0xc5, 0x43, 0xec, 0x8e, 0xdb, 0xb6, 0x2d, 0xe1, 0xa4, 0x83, + 0x2f, 0x82, 0xd9, 0x1f, 0xde, 0x7f, 0xdd, 0x3c, 0xb2, 0x07, 0xa0, 0x69, 0x5d, 0x8f, 0x85, 0x66, 0xf7, 0xd2, 0xbf, + 0xa5, 0xd9, 0x45, 0x57, 0xb9, 0xe0, 0x65, 0x6a, 0xba, 0x28, 0xb3, 0xba, 0xb6, 0x75, 0x33, 0x89, 0x93, 0x9c, 0xd8, + 0x11, 0xe7, 0x53, 0xaf, 0xd5, 0x9a, 0xcf, 0xe7, 0xee, 0x7c, 0xdf, 0x4d, 0xb3, 0x71, 0xab, 0xdb, 0x6e, 0xb7, 0xe1, + 0xe3, 0x22, 0xb6, 0x75, 0xcd, 0xe8, 0xfc, 0x49, 0x7a, 0x43, 0xec, 0xb6, 0xd5, 0xb6, 0x3a, 0xdd, 0x23, 0xab, 0xd3, + 0x3d, 0x70, 0x1f, 0x1e, 0xd9, 0xfd, 0x2f, 0x2c, 0xeb, 0x38, 0xa4, 0xa3, 0x1c, 0x7e, 0x58, 0xd6, 0xb1, 0x50, 0xbc, + 0xe4, 0x6f, 0xcb, 0x72, 0x83, 0x38, 0x6f, 0x76, 0xac, 0x85, 0x7a, 0xb4, 0x2c, 0xb8, 0xb0, 0xc8, 0xb3, 0xbe, 0x1c, + 0x75, 0x47, 0x07, 0xa3, 0xc7, 0x3d, 0x55, 0x5c, 0x7c, 0x51, 0xab, 0x8e, 0xe5, 0xbf, 0x5d, 0xa3, 0x59, 0xce, 0xb3, + 0xf4, 0x23, 0x55, 0xae, 0x7d, 0x0b, 0x44, 0xcf, 0xc6, 0xa6, 0xdd, 0xf5, 0x91, 0x3a, 0x47, 0x57, 0xc1, 0xa8, 0x5b, + 0x55, 0x17, 0x30, 0xb6, 0x4a, 0x20, 0x8f, 0x5b, 0x1a, 0xf4, 0x63, 0x13, 0x4d, 0x9d, 0xe6, 0x26, 0x44, 0x75, 0x6c, + 0x35, 0xc7, 0xb1, 0x9e, 0xdf, 0x31, 0x9c, 0x8f, 0xd7, 0xba, 0xaa, 0x80, 0xc0, 0xb6, 0x42, 0x62, 0xbf, 0xea, 0x74, + 0x8f, 0x70, 0xa7, 0xf3, 0xd0, 0x7d, 0x78, 0x14, 0xb4, 0xf1, 0x81, 0x7b, 0xd0, 0xdc, 0x77, 0x1f, 0xe2, 0xa3, 0xe6, + 0x11, 0x3e, 0x7a, 0x7e, 0x14, 0x34, 0x0f, 0xdc, 0x03, 0xdc, 0x6e, 0x1e, 0x41, 0x61, 0xf3, 0xa8, 0x79, 0x74, 0xdd, + 0x3c, 0x38, 0x0a, 0xda, 0xa2, 0xb4, 0xeb, 0x1e, 0x1e, 0x36, 0x3b, 0x6d, 0xf7, 0xf0, 0x10, 0x1f, 0xba, 0x0f, 0x1f, + 0x36, 0x3b, 0xfb, 0xee, 0xc3, 0x87, 0x2f, 0x0f, 0x8f, 0xdc, 0x7d, 0x78, 0xb7, 0xbf, 0x1f, 0xec, 0xbb, 0x9d, 0x4e, + 0x13, 0xfe, 0xe0, 0x23, 0xb7, 0x2b, 0x7f, 0x74, 0x3a, 0xee, 0x7e, 0x07, 0xb7, 0xe3, 0xc3, 0xae, 0xfb, 0xf0, 0x31, + 0x16, 0x7f, 0x45, 0x35, 0x2c, 0xfe, 0x40, 0x37, 0xf8, 0xb1, 0xdb, 0x7d, 0x28, 0x7f, 0x89, 0x0e, 0xaf, 0x0f, 0x8e, + 0x7e, 0xb4, 0x5b, 0x5b, 0xe7, 0xd0, 0x91, 0x73, 0x38, 0x3a, 0x74, 0xf7, 0xf7, 0xf1, 0x41, 0xc7, 0x3d, 0xda, 0x8f, + 0x9a, 0x07, 0x5d, 0xf7, 0xe1, 0xa3, 0xa0, 0xd9, 0x71, 0x1f, 0x3d, 0xc2, 0xed, 0xe6, 0xbe, 0xdb, 0xc5, 0x1d, 0xf7, + 0x60, 0x5f, 0xfc, 0xd8, 0x77, 0xbb, 0xd7, 0x8f, 0x1e, 0xbb, 0x0f, 0x0f, 0xa3, 0x87, 0xee, 0xc1, 0xb7, 0x07, 0x47, + 0x6e, 0x77, 0x3f, 0xda, 0x7f, 0xe8, 0x76, 0x1f, 0x5d, 0x3f, 0x74, 0x0f, 0xa2, 0x66, 0xf7, 0xe1, 0x9d, 0x2d, 0x3b, + 0x5d, 0x17, 0x70, 0x24, 0x5e, 0xc3, 0x0b, 0xac, 0x5e, 0xc0, 0xff, 0x91, 0x68, 0xfb, 0x6f, 0xd8, 0x4d, 0xbe, 0xde, + 0xf4, 0xb1, 0x7b, 0xf4, 0x28, 0x90, 0xd5, 0xa1, 0xa0, 0xa9, 0x6b, 0x40, 0x93, 0xeb, 0xa6, 0x1c, 0x56, 0x74, 0xd7, + 0xd4, 0x1d, 0xe9, 0xff, 0xd5, 0x60, 0xd7, 0x4d, 0x18, 0x58, 0x8e, 0xfb, 0xef, 0xda, 0x4f, 0xb9, 0xe4, 0xc7, 0xad, + 0xb1, 0x24, 0xfd, 0x71, 0xff, 0x0b, 0xf9, 0xe5, 0xa0, 0x2f, 0x2e, 0xb1, 0xbf, 0xcd, 0xf1, 0x11, 0x7f, 0xda, 0xf1, + 0x11, 0xd1, 0xfb, 0x78, 0x3e, 0xe2, 0x3f, 0xdc, 0xf3, 0xe1, 0xaf, 0xba, 0xcd, 0x6f, 0xf8, 0x9a, 0x83, 0x63, 0xd5, + 0x2a, 0x7e, 0xc1, 0x9d, 0xf3, 0x14, 0xbe, 0x52, 0x5d, 0xf4, 0x6e, 0x38, 0x89, 0xa8, 0xe9, 0x07, 0x4a, 0x81, 0xc5, + 0xde, 0x70, 0xc9, 0x63, 0x83, 0x6d, 0x08, 0x09, 0x3f, 0x8d, 0x90, 0xef, 0xee, 0x83, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, + 0x60, 0xe2, 0xa3, 0xe6, 0xc9, 0x17, 0x9e, 0x06, 0xe1, 0x29, 0x38, 0x13, 0xcf, 0x0e, 0x5c, 0xd0, 0xd1, 0xb0, 0x5b, + 0xf4, 0x5a, 0x44, 0xee, 0x64, 0x70, 0xfd, 0xf9, 0xe7, 0x04, 0x1d, 0xe4, 0x6d, 0x3c, 0x44, 0x1f, 0x8b, 0x98, 0x0a, + 0xa9, 0xa3, 0x1e, 0x4a, 0xa1, 0xd4, 0x75, 0xdb, 0x6e, 0xbb, 0x74, 0xe9, 0xc0, 0x0d, 0x4c, 0x64, 0x91, 0x72, 0xdf, + 0xdb, 0xe9, 0xe0, 0x38, 0x1d, 0xc3, 0xbd, 0x4c, 0xe2, 0x4b, 0x75, 0x70, 0xe2, 0x21, 0x90, 0x1f, 0x09, 0x84, 0xf4, + 0x09, 0xe5, 0xe8, 0xf1, 0xb3, 0x8f, 0x7f, 0x83, 0x20, 0xa6, 0x8e, 0x49, 0x4c, 0xc0, 0xdb, 0xf1, 0x8a, 0x86, 0xcc, + 0x77, 0x6c, 0x67, 0x9a, 0xd1, 0x11, 0xcd, 0xf2, 0x66, 0xed, 0x6a, 0x20, 0x71, 0x2b, 0x10, 0xb2, 0x15, 0x84, 0xa3, + 0x0c, 0xbe, 0xbc, 0x44, 0xce, 0x95, 0xbf, 0xd1, 0x56, 0x06, 0x98, 0x5d, 0x60, 0x5d, 0x92, 0x81, 0xac, 0xad, 0x94, + 0x36, 0x5b, 0x6a, 0x6d, 0x1d, 0xb7, 0x7b, 0x88, 0x2c, 0x51, 0x0c, 0xdf, 0xb4, 0xf9, 0xc1, 0x69, 0xee, 0xb7, 0xff, + 0x84, 0x8c, 0x66, 0x65, 0x47, 0x43, 0xe5, 0x6e, 0xcb, 0xcb, 0x2f, 0x1f, 0xae, 0x84, 0x5d, 0x6d, 0x49, 0x11, 0x5f, + 0xca, 0xb9, 0xdb, 0xa8, 0x97, 0xab, 0xa4, 0x39, 0x79, 0xfb, 0xe0, 0x88, 0x8d, 0x1d, 0xe3, 0x66, 0x8b, 0x5c, 0x7e, + 0x33, 0x07, 0x2e, 0xc6, 0x47, 0xa8, 0xa8, 0xaa, 0xe4, 0x68, 0x21, 0xa2, 0x2d, 0x2c, 0xb1, 0xf2, 0xe5, 0xd2, 0x11, + 0x2e, 0x72, 0x62, 0xe0, 0x14, 0x9e, 0x51, 0x0d, 0xc9, 0x39, 0x2e, 0x01, 0x12, 0x08, 0x26, 0xb9, 0xfc, 0xb7, 0x2a, + 0xd6, 0x3f, 0x94, 0xe3, 0xcb, 0x8d, 0xfd, 0x64, 0x0c, 0x54, 0xe8, 0x27, 0xe3, 0x35, 0xb7, 0x9a, 0x0c, 0x18, 0xad, + 0x94, 0x56, 0x5d, 0x55, 0xee, 0xb3, 0xfc, 0xc9, 0xed, 0x7b, 0x75, 0xb9, 0xb6, 0x0d, 0xde, 0x69, 0x11, 0xdf, 0xa8, + 0x3e, 0x04, 0xd4, 0x20, 0x0f, 0x8e, 0x27, 0x94, 0xfb, 0xf2, 0x5c, 0x1c, 0xe8, 0x13, 0x90, 0xcb, 0x62, 0x29, 0x6b, + 0x54, 0x05, 0xf5, 0x89, 0xbc, 0x37, 0x40, 0x8a, 0x7a, 0x6c, 0xa9, 0x5b, 0xe9, 0x9a, 0x62, 0x69, 0x48, 0x07, 0x4b, + 0x7f, 0x4c, 0xe0, 0x8b, 0x93, 0xcf, 0x24, 0x49, 0xed, 0xfe, 0x83, 0x32, 0xd7, 0x65, 0xdb, 0x22, 0xc4, 0x2c, 0xf9, + 0x78, 0x9e, 0xd1, 0xf8, 0x9f, 0xc8, 0x03, 0x16, 0xa4, 0xc9, 0x83, 0xa1, 0x8d, 0x7a, 0xdc, 0x8d, 0x32, 0x3a, 0x22, + 0x0f, 0x40, 0xc6, 0x7b, 0xc2, 0xfa, 0x00, 0x46, 0xd8, 0xb8, 0x99, 0xc4, 0x58, 0x68, 0x4c, 0xf7, 0x50, 0x88, 0x24, + 0xb8, 0x76, 0xf7, 0xd0, 0xb6, 0xa4, 0x4d, 0x2c, 0x7e, 0xf7, 0xa5, 0x38, 0x15, 0x4a, 0x80, 0xd5, 0xe9, 0xba, 0x87, + 0x51, 0xd7, 0x7d, 0x7c, 0xfd, 0xc8, 0x3d, 0x8a, 0x3a, 0x8f, 0xae, 0x9b, 0xf0, 0x6f, 0xd7, 0x7d, 0x1c, 0x37, 0xbb, + 0xee, 0x63, 0xf8, 0xff, 0xdb, 0x03, 0xf7, 0x30, 0x6a, 0x76, 0xdc, 0xa3, 0xeb, 0x7d, 0x77, 0xff, 0x65, 0xa7, 0xeb, + 0xee, 0x5b, 0x1d, 0x4b, 0xb6, 0x03, 0x76, 0x2d, 0xb9, 0xf3, 0x83, 0x95, 0x0d, 0xb1, 0x21, 0x18, 0x27, 0xcf, 0xf6, + 0xd9, 0x58, 0x1c, 0xc7, 0x36, 0xf7, 0xa7, 0x72, 0xd6, 0x3d, 0xf5, 0x33, 0xf8, 0x88, 0x6a, 0x7d, 0xef, 0xd6, 0xde, + 0xe1, 0x1a, 0xbf, 0xd8, 0x30, 0xc4, 0x54, 0x44, 0xc0, 0xcd, 0x6b, 0xdd, 0xa8, 0xb8, 0x2e, 0x4f, 0x7e, 0x76, 0x4a, + 0x45, 0xc1, 0xca, 0xec, 0x22, 0x83, 0xac, 0x65, 0x0d, 0x48, 0x00, 0x12, 0x34, 0xb8, 0x9a, 0x3f, 0x5a, 0xd1, 0x79, + 0x06, 0x57, 0x21, 0x68, 0x5e, 0xc2, 0xc4, 0xc7, 0xff, 0x04, 0x0c, 0x2f, 0xc2, 0x62, 0x15, 0x3c, 0x38, 0x81, 0x98, + 0xa5, 0xc6, 0xc5, 0x77, 0xb4, 0xca, 0x01, 0x08, 0x19, 0x5c, 0x55, 0x58, 0x14, 0x7a, 0x66, 0x35, 0x2f, 0x6e, 0x85, + 0x44, 0xc1, 0x4e, 0x68, 0x3e, 0xb0, 0xa1, 0xc8, 0xf6, 0x6c, 0xe1, 0x01, 0xb4, 0xcb, 0xef, 0xcc, 0x96, 0x74, 0x5f, + 0x15, 0x60, 0x71, 0x0f, 0x05, 0x6c, 0x6a, 0x40, 0x9f, 0x8d, 0xf6, 0xf6, 0xb6, 0x6e, 0x27, 0xa1, 0x5f, 0xc2, 0xd4, + 0xaa, 0xcf, 0x53, 0x9a, 0x9c, 0xca, 0x36, 0xd7, 0xa1, 0xec, 0x57, 0x60, 0x18, 0x29, 0xb4, 0x5c, 0x51, 0x9f, 0xbb, + 0x7e, 0x22, 0x0f, 0x18, 0x18, 0xfc, 0x0c, 0x77, 0xe8, 0x3e, 0x2a, 0x52, 0xee, 0xcb, 0x9c, 0x31, 0x93, 0x0d, 0xa4, + 0xdc, 0xd7, 0xd7, 0x38, 0xf9, 0xbc, 0x76, 0x84, 0x3f, 0xea, 0xf6, 0xdf, 0xbc, 0x3f, 0xb1, 0xe4, 0xee, 0x3d, 0x6e, + 0x45, 0xdd, 0xfe, 0xb1, 0x70, 0xa9, 0xc8, 0xac, 0x00, 0x22, 0xb3, 0x02, 0x2c, 0x75, 0x7f, 0x0d, 0x04, 0xda, 0x8a, + 0x96, 0x9c, 0xb6, 0x30, 0x29, 0xa4, 0x33, 0x78, 0x32, 0x8b, 0x39, 0x83, 0xcf, 0x2b, 0xb5, 0x44, 0x4a, 0x80, 0x48, + 0x31, 0xd0, 0xc7, 0x61, 0x95, 0xf2, 0x78, 0xc5, 0x13, 0xed, 0x3a, 0x1e, 0xb1, 0x98, 0xea, 0x03, 0xb0, 0xaa, 0xab, + 0x32, 0x1f, 0x68, 0xbd, 0x76, 0x3e, 0xbb, 0x82, 0x9c, 0x08, 0x9d, 0x7d, 0xf4, 0x41, 0x35, 0x38, 0x16, 0x43, 0x41, + 0x60, 0x5f, 0x4a, 0x71, 0xfd, 0x21, 0xd9, 0xfa, 0x92, 0xaa, 0xd9, 0x2b, 0x01, 0x02, 0x97, 0x86, 0x44, 0xfb, 0xfd, + 0xd2, 0x9b, 0x6c, 0xbe, 0x2b, 0x8e, 0x5b, 0xd1, 0x7e, 0xff, 0xd2, 0x1b, 0xab, 0xfe, 0x5e, 0xa6, 0xe3, 0xcd, 0x7d, + 0xc5, 0xe9, 0x78, 0x20, 0x4e, 0xe4, 0xcb, 0xdb, 0xa5, 0xb4, 0x6e, 0x9c, 0xc6, 0x76, 0xff, 0x58, 0xe9, 0x0a, 0x96, + 0x88, 0xba, 0xdb, 0x87, 0x6d, 0x7d, 0xc8, 0x3f, 0x4e, 0xc7, 0xb0, 0x5f, 0x65, 0x13, 0x63, 0x90, 0x9a, 0x43, 0x3e, + 0xea, 0xf4, 0x8f, 0x7d, 0x4b, 0xb0, 0x1e, 0xc1, 0x5b, 0x72, 0xaf, 0x05, 0x8d, 0xa3, 0x74, 0x42, 0x5d, 0x96, 0xb6, + 0xe6, 0xf4, 0xaa, 0xe9, 0x4f, 0x59, 0xe5, 0xfd, 0x06, 0x9d, 0xa4, 0x1c, 0x32, 0x5d, 0xc9, 0xc0, 0xea, 0x56, 0xde, + 0xb8, 0x03, 0x30, 0x89, 0xb4, 0xe7, 0x4e, 0xb8, 0xec, 0x0c, 0xb0, 0xd2, 0xfe, 0x71, 0xcb, 0x5f, 0xc1, 0x88, 0xd8, + 0x8a, 0x85, 0xf2, 0xc3, 0x83, 0xdd, 0x73, 0x25, 0xd2, 0xbf, 0xa4, 0xb4, 0xd0, 0xfe, 0x7a, 0x25, 0xc7, 0x0b, 0xbb, + 0xff, 0xaf, 0xff, 0xe3, 0x7f, 0x29, 0x17, 0xfc, 0x71, 0x2b, 0xea, 0xe8, 0xbe, 0x56, 0x56, 0xa5, 0x38, 0x86, 0x2b, + 0x73, 0xaa, 0x98, 0x31, 0xbd, 0x69, 0x8e, 0x33, 0x16, 0x36, 0x23, 0x3f, 0x1e, 0xd9, 0xfd, 0xed, 0xd8, 0x94, 0xe9, + 0x89, 0x4d, 0x1d, 0x6d, 0x5d, 0x2f, 0x02, 0x7a, 0xfd, 0x4d, 0xf7, 0x3f, 0xe8, 0x8c, 0x2f, 0xb1, 0xb5, 0xcd, 0xdb, + 0x20, 0xaa, 0xdd, 0x57, 0xbb, 0x11, 0x22, 0x57, 0x5f, 0xa7, 0x56, 0x0c, 0x32, 0xaf, 0x5d, 0x04, 0x51, 0xd8, 0x56, + 0x19, 0xf3, 0xfa, 0xbf, 0xff, 0xf3, 0xbf, 0xfc, 0x37, 0xfd, 0x08, 0xa1, 0xac, 0x7f, 0xfd, 0xef, 0xff, 0xf9, 0xff, + 0xfc, 0xef, 0xff, 0x0a, 0xe9, 0x69, 0x2a, 0xdc, 0x25, 0x98, 0x8a, 0x55, 0xc5, 0xba, 0x24, 0x77, 0xb1, 0xe0, 0xd0, + 0xdb, 0x84, 0xe5, 0x9c, 0x05, 0xf5, 0xab, 0x21, 0xce, 0xc4, 0x80, 0x62, 0x67, 0x2a, 0xe8, 0xc4, 0x0e, 0x2f, 0x2a, + 0x82, 0xaa, 0xa1, 0x5c, 0x10, 0x6e, 0x71, 0xdc, 0x02, 0x7c, 0xdf, 0xef, 0x66, 0x1b, 0xb7, 0x5c, 0x8e, 0x85, 0x26, + 0x13, 0x28, 0x29, 0xaa, 0x72, 0x0b, 0x42, 0x2f, 0x0b, 0x78, 0xf4, 0xba, 0x46, 0xb1, 0x58, 0xbd, 0x5a, 0x9b, 0xde, + 0xcf, 0xb3, 0x9c, 0xb3, 0x11, 0xa0, 0x5c, 0xba, 0x91, 0x45, 0x94, 0xbb, 0x09, 0xaa, 0x64, 0x7c, 0x5b, 0x88, 0x5e, + 0x24, 0x81, 0x1e, 0x1c, 0xfd, 0xa9, 0xf8, 0xf3, 0x04, 0x14, 0x36, 0xcb, 0x99, 0xf8, 0x37, 0xca, 0x7a, 0x7f, 0xd8, + 0x6e, 0x4f, 0x6f, 0xd0, 0xa2, 0x1a, 0x01, 0x6f, 0x1b, 0x4c, 0xd0, 0xb1, 0xd9, 0xa1, 0x08, 0x8f, 0x97, 0x5e, 0xee, + 0xb6, 0x05, 0xae, 0x72, 0xab, 0x5d, 0x14, 0x5f, 0x2d, 0x84, 0xa3, 0x95, 0xfd, 0x0a, 0x61, 0x6c, 0xe5, 0x93, 0xbe, + 0x4a, 0xcd, 0xc9, 0x2d, 0x8c, 0x56, 0x5d, 0xd9, 0x2a, 0xea, 0xac, 0x5f, 0x12, 0x63, 0x86, 0xe1, 0xcd, 0x00, 0xfa, + 0x01, 0x84, 0xc4, 0xa3, 0x0e, 0x8e, 0xba, 0x8b, 0xb2, 0x7b, 0xce, 0xd3, 0x89, 0x19, 0x77, 0xa7, 0x3e, 0x0d, 0xe8, + 0x48, 0xfb, 0xf2, 0xd5, 0x7b, 0x19, 0x53, 0x2f, 0xa2, 0xfd, 0x0d, 0x63, 0x29, 0x90, 0x44, 0xbc, 0xdd, 0x6a, 0x17, + 0x5f, 0xc2, 0x0e, 0x5c, 0x8c, 0xe2, 0xd4, 0xe7, 0x9e, 0x20, 0xd8, 0x9e, 0x19, 0xbd, 0xf7, 0x81, 0x27, 0xa5, 0x0b, + 0x03, 0x9e, 0x9e, 0xac, 0x0a, 0x5e, 0xf5, 0xfa, 0x65, 0x91, 0x85, 0x2b, 0x9a, 0x9b, 0x5d, 0x49, 0xa7, 0xdc, 0x77, + 0x2a, 0x28, 0xfe, 0xbc, 0xe6, 0xcd, 0x52, 0x02, 0xa9, 0x8b, 0x36, 0xbf, 0x97, 0x62, 0x5f, 0xbe, 0xfd, 0x9e, 0x3b, + 0xb6, 0x00, 0xd3, 0x5e, 0xad, 0x25, 0x0a, 0xa1, 0xd6, 0x73, 0xf2, 0x5d, 0x69, 0x51, 0xf9, 0xd3, 0xa9, 0xa8, 0x88, + 0x7a, 0xc7, 0x2d, 0xa9, 0x08, 0x03, 0xf7, 0x10, 0x19, 0x1f, 0x32, 0xc1, 0x42, 0x55, 0x52, 0x5b, 0x41, 0xfe, 0x52, + 0xa9, 0x17, 0xf0, 0xd5, 0xf2, 0xfe, 0xff, 0x03, 0x0c, 0xbd, 0x72, 0x0a, 0x4e, 0x98, 0x00, 0x00}; #else // Brotli (default, smaller) constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x1b, 0xe2, 0x97, 0xa3, 0x90, 0xa2, 0x95, 0x55, 0x51, 0x04, 0x1b, 0x07, 0x80, 0x20, 0x79, 0x0e, 0x50, 0xab, 0x02, - 0xdb, 0x98, 0x16, 0xf4, 0x7b, 0x22, 0xa3, 0x4d, 0xd3, 0x86, 0xc1, 0x26, 0x48, 0x49, 0x60, 0xbe, 0xb3, 0xc9, 0xa1, - 0x8c, 0x96, 0x10, 0x1b, 0x21, 0xcf, 0x48, 0x68, 0xce, 0x10, 0x34, 0x32, 0x7c, 0xbf, 0x71, 0x7b, 0x03, 0x8f, 0xdd, - 0x37, 0x06, 0x9a, 0x30, 0x50, 0xe4, 0x08, 0x47, 0x68, 0xec, 0x93, 0xdc, 0x7d, 0x53, 0xf5, 0x4f, 0xd7, 0x8a, 0xcf, - 0x2f, 0x85, 0x3a, 0x6c, 0xa9, 0x63, 0xcb, 0xf5, 0xc8, 0x18, 0xe3, 0xf5, 0xdb, 0x0c, 0x05, 0x9b, 0x48, 0x2c, 0x42, - 0x21, 0xa0, 0x2c, 0x25, 0xf7, 0xcb, 0xb7, 0xaa, 0x65, 0xd5, 0x7f, 0x3e, 0x2f, 0x94, 0x2b, 0x53, 0xa7, 0x0f, 0x4e, - 0x56, 0xa9, 0xf7, 0xce, 0x54, 0x88, 0x39, 0xef, 0xea, 0x3d, 0x69, 0x56, 0xd0, 0x52, 0x96, 0x0a, 0x5b, 0x35, 0xd4, - 0x42, 0xd6, 0x35, 0x10, 0xf3, 0x7f, 0xec, 0x95, 0xd3, 0x2a, 0xfe, 0x4d, 0x22, 0x6a, 0xd6, 0x69, 0x6e, 0x7b, 0x5a, - 0xdd, 0x33, 0x58, 0x21, 0xa4, 0x33, 0xd6, 0x61, 0x05, 0xf5, 0x08, 0xa9, 0x10, 0x32, 0xf5, 0xdc, 0x04, 0x59, 0x72, - 0x41, 0xf4, 0x09, 0xfa, 0x08, 0x08, 0x9b, 0xcc, 0xf3, 0x2d, 0x71, 0xb4, 0x1c, 0xe3, 0x04, 0x64, 0x9a, 0x96, 0x5b, - 0x16, 0x58, 0xed, 0xbf, 0x37, 0xd5, 0x2a, 0x6d, 0x80, 0x66, 0xcf, 0xba, 0xd4, 0xb8, 0xd4, 0x38, 0x65, 0x10, 0x57, - 0x3a, 0xe7, 0x93, 0xe0, 0x82, 0x90, 0x78, 0xef, 0xfd, 0xff, 0x96, 0xed, 0x30, 0x44, 0x77, 0x13, 0x3b, 0x70, 0x92, - 0xe8, 0xb0, 0xf4, 0x25, 0x5a, 0xc9, 0xff, 0xff, 0xbb, 0x01, 0x76, 0x03, 0x94, 0x16, 0x20, 0xa5, 0x2d, 0x8a, 0xe2, - 0x56, 0x51, 0xd4, 0xdc, 0x18, 0xcb, 0x99, 0xb3, 0x4e, 0x3b, 0x55, 0x67, 0x5d, 0x64, 0xa3, 0xc4, 0xf8, 0xec, 0x2a, - 0xb7, 0x51, 0x68, 0x6c, 0x7a, 0xd9, 0x85, 0x97, 0x9e, 0xcb, 0x61, 0x26, 0xbe, 0xeb, 0x3a, 0x6b, 0xe5, 0x38, 0xd8, - 0x63, 0xa8, 0xb5, 0xba, 0xbe, 0xbd, 0x1d, 0xe3, 0xc4, 0x11, 0xa3, 0x08, 0xc4, 0xfe, 0x26, 0x3a, 0xfb, 0x01, 0x5d, - 0xb4, 0xfc, 0x1d, 0x78, 0xca, 0xb2, 0xac, 0x61, 0x39, 0x21, 0xe5, 0x6b, 0x5a, 0x70, 0x76, 0x57, 0x91, 0x4c, 0x8c, - 0x3d, 0x0e, 0x50, 0x4e, 0x41, 0x1c, 0xda, 0x62, 0xd2, 0xf1, 0x25, 0xce, 0x03, 0xf4, 0xfa, 0x3b, 0xc1, 0xc4, 0xed, - 0xc1, 0xdf, 0x8f, 0xe1, 0xc0, 0x0e, 0x34, 0x72, 0x3c, 0xb3, 0x3f, 0xfa, 0xc0, 0xe6, 0xcd, 0xf4, 0x01, 0x19, 0xf4, - 0x28, 0x5b, 0xde, 0x02, 0x6e, 0xa2, 0x24, 0x59, 0x76, 0x94, 0x8d, 0x00, 0x35, 0xab, 0xbe, 0xd9, 0xc0, 0xfb, 0xfa, - 0x97, 0x4f, 0x8f, 0x6e, 0xa4, 0x28, 0x0a, 0xfd, 0x43, 0xd9, 0xf2, 0xb2, 0xc2, 0x75, 0x49, 0x97, 0x8c, 0xcd, 0x61, - 0xb3, 0x64, 0x52, 0x1e, 0x46, 0x1e, 0xa2, 0xe0, 0xb5, 0x26, 0xd3, 0xf5, 0x3c, 0x9e, 0x19, 0x32, 0x45, 0xb9, 0x28, - 0xf5, 0x40, 0xcf, 0xa5, 0xf1, 0xcd, 0x8d, 0x29, 0x55, 0xe3, 0x2c, 0x43, 0x12, 0xa2, 0x4d, 0x37, 0xda, 0x94, 0x3e, - 0x49, 0x88, 0x4f, 0x2c, 0xa5, 0x67, 0xbe, 0xb7, 0x75, 0x28, 0xb8, 0x2f, 0x0d, 0x75, 0xce, 0x87, 0x3f, 0xe3, 0x30, - 0x5a, 0x25, 0x92, 0x30, 0xd3, 0x2d, 0x45, 0xca, 0xe5, 0x49, 0xc7, 0x4d, 0x13, 0x95, 0xa5, 0x9f, 0x7f, 0x2d, 0x49, - 0x46, 0x5a, 0x49, 0x11, 0x12, 0xd2, 0xdd, 0x38, 0x3c, 0x31, 0x63, 0x5e, 0xb6, 0xe6, 0xde, 0xcd, 0xcd, 0x6d, 0x67, - 0x46, 0x53, 0x16, 0x86, 0xd4, 0xad, 0x07, 0xac, 0xdb, 0xd7, 0x9f, 0x48, 0xcc, 0xa6, 0x4d, 0x9f, 0x6c, 0x8b, 0xf2, - 0xcb, 0xcd, 0x1b, 0x1e, 0xa9, 0x39, 0x37, 0x4d, 0xcd, 0x80, 0x9b, 0x19, 0x67, 0x64, 0x70, 0xb0, 0x38, 0x00, 0x9f, - 0xab, 0x26, 0xca, 0xb7, 0xbb, 0x55, 0x50, 0xcf, 0x31, 0x65, 0x12, 0xb6, 0x2b, 0x36, 0x9d, 0x17, 0x2b, 0x50, 0xd1, - 0x13, 0x72, 0x80, 0x7f, 0x88, 0x62, 0xe4, 0xee, 0x40, 0xb7, 0x56, 0xd2, 0x66, 0xa3, 0xb0, 0x0e, 0x31, 0xeb, 0x9a, - 0x27, 0xc1, 0xd5, 0x7b, 0x63, 0xb3, 0x84, 0x1b, 0xc8, 0xbf, 0x35, 0x29, 0x8a, 0x3c, 0xd0, 0x66, 0x03, 0x2a, 0x3e, - 0xb9, 0x79, 0x4c, 0x16, 0x01, 0xaa, 0xe8, 0x0e, 0x01, 0x1c, 0x73, 0x05, 0x78, 0x3a, 0x9c, 0x23, 0xb8, 0x18, 0x78, - 0xc5, 0xcd, 0x53, 0x7f, 0xb4, 0x61, 0xb8, 0x11, 0xa4, 0xcd, 0xc6, 0x27, 0x27, 0xb6, 0xae, 0x51, 0x01, 0x1d, 0xec, - 0xe4, 0x6b, 0x99, 0xe4, 0xdb, 0x2d, 0xbb, 0x66, 0x38, 0x54, 0xf5, 0xf2, 0xba, 0xc3, 0x24, 0x41, 0xfa, 0xae, 0x46, - 0x43, 0xdd, 0xfd, 0x9d, 0x0d, 0x52, 0x98, 0x1c, 0x7f, 0xab, 0x29, 0x83, 0x0f, 0xb9, 0x11, 0xe0, 0xd3, 0x49, 0x86, - 0xef, 0xbb, 0xdf, 0x0a, 0x04, 0x76, 0x11, 0x07, 0x48, 0x7f, 0x86, 0x4c, 0x3a, 0x84, 0xf5, 0xb6, 0x32, 0x54, 0x59, - 0xed, 0xd5, 0xf1, 0xf0, 0xf8, 0x39, 0x2d, 0x10, 0x85, 0x11, 0xd2, 0xef, 0x72, 0xcb, 0xd2, 0x8f, 0xf2, 0x2e, 0x7c, - 0x9b, 0x28, 0xa6, 0x07, 0x7f, 0x7a, 0x7c, 0x43, 0x28, 0x0b, 0x3f, 0xe5, 0x98, 0x64, 0x6f, 0x63, 0xad, 0xd9, 0x90, - 0x34, 0x84, 0x30, 0xf9, 0x53, 0x6e, 0xea, 0xe3, 0x5f, 0x36, 0x39, 0xe7, 0x26, 0x49, 0xf0, 0xe9, 0xe7, 0x32, 0x50, - 0x58, 0x41, 0x1e, 0xaa, 0x98, 0x6f, 0x6b, 0xfa, 0x94, 0x4b, 0x20, 0x01, 0x84, 0x2a, 0x32, 0x84, 0x73, 0x5a, 0x39, - 0x4a, 0x57, 0xbc, 0xbd, 0x86, 0xa4, 0xb2, 0x77, 0x99, 0xe5, 0xdd, 0x44, 0x5d, 0xd5, 0xde, 0x5b, 0x94, 0x7e, 0xec, - 0x53, 0xdd, 0x67, 0xb8, 0x8d, 0xbb, 0x1d, 0x65, 0xf4, 0xe8, 0xe4, 0x73, 0x3d, 0xbc, 0xba, 0xd9, 0x30, 0xbe, 0x1f, - 0xeb, 0x0b, 0x21, 0xaf, 0x24, 0x9a, 0x44, 0xa2, 0x0a, 0xbf, 0xfe, 0xfa, 0x06, 0x14, 0x59, 0x76, 0x6d, 0x97, 0x7e, - 0xe0, 0x70, 0x8c, 0x41, 0x28, 0xac, 0x0b, 0xad, 0x4b, 0xb5, 0xbb, 0x54, 0xeb, 0x0d, 0x05, 0x24, 0xc7, 0xad, 0x04, - 0xfb, 0x9b, 0x12, 0x44, 0xec, 0x20, 0x03, 0xff, 0xba, 0x91, 0xa0, 0x50, 0xba, 0x24, 0xed, 0x9c, 0x96, 0x7e, 0xef, - 0x6f, 0x24, 0x6c, 0xd1, 0x8c, 0x55, 0xe9, 0x0f, 0x8c, 0x2f, 0x8b, 0x62, 0x44, 0x3c, 0x1b, 0x46, 0x3b, 0x49, 0x99, - 0xdc, 0xd6, 0x7a, 0x70, 0x5d, 0xe5, 0x2a, 0x62, 0x2e, 0x54, 0xab, 0x44, 0xf2, 0xf4, 0x61, 0xb2, 0xd8, 0x07, 0x8b, - 0x01, 0x3e, 0x04, 0x19, 0xe1, 0x5d, 0x8e, 0x2a, 0xff, 0x86, 0xa3, 0x59, 0xe5, 0xcc, 0x8d, 0xd3, 0x51, 0x6f, 0xc1, - 0x15, 0x9f, 0x37, 0x73, 0x3d, 0x49, 0x99, 0xca, 0x53, 0x3a, 0x96, 0x0c, 0x92, 0x2b, 0x8b, 0xde, 0x08, 0x68, 0x52, - 0x87, 0x31, 0xb2, 0x68, 0x81, 0xb1, 0xe9, 0x9f, 0x78, 0xf1, 0x22, 0xe8, 0x84, 0x48, 0xdb, 0x49, 0x4d, 0xd2, 0xea, - 0x80, 0x1f, 0xec, 0x50, 0x77, 0x66, 0xe7, 0x13, 0x36, 0x02, 0x85, 0x6f, 0xdd, 0x68, 0xe0, 0x4b, 0x6c, 0x5b, 0xbe, - 0x18, 0xca, 0xaf, 0x92, 0x97, 0xdd, 0x4e, 0x90, 0x28, 0x4e, 0x48, 0x42, 0x62, 0xc3, 0xf1, 0xf7, 0x71, 0x59, 0x2b, - 0x24, 0x2e, 0x4b, 0xf1, 0x52, 0x2d, 0x7b, 0xbf, 0x8f, 0x5d, 0x1a, 0x29, 0x6b, 0xdd, 0xed, 0x8b, 0x0d, 0xa3, 0xaf, - 0x1a, 0x94, 0x32, 0xc4, 0x54, 0x3d, 0xa1, 0xee, 0x41, 0x42, 0x00, 0xc3, 0xc2, 0x23, 0x57, 0x52, 0x9c, 0x48, 0x54, - 0x42, 0x82, 0x61, 0xb1, 0xcb, 0x1b, 0xae, 0x8f, 0xfa, 0x30, 0x6c, 0x00, 0xc4, 0x1b, 0x74, 0x7c, 0x99, 0x51, 0x60, - 0x45, 0x6d, 0x05, 0xe0, 0x44, 0x15, 0x24, 0x98, 0xb1, 0x40, 0x5f, 0xa1, 0x5e, 0x43, 0x55, 0xae, 0x10, 0xbd, 0x9d, - 0x80, 0x41, 0x6e, 0x35, 0x5d, 0xe8, 0xb2, 0x34, 0x7a, 0x1b, 0xb8, 0x29, 0xad, 0x6d, 0xd3, 0xb4, 0x4f, 0x32, 0x0e, - 0x4e, 0xd7, 0xb3, 0x98, 0x12, 0x37, 0xd4, 0x5c, 0x19, 0xbd, 0x26, 0xaa, 0xbb, 0x5b, 0x7d, 0x92, 0xd3, 0xb7, 0xd3, - 0x2e, 0xfa, 0x6e, 0xf6, 0x2b, 0xaa, 0xc4, 0x24, 0x46, 0x6d, 0x58, 0xe9, 0x7e, 0x8d, 0x27, 0x23, 0x14, 0xbc, 0x15, - 0xaf, 0x1b, 0x88, 0x7b, 0xd1, 0x9d, 0xba, 0x9c, 0x08, 0xd2, 0xf9, 0x9b, 0x81, 0xfd, 0xf8, 0x94, 0xc1, 0x0a, 0xf1, - 0xc8, 0xfa, 0x4e, 0x5b, 0x87, 0x86, 0x34, 0xed, 0x92, 0xcf, 0xfd, 0x49, 0xda, 0xd7, 0x25, 0xc9, 0xe3, 0x22, 0xdf, - 0x9e, 0xdd, 0x53, 0x30, 0x15, 0xe0, 0x2c, 0x5a, 0xcf, 0x41, 0xb3, 0x0d, 0xa4, 0x3a, 0x7b, 0x70, 0xc8, 0x9e, 0x4e, - 0x6b, 0xa7, 0xeb, 0xa3, 0x55, 0xd5, 0xc6, 0x45, 0x89, 0x21, 0x81, 0x5f, 0xb1, 0x29, 0x01, 0xe4, 0x40, 0xe4, 0xd1, - 0x6b, 0xe3, 0x4b, 0xe1, 0xfa, 0xf5, 0x12, 0x7d, 0x82, 0x59, 0xb9, 0xfa, 0x07, 0x0d, 0xa9, 0xa4, 0xd5, 0x80, 0x90, - 0x91, 0xfa, 0x8c, 0xf2, 0xcc, 0x0a, 0xee, 0x97, 0xce, 0x17, 0x31, 0x3a, 0x3c, 0xfd, 0x6e, 0x3f, 0x34, 0xf6, 0x2d, - 0x94, 0x17, 0x65, 0xa5, 0x32, 0x73, 0x94, 0x13, 0x80, 0x24, 0x96, 0x3c, 0x25, 0xd2, 0xc6, 0xb7, 0xad, 0x2d, 0x11, - 0xc1, 0x37, 0x7c, 0x88, 0x77, 0xee, 0x05, 0xc7, 0x26, 0x21, 0x81, 0x0a, 0xed, 0x76, 0x01, 0x14, 0x54, 0x90, 0x89, - 0x23, 0xc9, 0xd5, 0xd1, 0x20, 0xb1, 0x3f, 0x56, 0x36, 0x1d, 0x3c, 0x22, 0x92, 0xb5, 0xcd, 0x06, 0xd0, 0x91, 0xc6, - 0xab, 0x4a, 0x92, 0x83, 0x08, 0x4b, 0x00, 0x3a, 0x56, 0xfe, 0x49, 0x4a, 0x5c, 0x4e, 0xd0, 0x85, 0x41, 0xc1, 0x5d, - 0x1a, 0xc6, 0xcd, 0x26, 0xb9, 0xb0, 0x52, 0x01, 0xfd, 0x78, 0xe8, 0xc7, 0x63, 0x0f, 0x45, 0x0a, 0x82, 0x56, 0x88, - 0x87, 0x9c, 0xd2, 0x81, 0x22, 0xfa, 0xa5, 0xfe, 0x71, 0x9b, 0x37, 0xbf, 0x26, 0xe6, 0x46, 0x89, 0x8a, 0xe6, 0x3c, - 0xa6, 0x52, 0xd4, 0xc7, 0x88, 0xc1, 0x3f, 0x66, 0xec, 0xd0, 0x61, 0xa2, 0x92, 0x5e, 0xaa, 0x54, 0xac, 0x83, 0x75, - 0x26, 0x95, 0x02, 0xed, 0xd4, 0xf8, 0xe2, 0x9b, 0x48, 0x12, 0xbc, 0x13, 0xb3, 0xce, 0x20, 0x85, 0x97, 0x2a, 0xac, - 0x95, 0xe8, 0x97, 0x2d, 0x0a, 0xa2, 0xc4, 0x35, 0xb4, 0x0e, 0x69, 0x42, 0x11, 0xec, 0x09, 0x1d, 0x94, 0x68, 0xf9, - 0x87, 0xb6, 0xca, 0x48, 0x82, 0x72, 0xdf, 0xf3, 0xc1, 0xbb, 0xcb, 0x80, 0xf4, 0xf0, 0x51, 0x0f, 0x29, 0x24, 0x16, - 0x3e, 0x61, 0xcb, 0x01, 0x5d, 0xb7, 0x41, 0x52, 0x00, 0xef, 0xaa, 0x62, 0x79, 0xd9, 0x2c, 0x88, 0xbb, 0x93, 0x35, - 0x35, 0x63, 0xbf, 0x4c, 0x60, 0xa7, 0x82, 0xa3, 0xd5, 0xb6, 0x09, 0x6b, 0xa9, 0x96, 0x24, 0xa3, 0x63, 0x81, 0x59, - 0x02, 0x89, 0x10, 0xe9, 0xfe, 0x58, 0x9c, 0x03, 0x31, 0xaf, 0x93, 0xcc, 0x80, 0xf3, 0xd4, 0x2a, 0x47, 0x13, 0x28, - 0x1c, 0xc7, 0x72, 0xbe, 0x26, 0x29, 0xc9, 0x13, 0x0e, 0xb0, 0x1a, 0xaf, 0xb0, 0x8e, 0x82, 0xfb, 0xb8, 0xa6, 0xa4, - 0xcc, 0xee, 0x7f, 0x99, 0xd2, 0xc4, 0x60, 0x57, 0xa2, 0x03, 0x12, 0x40, 0x4a, 0xb3, 0xd4, 0x62, 0xf0, 0x79, 0x44, - 0x3c, 0x16, 0x82, 0x89, 0x88, 0x44, 0xe1, 0x2b, 0x5d, 0xcb, 0xcf, 0xbc, 0x04, 0x84, 0xca, 0x4c, 0x83, 0xce, 0x92, - 0xd7, 0xaa, 0xa4, 0x86, 0xf6, 0x1b, 0xed, 0x46, 0x35, 0x2b, 0x9f, 0x14, 0x3e, 0x64, 0x1d, 0xb9, 0x7f, 0x1a, 0x98, - 0x64, 0xbd, 0xc9, 0x29, 0x95, 0x76, 0x96, 0xaf, 0xfe, 0xf5, 0x05, 0x8a, 0x8d, 0xaa, 0xa3, 0xe9, 0xb6, 0x3e, 0xda, - 0x10, 0x75, 0xf6, 0x11, 0x71, 0xc0, 0x13, 0x56, 0x33, 0x97, 0x5f, 0x65, 0xf8, 0xe1, 0x32, 0x39, 0x20, 0x45, 0x73, - 0x66, 0xa2, 0x6b, 0xfa, 0xef, 0x22, 0x39, 0x70, 0x89, 0xad, 0xc0, 0x14, 0x50, 0x46, 0x15, 0x63, 0x64, 0x39, 0x90, - 0xc4, 0x52, 0xc9, 0xe5, 0x7c, 0x84, 0x16, 0x59, 0x57, 0x4e, 0x19, 0x0a, 0x95, 0xd3, 0xc8, 0x1c, 0x36, 0x29, 0x8e, - 0x61, 0x5e, 0x96, 0xea, 0x79, 0x86, 0x90, 0x26, 0xdd, 0xd5, 0xa7, 0x88, 0x42, 0xcd, 0xaa, 0x7d, 0x17, 0xa6, 0xbe, - 0x08, 0x57, 0x85, 0x01, 0xf2, 0xfc, 0x61, 0x2d, 0xb2, 0xce, 0xa4, 0xf1, 0x62, 0x67, 0xbc, 0xa0, 0xb2, 0x61, 0x24, - 0x59, 0x96, 0x38, 0x28, 0x41, 0xe0, 0x94, 0x90, 0xc6, 0x3e, 0x71, 0xb8, 0x2d, 0x3f, 0x1e, 0x33, 0xb7, 0xe9, 0x50, - 0x46, 0x31, 0xe2, 0xea, 0x49, 0x95, 0x75, 0x0d, 0xe7, 0x21, 0xe6, 0x0f, 0x2f, 0x8b, 0xda, 0x6f, 0xba, 0xd2, 0xe8, - 0xc6, 0xa1, 0xb3, 0x02, 0x6d, 0x4f, 0x27, 0x73, 0x3a, 0x7d, 0x11, 0x57, 0x75, 0x52, 0x10, 0x50, 0x04, 0xc2, 0x1e, - 0x8f, 0xfe, 0x51, 0x1a, 0xed, 0x1f, 0x01, 0x4b, 0xd6, 0x31, 0xd8, 0x93, 0x6a, 0x8f, 0x09, 0x49, 0xcb, 0xdb, 0x1f, - 0x81, 0xb9, 0x52, 0x25, 0xd1, 0x43, 0xf0, 0xe1, 0x08, 0xa5, 0x05, 0x85, 0x64, 0xd3, 0x93, 0x6e, 0x43, 0xa6, 0x09, - 0x98, 0xe8, 0x71, 0x90, 0x67, 0xc3, 0x1b, 0x17, 0x55, 0x88, 0x3e, 0x3e, 0x30, 0xd9, 0xa5, 0x63, 0xb4, 0x69, 0x95, - 0x6d, 0xf6, 0x9f, 0xa1, 0xd8, 0xef, 0xf7, 0xd7, 0xcc, 0xa1, 0x48, 0xef, 0x3a, 0x23, 0x37, 0xb1, 0xe0, 0xfc, 0x14, - 0x25, 0xc6, 0xb3, 0xb6, 0x34, 0xa4, 0xe5, 0x10, 0x45, 0x48, 0x0e, 0x1d, 0x82, 0xbe, 0x0c, 0x19, 0x56, 0x57, 0xe8, - 0xf0, 0x2d, 0xfd, 0x82, 0x43, 0x26, 0x29, 0x39, 0xd2, 0x64, 0xbf, 0x97, 0xc4, 0x64, 0x57, 0xba, 0xa8, 0x40, 0x87, - 0xd5, 0xb4, 0x13, 0x43, 0xb2, 0xd5, 0xbb, 0xda, 0x66, 0xa9, 0xe5, 0x08, 0xee, 0xce, 0x03, 0xc9, 0x1f, 0x81, 0xaa, - 0xe7, 0xd1, 0x19, 0x47, 0x0b, 0x44, 0x9d, 0x4b, 0x92, 0xdb, 0x49, 0x31, 0xc8, 0x26, 0x52, 0x28, 0x90, 0xae, 0x10, - 0x8d, 0x61, 0x31, 0x6d, 0x3f, 0x08, 0x1c, 0x2c, 0x75, 0x9b, 0x64, 0xa4, 0xcf, 0x9d, 0xdd, 0x26, 0xc5, 0x23, 0x54, - 0x1e, 0xb5, 0xee, 0xbb, 0x69, 0x49, 0x90, 0xea, 0x24, 0x4f, 0x10, 0xb4, 0x67, 0x63, 0xef, 0x98, 0x80, 0xf9, 0xde, - 0x54, 0xcc, 0xaf, 0xa7, 0x6e, 0xc2, 0xc2, 0xee, 0x43, 0x8a, 0x5b, 0x66, 0x76, 0xf2, 0x9d, 0xf9, 0x1c, 0x69, 0xce, - 0x0c, 0x9d, 0xd4, 0x29, 0x24, 0xb3, 0xb1, 0xb7, 0xf4, 0x17, 0xa4, 0x79, 0x77, 0x2f, 0x3a, 0x94, 0x4d, 0xf8, 0x3d, - 0x21, 0xb8, 0x1e, 0x91, 0xc3, 0x08, 0xbe, 0xea, 0x90, 0xd8, 0xcd, 0x46, 0x2b, 0x52, 0x68, 0xed, 0x68, 0x88, 0x4b, - 0xb6, 0x7b, 0x37, 0x0b, 0x00, 0x88, 0x90, 0xd3, 0xef, 0x95, 0x86, 0x8c, 0x2d, 0xfd, 0xe2, 0x8c, 0xad, 0x14, 0xe8, - 0x59, 0x2d, 0xe2, 0x09, 0xaf, 0x09, 0x29, 0x41, 0x67, 0x85, 0x63, 0x86, 0xea, 0x43, 0xd0, 0xce, 0x1b, 0x4a, 0xb6, - 0x74, 0x30, 0x9c, 0xb8, 0x86, 0x92, 0x2d, 0x8c, 0x18, 0x1f, 0xba, 0xd9, 0x7b, 0x5a, 0x24, 0x43, 0xc1, 0x8f, 0x54, - 0x11, 0xe5, 0x22, 0x6a, 0x46, 0x68, 0x6c, 0x69, 0x30, 0x8a, 0x36, 0x9c, 0x9b, 0x77, 0x57, 0x04, 0x71, 0xd9, 0x27, - 0x56, 0x52, 0xc4, 0x8f, 0x83, 0xc4, 0xe9, 0x57, 0xab, 0x11, 0xf4, 0x32, 0x67, 0xa1, 0x34, 0xbe, 0x29, 0x85, 0x79, - 0xe4, 0x81, 0xc1, 0xb2, 0xb5, 0x0d, 0xd3, 0x3e, 0x69, 0x59, 0x3d, 0x5f, 0x55, 0x03, 0xdb, 0x20, 0x1c, 0xb5, 0x2c, - 0x1d, 0xeb, 0xe7, 0xb3, 0x8a, 0x5e, 0x37, 0xf2, 0xaf, 0x56, 0xac, 0xc5, 0x17, 0x20, 0x3b, 0x63, 0x98, 0xcd, 0x98, - 0x34, 0x2a, 0xa0, 0x16, 0x92, 0x29, 0x6b, 0x8b, 0x8a, 0xa7, 0x49, 0x09, 0x1b, 0x1a, 0x70, 0x34, 0x2d, 0x0b, 0xe9, - 0xc5, 0xeb, 0xa1, 0x7d, 0x70, 0xd6, 0xe1, 0x73, 0xcb, 0xd2, 0x23, 0x58, 0x0d, 0x78, 0x8d, 0x88, 0x12, 0x44, 0x2a, - 0xa4, 0x44, 0x85, 0x94, 0x43, 0x15, 0xd3, 0x41, 0xa7, 0x5c, 0x53, 0x67, 0xa5, 0x95, 0x79, 0x97, 0xc6, 0xf8, 0xd3, - 0x22, 0xa4, 0xb0, 0xae, 0x80, 0xc1, 0xa2, 0xf8, 0x0d, 0x04, 0xc0, 0x8b, 0x35, 0xd3, 0x33, 0x31, 0x30, 0xc7, 0x4b, - 0x5a, 0xde, 0x4b, 0x13, 0x66, 0xb1, 0x74, 0x63, 0x53, 0xa8, 0x8f, 0x8c, 0x42, 0x7a, 0xce, 0x25, 0x20, 0xea, 0xa6, - 0xc7, 0x97, 0xd9, 0x7a, 0xcf, 0xb8, 0x24, 0xff, 0x75, 0xbe, 0xdd, 0x9b, 0x15, 0x0e, 0xcf, 0x3d, 0x72, 0x38, 0x70, - 0x06, 0xa9, 0x48, 0x63, 0x06, 0x39, 0x05, 0x2f, 0x7a, 0x85, 0x19, 0x7f, 0xa4, 0x2b, 0x59, 0x22, 0x0a, 0x4f, 0x00, - 0x7f, 0x57, 0x2d, 0x42, 0xb7, 0x07, 0x84, 0xef, 0x42, 0xc6, 0x67, 0x35, 0x4c, 0xf2, 0x47, 0x18, 0x23, 0xf1, 0xe5, - 0x7b, 0x70, 0x53, 0x99, 0x8c, 0x6f, 0x7e, 0xcb, 0x92, 0x40, 0x65, 0x19, 0x4c, 0x53, 0x83, 0x92, 0x3a, 0xfb, 0x04, - 0x79, 0xe4, 0xbc, 0xaa, 0x1b, 0xa6, 0x4e, 0x9a, 0x49, 0x1e, 0xf4, 0x41, 0xa6, 0x08, 0x44, 0xa7, 0x8b, 0x61, 0xe4, - 0x81, 0x10, 0x00, 0xcf, 0x11, 0x88, 0xb4, 0x04, 0xce, 0x00, 0x8e, 0xe9, 0x9c, 0x0c, 0x1a, 0x91, 0xd1, 0xf8, 0xa9, - 0x51, 0x84, 0x8a, 0x54, 0xae, 0x63, 0xc7, 0xe1, 0x68, 0x89, 0x68, 0x94, 0xdf, 0x40, 0x31, 0x05, 0xff, 0xd2, 0xb8, - 0xb5, 0x69, 0xd7, 0x7b, 0xe2, 0x19, 0xc6, 0x96, 0xa6, 0x99, 0xa6, 0x45, 0xd1, 0x48, 0xdd, 0x67, 0x0c, 0x57, 0x2c, - 0x41, 0x9b, 0x84, 0xa2, 0x0c, 0xa3, 0x3a, 0xa6, 0x4a, 0x71, 0x0b, 0x47, 0x68, 0x54, 0xbe, 0xb5, 0x08, 0xed, 0xfd, - 0xc4, 0xf1, 0xe9, 0x32, 0x42, 0x5a, 0x9f, 0x1f, 0xbd, 0x2c, 0x30, 0xfd, 0x32, 0x9c, 0xa1, 0xaf, 0x44, 0x44, 0x34, - 0xad, 0x02, 0x3b, 0x1c, 0xe8, 0x6a, 0xc3, 0x4b, 0x73, 0x17, 0xb7, 0x35, 0xd1, 0x83, 0x33, 0xf6, 0x54, 0x86, 0xf4, - 0xed, 0x99, 0xc8, 0xba, 0x28, 0xea, 0xf6, 0xb7, 0x93, 0xaf, 0xe1, 0xb1, 0xf9, 0x78, 0x4c, 0xea, 0x14, 0xe5, 0x6b, - 0xa2, 0xd6, 0xea, 0x5a, 0x1f, 0x82, 0x99, 0x79, 0xf4, 0x5c, 0x31, 0x19, 0xe3, 0xd4, 0x8c, 0x8c, 0xac, 0xef, 0x59, - 0xe2, 0xc5, 0x36, 0xf1, 0x3b, 0x85, 0xe4, 0x47, 0xc7, 0x19, 0xd2, 0x88, 0x82, 0xa0, 0xca, 0xfc, 0x8a, 0x42, 0x19, - 0x18, 0xe9, 0xe7, 0xb6, 0xf6, 0x03, 0x72, 0xc5, 0x28, 0x96, 0xf1, 0x6c, 0x33, 0x3e, 0xe5, 0xea, 0x1f, 0x57, 0x34, - 0xc8, 0xb2, 0xb4, 0xdf, 0x89, 0xa7, 0x6d, 0x1e, 0xda, 0x66, 0x5e, 0xd9, 0x24, 0x02, 0x78, 0x95, 0x26, 0xd9, 0xf6, - 0x70, 0xaa, 0xf5, 0x47, 0xe0, 0x57, 0x5e, 0x41, 0x80, 0xcb, 0x49, 0x58, 0xb9, 0x8b, 0x02, 0x45, 0xb5, 0x2d, 0xb8, - 0x7c, 0xb0, 0x4b, 0x9f, 0x47, 0xb1, 0x44, 0x36, 0xf7, 0xc0, 0x6c, 0x51, 0x44, 0x78, 0x4a, 0xbd, 0xad, 0x51, 0xef, - 0xdf, 0x4d, 0x11, 0x1f, 0x71, 0x24, 0x77, 0x27, 0xab, 0x6e, 0x1c, 0x95, 0x47, 0x5a, 0x28, 0xfd, 0x00, 0x2f, 0x2e, - 0x9a, 0x4b, 0x97, 0x8a, 0xc7, 0x5e, 0x0a, 0xd9, 0x46, 0xc2, 0xdc, 0x22, 0x4e, 0x6d, 0xfb, 0x6a, 0xf2, 0xfd, 0x5c, - 0xd0, 0x24, 0x31, 0xeb, 0x4b, 0x97, 0xd6, 0x86, 0x4f, 0x32, 0xbd, 0xb3, 0xcf, 0x7a, 0xf6, 0x64, 0xce, 0xe4, 0xc6, - 0xe0, 0x39, 0xa8, 0xfa, 0xbd, 0xfd, 0x94, 0xba, 0x6e, 0x78, 0x94, 0xc4, 0x94, 0x26, 0x7f, 0xe1, 0x4e, 0x92, 0xe9, - 0xae, 0x33, 0x1f, 0x25, 0xdd, 0x37, 0x1c, 0xce, 0xde, 0xdf, 0xc6, 0x5d, 0x81, 0x54, 0x16, 0x1f, 0x43, 0x24, 0x3c, - 0xf1, 0xeb, 0xad, 0x31, 0xe0, 0xa1, 0x40, 0xc0, 0x83, 0x4a, 0xba, 0x59, 0xac, 0x15, 0x1d, 0xe7, 0x74, 0xff, 0x66, - 0x13, 0xce, 0x0a, 0xc3, 0x93, 0x1c, 0x27, 0xb1, 0xcb, 0xab, 0xdc, 0x4e, 0xa5, 0xad, 0x7e, 0x9a, 0x6c, 0xc0, 0x5b, - 0x68, 0x43, 0xd1, 0x72, 0x7c, 0x86, 0x5d, 0xf5, 0x43, 0x53, 0xf9, 0x27, 0xa1, 0x14, 0xc7, 0x36, 0x0d, 0x63, 0x0d, - 0xb9, 0xfe, 0xbe, 0x1d, 0xc8, 0xb4, 0x7a, 0xf3, 0xcf, 0xd9, 0xf7, 0xea, 0xed, 0x58, 0x37, 0x3c, 0x91, 0xde, 0x0e, - 0xfe, 0x7a, 0x48, 0x8a, 0x62, 0x79, 0x7b, 0x55, 0xfd, 0xd7, 0x16, 0xbf, 0x7e, 0xac, 0x2e, 0x6b, 0x2c, 0x96, 0xf9, - 0xf8, 0xb2, 0x1a, 0x4f, 0x2d, 0xef, 0xdf, 0x4e, 0xf5, 0x87, 0x2f, 0x3f, 0xf5, 0x1a, 0xb8, 0x3a, 0x73, 0x9e, 0xa4, - 0x57, 0x14, 0xfb, 0x28, 0x57, 0xc1, 0x4b, 0xf8, 0x20, 0x3f, 0x6d, 0x8f, 0xeb, 0xa7, 0xfb, 0x65, 0x3d, 0x1f, 0x68, - 0xc9, 0xe3, 0x66, 0xeb, 0xed, 0x8d, 0xe6, 0xd5, 0x5e, 0xa6, 0x75, 0x0e, 0x1b, 0x13, 0x7c, 0x28, 0xb7, 0x8a, 0x82, - 0xf1, 0x26, 0x20, 0xf9, 0x03, 0x31, 0xaf, 0xde, 0x36, 0xbb, 0xbe, 0xfc, 0x58, 0xac, 0x59, 0x5e, 0x61, 0x01, 0x96, - 0x35, 0x1a, 0x9a, 0xb3, 0x01, 0x67, 0x49, 0x7a, 0xaf, 0xae, 0xce, 0x9c, 0xe0, 0x9c, 0xc9, 0xed, 0x4d, 0xfc, 0xc7, - 0x4f, 0x53, 0x6d, 0xce, 0x32, 0xcb, 0xe1, 0x2f, 0x8e, 0x62, 0x67, 0x71, 0xd8, 0x6e, 0xc0, 0xfa, 0xaa, 0xe3, 0x2d, - 0xaa, 0xca, 0x56, 0xe7, 0x62, 0x26, 0x4b, 0x44, 0xe5, 0x76, 0xd2, 0xe1, 0x40, 0x37, 0x73, 0x6b, 0x1f, 0xf8, 0xdf, - 0x63, 0x17, 0x2a, 0x9d, 0xc2, 0x3f, 0x97, 0x47, 0x05, 0x17, 0x72, 0x9b, 0x6c, 0x2e, 0xb9, 0x91, 0xee, 0x58, 0x9f, - 0xbc, 0xb1, 0x33, 0x13, 0x46, 0x33, 0x11, 0x56, 0xd8, 0x0f, 0x47, 0xc0, 0x3d, 0x2e, 0x18, 0x7b, 0x2e, 0xfc, 0xd6, - 0xc5, 0x96, 0xbd, 0x77, 0x7d, 0x36, 0xf9, 0x28, 0x64, 0x01, 0xfb, 0x0d, 0x81, 0x1d, 0x68, 0xdc, 0x1c, 0x47, 0x3b, - 0x24, 0xeb, 0x08, 0xe6, 0xa2, 0x5b, 0xc9, 0x56, 0x06, 0xbf, 0x55, 0xb8, 0x9f, 0xdc, 0x05, 0x20, 0x69, 0xf5, 0xee, - 0xc7, 0x5e, 0xdf, 0x7f, 0xd5, 0xcf, 0x5b, 0xbd, 0xca, 0xd8, 0x3e, 0x19, 0xf8, 0x48, 0x83, 0xae, 0x77, 0xc3, 0xcb, - 0x95, 0x6a, 0xa2, 0xcd, 0xb8, 0x59, 0x5e, 0xc9, 0xe8, 0x0d, 0xe9, 0xda, 0xee, 0x3c, 0x54, 0x27, 0x37, 0x5e, 0xb6, - 0x4c, 0x06, 0x78, 0x55, 0x67, 0x33, 0xf9, 0x05, 0x62, 0x7d, 0x7d, 0xd3, 0xc5, 0x16, 0x9a, 0xd9, 0x23, 0xd4, 0x09, - 0x7f, 0xbd, 0x8c, 0xa2, 0x52, 0x24, 0x7a, 0x69, 0x76, 0xf2, 0x78, 0xde, 0x1b, 0x6f, 0x0c, 0x59, 0x4e, 0xa6, 0x87, - 0x20, 0xd1, 0x47, 0xf4, 0x92, 0xc5, 0xf5, 0x0e, 0x83, 0xcf, 0x73, 0x94, 0x66, 0xd9, 0xb0, 0xf2, 0x45, 0xfc, 0x8c, - 0x8e, 0x25, 0x1b, 0xf9, 0xb8, 0x85, 0xad, 0x64, 0xd9, 0xe6, 0x7e, 0xd7, 0x3b, 0x5b, 0xd5, 0xcb, 0x37, 0x1b, 0xcb, - 0xee, 0x58, 0x79, 0x7e, 0x51, 0xa9, 0x59, 0x0a, 0x3b, 0x7c, 0x8e, 0x47, 0x6b, 0xc1, 0x92, 0xdb, 0xbc, 0x1c, 0x21, - 0xbd, 0x97, 0xa4, 0xbf, 0x76, 0xb2, 0x14, 0xc9, 0x87, 0xb2, 0xac, 0xf2, 0x1f, 0x92, 0x24, 0x62, 0x55, 0x14, 0xa6, - 0x1c, 0x64, 0x9e, 0x7c, 0x28, 0x37, 0xbd, 0x78, 0xe7, 0xab, 0xc2, 0x4f, 0x75, 0xd8, 0x4b, 0xeb, 0x37, 0x20, 0x0a, - 0x66, 0x01, 0x7c, 0x21, 0xee, 0x45, 0xb3, 0xeb, 0x99, 0x3c, 0x06, 0x09, 0xf4, 0x39, 0xf0, 0x0f, 0x3e, 0xfa, 0x01, - 0x05, 0x9f, 0x8b, 0x0e, 0x8c, 0x00, 0x00, 0x72, 0xc7, 0xa1, 0xec, 0xf9, 0xbd, 0x29, 0x57, 0x28, 0xab, 0xa1, 0x5a, - 0x9b, 0xfc, 0x49, 0x73, 0x1d, 0x09, 0xce, 0x7b, 0xa4, 0xc8, 0x3f, 0xf1, 0x7a, 0x36, 0x69, 0x1a, 0x62, 0x17, 0x15, - 0x0a, 0x4c, 0x54, 0xe9, 0x24, 0x4f, 0x95, 0x2c, 0xb5, 0x2a, 0x59, 0xa3, 0x07, 0x1f, 0x76, 0xeb, 0xb5, 0x79, 0x55, - 0x1e, 0x92, 0x9f, 0x25, 0x10, 0xa6, 0x7f, 0xa5, 0x65, 0xb9, 0xbd, 0xa1, 0x4a, 0xe5, 0x20, 0x0f, 0xc1, 0x23, 0xab, - 0xfc, 0xba, 0x7c, 0xe2, 0xc1, 0x8d, 0x28, 0x7f, 0xa5, 0xcf, 0x21, 0xa8, 0x04, 0xfe, 0x5b, 0x36, 0x9b, 0xbe, 0xf2, - 0xf9, 0xf6, 0xc7, 0x73, 0x41, 0x04, 0x4f, 0x97, 0xec, 0x0d, 0xe6, 0x6a, 0x6f, 0x50, 0x9a, 0xac, 0x7b, 0x67, 0x43, - 0xcc, 0x05, 0xcb, 0x1f, 0xd1, 0xe6, 0xdf, 0x27, 0xdf, 0xec, 0x85, 0x96, 0x40, 0xd3, 0x7a, 0x0d, 0xd0, 0x32, 0xcf, - 0x3b, 0x32, 0xd8, 0x23, 0xef, 0x52, 0x1e, 0x56, 0x1c, 0x57, 0xbd, 0xe4, 0xef, 0xe1, 0x2d, 0xdc, 0x69, 0x1b, 0x29, - 0x12, 0xf5, 0x3a, 0xab, 0x14, 0x81, 0xf3, 0x1e, 0xbe, 0x9a, 0xf3, 0x74, 0xaa, 0x21, 0xf1, 0x4b, 0xc7, 0xdc, 0x86, - 0x0a, 0xeb, 0x65, 0x31, 0xbb, 0xbf, 0x7b, 0x83, 0xdc, 0x12, 0x9b, 0xdf, 0x3b, 0x6f, 0x01, 0x48, 0xb5, 0xfa, 0x94, - 0xb2, 0x23, 0xff, 0x51, 0xaa, 0x1d, 0xf0, 0x2a, 0x1f, 0xa8, 0xa0, 0x1a, 0x33, 0xa4, 0xb4, 0xe2, 0xaa, 0x13, 0x49, - 0xd0, 0xdb, 0xa2, 0x64, 0xb0, 0x91, 0x29, 0xec, 0x43, 0x5e, 0x94, 0xe8, 0xfb, 0x52, 0xc9, 0x72, 0x0b, 0xaf, 0x1c, - 0xcb, 0x5a, 0xee, 0x1b, 0x25, 0x7e, 0x98, 0x20, 0x3f, 0x0d, 0x2f, 0x32, 0xf2, 0xe4, 0x6e, 0x71, 0x24, 0xf0, 0xf1, - 0x49, 0x26, 0x82, 0x7d, 0x03, 0x79, 0x92, 0x5c, 0x3c, 0x89, 0x44, 0x65, 0x62, 0xbb, 0xe4, 0xe8, 0xe8, 0xde, 0x24, - 0xa9, 0xd7, 0xd2, 0xe8, 0x50, 0xe8, 0xb8, 0x8d, 0x42, 0x6d, 0x1d, 0xcf, 0xd9, 0x94, 0x8d, 0x47, 0x77, 0xc9, 0x76, - 0x11, 0xb7, 0x87, 0xd2, 0x08, 0x95, 0xd4, 0x26, 0xe8, 0x58, 0x9a, 0x06, 0x91, 0xc7, 0x03, 0x5b, 0x84, 0x88, 0x3e, - 0x9b, 0x4a, 0x67, 0x39, 0xc4, 0x6a, 0x3b, 0x1f, 0x58, 0x8e, 0x2d, 0x87, 0x2c, 0x09, 0x28, 0x9a, 0x95, 0x22, 0xe1, - 0x60, 0xe0, 0x38, 0x9a, 0xa3, 0x4a, 0x81, 0x31, 0x73, 0x35, 0x87, 0x9d, 0xaf, 0x33, 0x72, 0x2c, 0x8d, 0x34, 0x1b, - 0xbe, 0x2e, 0x45, 0x77, 0x6b, 0xeb, 0x63, 0x6d, 0x44, 0x32, 0xb2, 0xc9, 0x9e, 0xcb, 0xdc, 0x13, 0x56, 0xe9, 0xa9, - 0xdc, 0x8d, 0x95, 0x94, 0x15, 0xe7, 0xf9, 0x64, 0xb4, 0xdb, 0x90, 0x45, 0xab, 0x06, 0xab, 0xf1, 0x25, 0xd3, 0xee, - 0xb3, 0x2f, 0xb5, 0x0d, 0xda, 0x6a, 0x52, 0x17, 0x68, 0xce, 0xc3, 0xba, 0xc2, 0xdf, 0xc8, 0x13, 0x38, 0x93, 0x9e, - 0xbd, 0xea, 0x2e, 0x3f, 0xaa, 0x51, 0xda, 0xee, 0x2d, 0x5c, 0x31, 0x8f, 0xb9, 0x0a, 0xc1, 0xae, 0xd5, 0x44, 0x4f, - 0x66, 0x90, 0xc3, 0xf7, 0x04, 0x5c, 0x8e, 0xfc, 0x66, 0x80, 0xed, 0xd9, 0x38, 0x97, 0xb4, 0x53, 0xde, 0x27, 0x2d, - 0x45, 0x7e, 0xc6, 0x4d, 0xd6, 0x9c, 0x28, 0xff, 0x97, 0x42, 0xac, 0xb2, 0xe0, 0x0b, 0xeb, 0x23, 0xeb, 0xef, 0xd1, - 0xa9, 0x4e, 0x79, 0xeb, 0xd5, 0x8c, 0x7e, 0x2b, 0x2a, 0x4c, 0xd8, 0x3b, 0xa0, 0xc1, 0x64, 0xc7, 0x5a, 0x0d, 0xed, - 0x6e, 0xd9, 0xd1, 0xa2, 0x38, 0x6d, 0xcf, 0x68, 0x55, 0x7b, 0x21, 0x23, 0x1e, 0x7e, 0x6e, 0x34, 0x12, 0x8b, 0xa4, - 0x58, 0x40, 0xe7, 0x2b, 0xea, 0xfd, 0xb5, 0x9c, 0xd3, 0x93, 0xd6, 0x64, 0xf0, 0xa8, 0x33, 0xa7, 0xce, 0x55, 0x9f, - 0xbd, 0xde, 0xd5, 0xa1, 0xf2, 0x27, 0xe2, 0xfa, 0x13, 0x34, 0x55, 0x55, 0x73, 0xd7, 0x4a, 0x90, 0x9a, 0xb2, 0x6c, - 0xe2, 0xc8, 0xa7, 0xc9, 0xf7, 0xf9, 0x4c, 0x87, 0xec, 0xc3, 0x75, 0xa8, 0x8a, 0x17, 0x23, 0xb1, 0x05, 0x21, 0xb9, - 0x70, 0x82, 0x65, 0x51, 0xdf, 0x63, 0x29, 0x8a, 0xa3, 0x84, 0x92, 0xe1, 0x05, 0x8a, 0xc0, 0x51, 0x0b, 0x0c, 0x94, - 0xe4, 0x44, 0x1d, 0x1a, 0xc9, 0xce, 0xd3, 0xc1, 0x8b, 0x4f, 0x6c, 0xf2, 0x2d, 0xa1, 0x43, 0x3a, 0x43, 0x79, 0x05, - 0xdf, 0x8b, 0x77, 0x45, 0x9e, 0x30, 0xd5, 0xd2, 0x31, 0xcf, 0xbd, 0x66, 0xfa, 0xd8, 0x35, 0x78, 0x21, 0xba, 0x0e, - 0x97, 0x67, 0x48, 0x19, 0xab, 0x48, 0xd5, 0x34, 0xed, 0x87, 0x77, 0x87, 0x28, 0x49, 0x55, 0xb6, 0xdb, 0x7b, 0xa7, - 0x2d, 0x44, 0x09, 0xa4, 0xc9, 0xba, 0x0d, 0x8c, 0x64, 0x7a, 0xce, 0xb1, 0x2a, 0x45, 0x31, 0x86, 0x19, 0x82, 0x5c, - 0xe8, 0xb0, 0x15, 0x92, 0x4a, 0x3f, 0xca, 0x22, 0xe8, 0x26, 0x9d, 0xf1, 0x60, 0x96, 0x81, 0x51, 0xe1, 0xf8, 0xa4, - 0xde, 0x85, 0x77, 0x6d, 0x73, 0x72, 0x68, 0x15, 0x69, 0x02, 0x75, 0x2c, 0x90, 0x1e, 0xe5, 0x6f, 0xbe, 0x7b, 0x8c, - 0x6b, 0xd3, 0xaf, 0x8b, 0x55, 0x21, 0x33, 0x76, 0x02, 0x07, 0x90, 0x69, 0xbb, 0xf9, 0x9d, 0x7d, 0xa3, 0x24, 0x05, - 0x23, 0xad, 0xe7, 0x9e, 0x19, 0x5c, 0x8c, 0xc9, 0x42, 0xb4, 0xad, 0x76, 0xd3, 0x47, 0x07, 0x6d, 0x64, 0xe5, 0x35, - 0x00, 0xab, 0x24, 0x2d, 0x39, 0x1b, 0xc0, 0xc2, 0x6a, 0xc7, 0x36, 0x4c, 0x2f, 0x0d, 0x80, 0x4d, 0xbf, 0x05, 0x58, - 0xc0, 0x0b, 0xa2, 0xfd, 0xcc, 0xbc, 0xd2, 0x2c, 0xc2, 0x03, 0xef, 0x13, 0x80, 0x0d, 0xb1, 0x52, 0x51, 0x2d, 0x16, - 0x0b, 0xaf, 0x14, 0x0b, 0x5a, 0xd3, 0xcc, 0x96, 0x4e, 0xc0, 0xfa, 0x72, 0x47, 0xa1, 0x3a, 0xe5, 0xaf, 0xa8, 0xc4, - 0x9a, 0x43, 0x55, 0xf1, 0xd1, 0xfd, 0x66, 0x7d, 0x9a, 0x62, 0x91, 0xda, 0xa7, 0xd6, 0x93, 0x0c, 0xf0, 0x76, 0x8b, - 0xe7, 0xc0, 0x4b, 0xcb, 0xa2, 0xf7, 0xbd, 0x9d, 0xb5, 0x64, 0x51, 0xdd, 0x72, 0x7c, 0xd5, 0xca, 0xe4, 0x74, 0x25, - 0x97, 0x82, 0x32, 0x14, 0xf9, 0x9e, 0x27, 0x49, 0x21, 0xae, 0x4f, 0x1f, 0xcc, 0x7c, 0x84, 0x71, 0x65, 0xc6, 0x8b, - 0x6f, 0xc5, 0xc3, 0x8c, 0x27, 0xa5, 0x48, 0x6a, 0x79, 0x51, 0x01, 0xa9, 0xc6, 0x28, 0xbe, 0x20, 0x43, 0xcf, 0xf9, - 0x7a, 0xd4, 0xa7, 0xb8, 0x73, 0xb6, 0x24, 0x0e, 0xa2, 0x70, 0x98, 0x3e, 0x2b, 0x54, 0x08, 0xfe, 0x3b, 0x20, 0x26, - 0x19, 0x82, 0x00, 0x73, 0x22, 0xa1, 0x0e, 0xb2, 0xf0, 0x84, 0x67, 0x7b, 0xc9, 0x68, 0x25, 0xa3, 0x13, 0xa9, 0x32, - 0x11, 0x51, 0xf9, 0xed, 0xca, 0x26, 0x41, 0xb3, 0xec, 0xa5, 0x28, 0xdc, 0x88, 0x25, 0x11, 0x5c, 0x2b, 0x83, 0x9b, - 0x7e, 0x44, 0xa9, 0xee, 0x96, 0x86, 0xeb, 0x8c, 0x65, 0x72, 0xe1, 0x1b, 0x6f, 0xe8, 0xfa, 0xd2, 0xf5, 0x67, 0xe4, - 0xb6, 0xa8, 0xf0, 0xc9, 0x47, 0xf2, 0xc0, 0xb9, 0xbe, 0x9a, 0x66, 0x3a, 0xc7, 0xbc, 0x8b, 0x50, 0x5c, 0x63, 0xb2, - 0xcd, 0x7e, 0xdb, 0x2c, 0xc1, 0xeb, 0xfc, 0x59, 0xb2, 0x9c, 0xa6, 0x02, 0x76, 0x31, 0xf1, 0x8b, 0xa0, 0x44, 0x1b, - 0xae, 0x7a, 0xff, 0xde, 0xd6, 0x7a, 0xf7, 0xd9, 0x07, 0x1c, 0x16, 0xe5, 0x6f, 0xd4, 0xf6, 0x0c, 0x28, 0xa8, 0xe4, - 0xb9, 0xab, 0xd6, 0x80, 0xb1, 0x1d, 0xc3, 0x0f, 0x51, 0x1f, 0xed, 0x1a, 0xa0, 0xae, 0xd9, 0xca, 0x29, 0x06, 0x63, - 0x00, 0x1d, 0xdd, 0xfa, 0xd4, 0x20, 0x75, 0x58, 0xd8, 0x94, 0xd6, 0xdb, 0x58, 0xbc, 0xd0, 0x22, 0xe6, 0x72, 0x95, - 0x2c, 0xad, 0xf9, 0x1a, 0xfe, 0x37, 0xb8, 0xa6, 0xe0, 0x77, 0x94, 0xbf, 0x92, 0x78, 0xd7, 0x9d, 0xd3, 0x0a, 0x89, - 0x82, 0x79, 0x2e, 0xbc, 0x22, 0xc2, 0x4a, 0x9f, 0x10, 0x73, 0xcc, 0x75, 0x99, 0x93, 0x7d, 0xe1, 0xd0, 0x1a, 0xa9, - 0x43, 0xc0, 0xa5, 0xfb, 0x1e, 0x4f, 0x2f, 0xf8, 0x12, 0x5d, 0x1d, 0xdf, 0xcb, 0x3c, 0x9a, 0x01, 0xab, 0xba, 0x6f, - 0x31, 0x09, 0x53, 0x51, 0x46, 0x09, 0x41, 0xdc, 0x54, 0x22, 0x0b, 0x43, 0xcf, 0x1a, 0x57, 0x1f, 0x3b, 0xad, 0xa7, - 0x0c, 0x00, 0x28, 0x25, 0x09, 0xdd, 0x33, 0x94, 0x31, 0xa3, 0x97, 0x56, 0x81, 0x72, 0xcb, 0xd5, 0xc1, 0xcb, 0xce, - 0x3d, 0x86, 0x81, 0x9d, 0xd9, 0x5a, 0x67, 0x1a, 0x07, 0x22, 0xcb, 0x40, 0x80, 0x38, 0xc8, 0xb7, 0xa9, 0xd2, 0x58, - 0x74, 0x03, 0xd4, 0xb5, 0xa8, 0x0f, 0xd2, 0x8e, 0x2c, 0xc4, 0x19, 0x26, 0x3a, 0x06, 0xf6, 0xa7, 0x97, 0x68, 0xa4, - 0xa4, 0x42, 0xe0, 0x95, 0x31, 0xf3, 0x40, 0x22, 0x7b, 0xa0, 0x1d, 0x94, 0x0d, 0x80, 0x24, 0x67, 0x8e, 0x2b, 0x05, - 0x69, 0x2d, 0x23, 0x96, 0xd0, 0xff, 0x13, 0x2b, 0x8d, 0x32, 0x01, 0xf9, 0xc8, 0xa1, 0x4d, 0x49, 0xe3, 0x79, 0x78, - 0x2d, 0x1c, 0x48, 0x3e, 0x4c, 0x7f, 0x9c, 0x05, 0x8d, 0xc8, 0x94, 0xd3, 0xb9, 0x15, 0x6c, 0xe3, 0x8b, 0x3b, 0x4f, - 0x23, 0x51, 0x61, 0xfa, 0xcc, 0x77, 0x96, 0xb7, 0x93, 0x55, 0x84, 0xe5, 0x8f, 0xb9, 0xec, 0xa7, 0xe8, 0x7f, 0xad, - 0xa2, 0x24, 0xc9, 0x06, 0x5f, 0x2a, 0x99, 0x8e, 0xdb, 0xf3, 0x6b, 0xad, 0x8d, 0x96, 0xee, 0x01, 0xce, 0x78, 0x0f, - 0xaa, 0x3b, 0x12, 0x7a, 0xc8, 0x69, 0xcd, 0x53, 0x87, 0xa8, 0xaa, 0xa0, 0x84, 0x44, 0x63, 0x5c, 0xa4, 0xd6, 0x44, - 0xea, 0x9b, 0xc5, 0xd3, 0x00, 0x92, 0x69, 0x0c, 0x2a, 0xaa, 0xdc, 0x57, 0xcf, 0x6c, 0x5e, 0x4c, 0x4f, 0xa4, 0xc9, - 0x74, 0x41, 0x2e, 0x3f, 0xc5, 0xe8, 0x66, 0x4a, 0xc9, 0xb2, 0x58, 0x86, 0xc3, 0x1e, 0x23, 0xcc, 0x01, 0x43, 0x44, - 0xa5, 0xc2, 0x78, 0xc3, 0xa4, 0xbc, 0x9f, 0x94, 0xfc, 0x69, 0x8a, 0xf3, 0x0b, 0x61, 0xf5, 0x61, 0x5b, 0x07, 0xb8, - 0x3a, 0x07, 0xe5, 0x08, 0xb7, 0x96, 0x07, 0xd8, 0xd8, 0x98, 0x47, 0x7b, 0x39, 0x55, 0x25, 0x22, 0xd3, 0xac, 0x3b, - 0x58, 0x52, 0xde, 0xa4, 0xef, 0x0c, 0x99, 0xb0, 0x75, 0x33, 0x14, 0x41, 0x6e, 0x3c, 0xc9, 0xae, 0xb1, 0xd5, 0x07, - 0x81, 0xb3, 0x7a, 0x44, 0x5e, 0xc6, 0xa3, 0xaa, 0xce, 0xaa, 0xed, 0x94, 0xfc, 0x34, 0xbc, 0x4f, 0xae, 0x3b, 0xc9, - 0x09, 0x95, 0xd5, 0x24, 0x2a, 0x0a, 0x8a, 0xc2, 0xf3, 0x24, 0x20, 0x82, 0xe2, 0x8c, 0xfa, 0xa1, 0x8a, 0xfa, 0xa6, - 0xc8, 0xfe, 0x71, 0xea, 0xd5, 0xbe, 0xe5, 0x25, 0x80, 0x24, 0x3f, 0x93, 0x49, 0x77, 0x8c, 0x7b, 0x67, 0x5d, 0x1e, - 0x3e, 0x4a, 0x14, 0xe6, 0x3e, 0x14, 0x57, 0x31, 0x49, 0x51, 0x2c, 0x01, 0xf7, 0xca, 0x65, 0x2f, 0x1e, 0x49, 0x3a, - 0x41, 0x66, 0xa8, 0x5c, 0x1b, 0xef, 0x9b, 0x7a, 0xa4, 0xea, 0x49, 0x96, 0x02, 0x79, 0xe4, 0xee, 0x77, 0x46, 0x50, - 0xf2, 0xfc, 0xb7, 0xec, 0x8a, 0xd7, 0x49, 0x79, 0x86, 0x36, 0x1b, 0x22, 0x7a, 0x5d, 0x8e, 0x36, 0x05, 0xcc, 0x31, - 0x23, 0x02, 0xfd, 0x73, 0xb0, 0x0a, 0xf9, 0xb2, 0xe3, 0x14, 0xdb, 0x54, 0x01, 0x4a, 0xb9, 0xf7, 0xfd, 0x55, 0x1e, - 0x08, 0xfb, 0x33, 0x92, 0x9c, 0x49, 0x46, 0x44, 0x50, 0xb6, 0xc0, 0x23, 0xb0, 0x2f, 0xa4, 0x8b, 0x13, 0xb5, 0x0a, - 0xfb, 0x82, 0x9a, 0xb3, 0x67, 0xa9, 0x88, 0xea, 0x14, 0x7d, 0xfc, 0xca, 0xb8, 0x60, 0x2e, 0xab, 0x91, 0xb6, 0x22, - 0x5a, 0x9e, 0xaa, 0x04, 0x04, 0xb5, 0x10, 0x0b, 0xb1, 0xa9, 0x80, 0x60, 0x7c, 0xaf, 0xa7, 0x27, 0x18, 0x31, 0x0b, - 0xc5, 0x8b, 0xcc, 0xe5, 0x44, 0xbb, 0xfc, 0x87, 0x9b, 0x30, 0x9d, 0x33, 0x86, 0x34, 0x22, 0xa9, 0x47, 0xd6, 0xef, - 0x5f, 0x2f, 0x2f, 0x54, 0x65, 0x13, 0x49, 0x65, 0x23, 0xee, 0xe6, 0x73, 0x9d, 0x1b, 0xd3, 0x44, 0x70, 0xc5, 0x92, - 0xd9, 0x62, 0xe3, 0xe9, 0xfc, 0xc3, 0x95, 0x59, 0x48, 0xd7, 0x44, 0x39, 0x92, 0xc8, 0x4f, 0x0a, 0xc1, 0x43, 0x8d, - 0xf2, 0x42, 0x18, 0x91, 0xfa, 0x6f, 0x86, 0xdc, 0x75, 0x29, 0xda, 0xd5, 0x46, 0x75, 0xd9, 0x02, 0xd8, 0xd2, 0xd7, - 0x30, 0x32, 0x14, 0x42, 0x47, 0x0c, 0x92, 0xdc, 0xa5, 0x3e, 0x2a, 0x19, 0xc8, 0xa2, 0x2b, 0xcc, 0x40, 0x99, 0x7b, - 0xe8, 0xe4, 0x8d, 0x93, 0x28, 0x01, 0xb9, 0x9f, 0x99, 0x4f, 0xea, 0xec, 0x24, 0xf2, 0x62, 0x2d, 0x05, 0x74, 0xa4, - 0xba, 0x4e, 0x25, 0x56, 0x59, 0xad, 0x84, 0x9e, 0x08, 0x76, 0x17, 0xcd, 0xe0, 0x55, 0x9b, 0xe7, 0xe9, 0xb1, 0xe6, - 0x9f, 0xc7, 0x57, 0x94, 0xb7, 0x35, 0x91, 0x16, 0x74, 0x22, 0xb4, 0xfa, 0xc0, 0x7d, 0xdd, 0x5c, 0xd9, 0x1a, 0xe4, - 0x65, 0x01, 0xd0, 0x72, 0xce, 0x72, 0x7a, 0x12, 0xca, 0xba, 0x79, 0x5e, 0x26, 0x99, 0x7b, 0x15, 0x07, 0x5b, 0x40, - 0x73, 0x01, 0x37, 0xc1, 0x67, 0x1f, 0x27, 0xa4, 0x7e, 0xa8, 0x3c, 0x56, 0x36, 0xdf, 0xd6, 0x60, 0xee, 0xdb, 0xc4, - 0xe9, 0xb0, 0xd9, 0x24, 0x22, 0x26, 0x73, 0x37, 0xb6, 0xde, 0x08, 0x67, 0x2d, 0x54, 0xed, 0x11, 0xf3, 0x84, 0x00, - 0x53, 0xd5, 0x20, 0x7c, 0xda, 0xc7, 0x49, 0x4c, 0x6f, 0x11, 0x15, 0xa0, 0x5c, 0x62, 0x52, 0xaf, 0xdc, 0xa5, 0xa5, - 0xd6, 0xbd, 0x4f, 0x17, 0x58, 0x57, 0xba, 0x78, 0xbc, 0xd3, 0x7d, 0xe0, 0x00, 0x70, 0x3f, 0x83, 0xaa, 0x55, 0x5e, - 0xaa, 0xea, 0x0b, 0x6a, 0x69, 0x82, 0x94, 0x04, 0xbc, 0x55, 0x49, 0xef, 0xe7, 0x99, 0x06, 0x82, 0xe6, 0x6b, 0x64, - 0x75, 0xe4, 0x0b, 0x91, 0xc8, 0x43, 0xcf, 0x4b, 0x7c, 0xbc, 0x08, 0xcf, 0x09, 0x1e, 0xbf, 0x8c, 0xad, 0x0b, 0x3a, - 0x65, 0xfe, 0x20, 0x81, 0xe5, 0x40, 0xed, 0xda, 0xe5, 0xeb, 0x38, 0x11, 0xec, 0x14, 0x05, 0xea, 0x29, 0x2a, 0x40, - 0x83, 0x40, 0xd1, 0x48, 0x0b, 0xe8, 0x24, 0xf9, 0x39, 0xa6, 0x05, 0x84, 0xd4, 0x29, 0x10, 0x31, 0xdf, 0x0e, 0xcb, - 0x11, 0xdc, 0x95, 0x22, 0x27, 0x9e, 0x38, 0x37, 0x6b, 0xe5, 0xcb, 0x7d, 0x88, 0xaa, 0x73, 0x7f, 0x7c, 0x83, 0x3b, - 0x70, 0x15, 0xdb, 0x8d, 0xe3, 0x1f, 0x71, 0xbf, 0x49, 0x16, 0x72, 0x0e, 0x44, 0x8a, 0xbc, 0x1c, 0x11, 0x22, 0x13, - 0x87, 0x3a, 0xdc, 0x84, 0x90, 0x8e, 0x2f, 0xa0, 0x3f, 0x8e, 0x98, 0xc6, 0x56, 0x9d, 0x26, 0x20, 0xe7, 0x3c, 0xbe, - 0x3d, 0x9d, 0xde, 0xba, 0xa8, 0x1e, 0x44, 0xdb, 0x22, 0xe2, 0x87, 0xb6, 0xa8, 0x51, 0xa8, 0x3c, 0x9c, 0x5a, 0x5f, - 0x53, 0xc3, 0x31, 0xc4, 0xe1, 0xdf, 0x06, 0x48, 0x00, 0x85, 0xdd, 0x26, 0xd7, 0x5c, 0xd0, 0xe9, 0x9d, 0xa4, 0x23, - 0xb4, 0xd6, 0xf4, 0x53, 0xb9, 0x6a, 0xd6, 0xc1, 0xca, 0xb4, 0xd3, 0xfb, 0x6c, 0xe3, 0xb6, 0x38, 0x01, 0x41, 0xb4, - 0xd2, 0xeb, 0x9b, 0x30, 0x61, 0x89, 0x31, 0x06, 0xde, 0x17, 0x62, 0xce, 0x53, 0x98, 0x49, 0xcc, 0xc7, 0x70, 0xb4, - 0x3a, 0x8b, 0x77, 0x6e, 0xd1, 0xa5, 0xbd, 0xd1, 0x9b, 0x36, 0x92, 0xa9, 0x84, 0x8e, 0x05, 0xf0, 0xc7, 0xe9, 0xa8, - 0x1d, 0x71, 0x07, 0x04, 0xd8, 0xca, 0x12, 0xe3, 0xd2, 0x2d, 0xd8, 0xaa, 0x6c, 0xf9, 0xb4, 0x71, 0xae, 0xdc, 0xcf, - 0xd6, 0x2e, 0x74, 0x44, 0x70, 0x58, 0x97, 0x34, 0x07, 0xe6, 0x63, 0xc1, 0x5c, 0x8a, 0x8b, 0xd5, 0x4e, 0x01, 0x12, - 0xb4, 0x92, 0x3c, 0x5c, 0x66, 0x48, 0x7a, 0x7c, 0xa2, 0x2e, 0x12, 0x72, 0xc6, 0x8d, 0xb6, 0x06, 0xec, 0xe0, 0xdd, - 0x5e, 0x8f, 0xb4, 0xee, 0xbc, 0x45, 0xde, 0x8b, 0xe2, 0x05, 0xa4, 0x9a, 0x02, 0x71, 0x65, 0x83, 0x20, 0xed, 0x3a, - 0x25, 0xac, 0xbf, 0x19, 0x2c, 0x8d, 0xdb, 0x77, 0x6d, 0x4a, 0x0f, 0x7a, 0x76, 0xa6, 0x87, 0x5c, 0xf8, 0xb3, 0xa2, - 0x6f, 0x5f, 0x79, 0xcb, 0x36, 0xec, 0xa7, 0xa5, 0x00, 0xb2, 0xba, 0xb8, 0x1b, 0xe7, 0xbc, 0x60, 0x8b, 0xa5, 0xc9, - 0xe9, 0xab, 0x65, 0x85, 0x9a, 0xc0, 0x1e, 0x7c, 0xa0, 0x65, 0xa4, 0x52, 0x5f, 0x29, 0x29, 0x5a, 0x1e, 0x1a, 0x93, - 0x6c, 0x6d, 0x6a, 0x85, 0xb8, 0xaa, 0x06, 0xab, 0xea, 0xe1, 0x12, 0x4b, 0x4b, 0xdb, 0x52, 0x0b, 0xcd, 0x75, 0xef, - 0x05, 0x98, 0x7c, 0x8f, 0x1e, 0xb1, 0xbc, 0x00, 0xba, 0xfb, 0x85, 0x7c, 0x19, 0x87, 0x41, 0x5a, 0x54, 0x41, 0x00, - 0xe9, 0x75, 0x1d, 0xc3, 0xa6, 0x61, 0x8d, 0x89, 0x0e, 0x8b, 0x3e, 0x8d, 0x40, 0x45, 0xa8, 0x81, 0x21, 0xd8, 0x42, - 0xae, 0x4c, 0xc5, 0xd2, 0xa9, 0x97, 0xc9, 0xe2, 0xd2, 0xe7, 0x5e, 0x6c, 0x6b, 0xd2, 0x15, 0xb3, 0x54, 0x41, 0x5c, - 0x1a, 0x75, 0xbd, 0xd1, 0x37, 0x6a, 0x79, 0xd0, 0x35, 0xde, 0xe3, 0x26, 0x19, 0xd6, 0xa6, 0x72, 0x7d, 0x94, 0x6c, - 0xb7, 0xff, 0x59, 0xb9, 0x44, 0xb5, 0xe4, 0x2c, 0xad, 0xb1, 0xea, 0x61, 0x8b, 0x02, 0x5c, 0xbe, 0xe3, 0x4e, 0xc6, - 0x00, 0x59, 0x8e, 0xb4, 0x61, 0x6e, 0x1d, 0xce, 0x65, 0x1b, 0x68, 0xfb, 0xcd, 0xa7, 0x92, 0x60, 0xeb, 0x57, 0x4f, - 0xa7, 0xb1, 0x4d, 0x91, 0xc3, 0x28, 0x70, 0x14, 0x9e, 0xbb, 0xe7, 0xbc, 0x5a, 0x29, 0xe3, 0x12, 0xdb, 0xed, 0x73, - 0xd3, 0x4f, 0x5e, 0xd9, 0x86, 0xf5, 0x14, 0x5f, 0x8f, 0x91, 0x8d, 0xbd, 0xe7, 0xc9, 0x7a, 0x32, 0x16, 0x77, 0x0c, - 0x80, 0x8b, 0x92, 0x62, 0xb8, 0x5b, 0xc1, 0xc5, 0x83, 0x5f, 0xf8, 0x7c, 0x2a, 0xa7, 0x9b, 0x34, 0xee, 0xcd, 0xbf, - 0xed, 0xed, 0x85, 0x07, 0xcd, 0x24, 0x7d, 0x99, 0xa5, 0xcb, 0x2a, 0xb9, 0x16, 0xc8, 0xb5, 0x1b, 0x9e, 0x8b, 0x72, - 0xbd, 0x69, 0x6b, 0x23, 0x4c, 0x50, 0xbc, 0x1c, 0xf8, 0xdb, 0xbb, 0xf8, 0xed, 0xb9, 0xec, 0xf9, 0xb9, 0xb7, 0x68, - 0x08, 0x31, 0xdf, 0xbc, 0x8f, 0x2a, 0x2b, 0x8e, 0x63, 0xe2, 0x7d, 0x3e, 0x34, 0xde, 0xeb, 0x1a, 0x2d, 0x63, 0xae, - 0xf0, 0xf3, 0x18, 0xaa, 0xda, 0xc7, 0xcd, 0x2d, 0xff, 0x6c, 0x77, 0x9d, 0x95, 0xa2, 0x30, 0x9b, 0xc9, 0xc3, 0xd2, - 0x94, 0xb4, 0x3b, 0x0e, 0x36, 0xdc, 0x3e, 0x2b, 0x00, 0x73, 0x00, 0x2c, 0x8f, 0x74, 0x7d, 0x16, 0x7b, 0x16, 0xca, - 0xb6, 0x8b, 0x38, 0x54, 0x6f, 0x61, 0x57, 0xdc, 0x9c, 0xe5, 0x59, 0xea, 0x6e, 0xe3, 0x0b, 0x03, 0x54, 0x3d, 0xe4, - 0x8e, 0x39, 0x92, 0x96, 0x09, 0xa6, 0x4a, 0x7e, 0xbb, 0x71, 0xdc, 0xcc, 0x19, 0x3b, 0xf1, 0x0c, 0xb3, 0x79, 0xea, - 0x0d, 0x2b, 0x9a, 0xf7, 0xed, 0x2b, 0xf7, 0x34, 0x30, 0xf1, 0xad, 0x8d, 0xca, 0x54, 0xf6, 0x75, 0x00, 0x94, 0x2c, - 0xd1, 0x9f, 0x76, 0x51, 0x5a, 0x57, 0x08, 0xa3, 0xc2, 0xa9, 0xf2, 0x0f, 0xd6, 0x92, 0x56, 0x31, 0x11, 0x8b, 0xa3, - 0x23, 0xcd, 0x19, 0xe0, 0x96, 0x78, 0xcb, 0xa8, 0x03, 0xc5, 0x98, 0xd1, 0xc6, 0x4c, 0xca, 0x6a, 0x8f, 0x66, 0x07, - 0xc2, 0xc8, 0x73, 0x6d, 0x11, 0xe9, 0x28, 0x60, 0xbd, 0x54, 0x70, 0xe0, 0x37, 0xef, 0x55, 0xa0, 0x79, 0xdf, 0xb3, - 0x01, 0xe5, 0x00, 0xee, 0x37, 0x74, 0x94, 0xd4, 0xa6, 0x8d, 0xbf, 0xe4, 0x8a, 0xd1, 0xd5, 0x83, 0x24, 0xd0, 0x36, - 0x63, 0xc0, 0x07, 0x4c, 0xae, 0xa8, 0x42, 0xfa, 0x34, 0x46, 0xde, 0x28, 0x90, 0x9c, 0x63, 0xd3, 0x50, 0x4c, 0x3b, - 0xac, 0x27, 0x91, 0x94, 0x0e, 0x22, 0x64, 0x8a, 0xc5, 0xf4, 0xa0, 0x0e, 0x96, 0x64, 0xa4, 0x75, 0x2a, 0x6f, 0x45, - 0x47, 0xfd, 0x9e, 0x8d, 0xa0, 0x39, 0xb6, 0xac, 0x2a, 0xd4, 0x37, 0xcb, 0x2d, 0x13, 0x95, 0x74, 0xf3, 0x6c, 0x2a, - 0x1f, 0x97, 0x83, 0xc8, 0xa6, 0x69, 0xc7, 0x6f, 0xfb, 0xbc, 0xc7, 0x0c, 0xee, 0x62, 0x84, 0x82, 0xac, 0x6d, 0xc8, - 0x60, 0x8f, 0x3c, 0x5c, 0xd0, 0x2d, 0xfd, 0x40, 0xa1, 0xdf, 0xae, 0x96, 0x00, 0x7e, 0x4a, 0xe0, 0x2b, 0x41, 0x6f, - 0x37, 0xb9, 0x53, 0xbb, 0xce, 0x3d, 0xef, 0x13, 0xd9, 0x0b, 0x27, 0x0f, 0x92, 0x6d, 0x5b, 0xa2, 0x6d, 0xd5, 0x8d, - 0x5b, 0xfe, 0xb1, 0xc3, 0x4f, 0x4a, 0x53, 0x44, 0xad, 0x49, 0xea, 0xb4, 0xb1, 0xdc, 0x12, 0xb5, 0xa3, 0xc1, 0x51, - 0xba, 0x11, 0x5e, 0xb8, 0xdf, 0x86, 0xfd, 0x86, 0x82, 0xb1, 0x1c, 0xbd, 0x72, 0x17, 0x1d, 0x0b, 0x68, 0x1c, 0x29, - 0xe8, 0xd8, 0x1e, 0x47, 0xb5, 0x31, 0x86, 0x72, 0xcc, 0xde, 0x70, 0x0c, 0x65, 0x35, 0x06, 0x6a, 0x63, 0xeb, 0x26, - 0x74, 0x37, 0x9e, 0x88, 0xe4, 0x30, 0xa0, 0x71, 0x40, 0xea, 0x96, 0x19, 0xa9, 0xfc, 0x3a, 0x27, 0x2c, 0x10, 0x83, - 0x3b, 0xf6, 0x78, 0xa1, 0x7d, 0xc1, 0x30, 0xc4, 0x11, 0xe8, 0x16, 0x8f, 0x62, 0xb6, 0xa8, 0x0c, 0xf5, 0xe2, 0xca, - 0x5a, 0x98, 0xc0, 0xda, 0x11, 0xa2, 0x42, 0x7f, 0x6c, 0xf3, 0x5d, 0x3b, 0x14, 0xe4, 0x8a, 0x1f, 0xc6, 0xfe, 0x32, - 0xfd, 0x68, 0xe4, 0xa9, 0xa4, 0xff, 0x22, 0x8c, 0x7e, 0xea, 0x84, 0x95, 0x13, 0x40, 0xf0, 0x27, 0x48, 0x72, 0xdb, - 0x78, 0x3f, 0x4c, 0x69, 0xa6, 0xff, 0xb1, 0xb1, 0xe9, 0x8a, 0xf7, 0x43, 0x3f, 0xcc, 0x1f, 0x3a, 0x51, 0x07, 0xf9, - 0xa7, 0x5f, 0x3c, 0x74, 0x5c, 0x8f, 0xed, 0x63, 0x4c, 0xdd, 0xb1, 0xa5, 0xf9, 0x78, 0xec, 0xda, 0x4b, 0xb6, 0xdb, - 0xf6, 0xe3, 0xf0, 0x64, 0x78, 0x38, 0x64, 0x43, 0x1a, 0xb8, 0xf7, 0xfc, 0x72, 0x8e, 0x3d, 0x4f, 0xde, 0x3d, 0xf4, - 0xe9, 0x81, 0x9c, 0x8b, 0x94, 0x31, 0xd9, 0x2d, 0x9e, 0xb6, 0x5d, 0xa4, 0x34, 0x02, 0xd4, 0xd1, 0x1b, 0xe1, 0x63, - 0x41, 0xd7, 0x24, 0x55, 0xc8, 0xa0, 0x7c, 0x86, 0x49, 0xa3, 0xea, 0x0f, 0xf1, 0x0c, 0x85, 0x88, 0x83, 0xc0, 0x7f, - 0xf9, 0x67, 0x8f, 0xd6, 0x13, 0xa7, 0xd5, 0x69, 0x6d, 0x78, 0xec, 0xf7, 0x65, 0x97, 0xa5, 0x9e, 0x94, 0x51, 0xba, - 0xcd, 0xc4, 0x4b, 0x8c, 0xcc, 0x4d, 0x7e, 0xc8, 0xb1, 0x3d, 0x75, 0x0f, 0x93, 0xff, 0x42, 0x04, 0x45, 0xb8, 0xc7, - 0x02, 0x19, 0xef, 0x21, 0x50, 0x39, 0x15, 0xa2, 0x98, 0x96, 0x8b, 0xb3, 0x05, 0xb0, 0x6b, 0xf4, 0x4b, 0x64, 0x45, - 0x3c, 0x33, 0x9e, 0xdf, 0xb5, 0x36, 0xd7, 0x01, 0xfc, 0x7e, 0x6d, 0xf4, 0x64, 0x46, 0xab, 0x80, 0xac, 0xfb, 0xa0, - 0x0c, 0x2e, 0x09, 0x4f, 0xa5, 0x3d, 0x97, 0xd5, 0x58, 0xd3, 0x7e, 0xa0, 0x57, 0x33, 0xfd, 0x69, 0xfb, 0xac, 0x21, - 0x74, 0x3d, 0x9a, 0x29, 0x05, 0x54, 0xaa, 0x7c, 0x50, 0x66, 0x5f, 0x5f, 0x40, 0x38, 0xa2, 0x55, 0xc8, 0x2f, 0x15, - 0xa7, 0x87, 0xb1, 0x8d, 0x82, 0x20, 0xdf, 0x79, 0x86, 0xc8, 0x0f, 0xc9, 0x13, 0x2a, 0xec, 0xce, 0xfd, 0x02, 0xf4, - 0x45, 0x85, 0xa7, 0xf4, 0xfd, 0x69, 0x8e, 0xdb, 0xd5, 0xbc, 0x8f, 0xef, 0x03, 0x19, 0x25, 0x58, 0x46, 0xba, 0x39, - 0x74, 0xd2, 0xa8, 0x1d, 0x3d, 0xf2, 0x95, 0x48, 0x8e, 0x2e, 0xd0, 0xf4, 0x3d, 0xd6, 0x86, 0x17, 0x49, 0x4a, 0xd0, - 0xa7, 0x72, 0x2d, 0xc9, 0xb0, 0x57, 0x75, 0x60, 0x74, 0x44, 0xde, 0x5e, 0x8a, 0x0d, 0x90, 0x24, 0xd5, 0xd3, 0x12, - 0xa1, 0xfd, 0x50, 0xce, 0x7a, 0x53, 0x7e, 0x89, 0x7b, 0xf1, 0x84, 0x57, 0x46, 0x74, 0xc3, 0x5f, 0x7c, 0x13, 0xe2, - 0x5e, 0x28, 0xee, 0x8b, 0x02, 0x96, 0x25, 0x54, 0x11, 0x41, 0x6f, 0x1a, 0xa8, 0x1c, 0x0c, 0xfd, 0xb1, 0x28, 0xf0, - 0x6c, 0x05, 0x58, 0x96, 0x09, 0x29, 0x03, 0x47, 0x6c, 0x44, 0xff, 0x4a, 0x9a, 0xfa, 0x29, 0xa5, 0xb9, 0x6f, 0x49, - 0xbc, 0xec, 0x17, 0x84, 0x94, 0x37, 0x10, 0x0a, 0x82, 0x96, 0x0a, 0xde, 0x04, 0x29, 0x68, 0x4c, 0x3b, 0xcc, 0x95, - 0x41, 0xd9, 0xe3, 0xb8, 0x01, 0x2e, 0x5f, 0x39, 0xa8, 0x4d, 0xd5, 0xeb, 0x24, 0xb6, 0x2a, 0x6e, 0xf4, 0x9f, 0xe8, - 0xd6, 0xda, 0x0f, 0x07, 0x28, 0x82, 0xb6, 0x28, 0x9b, 0xf4, 0x8a, 0xc6, 0xb3, 0x30, 0x16, 0x96, 0x3d, 0x46, 0x0f, - 0x6a, 0x06, 0x4a, 0x2a, 0xac, 0x36, 0x54, 0x28, 0xe6, 0x53, 0xbb, 0x08, 0xa3, 0xf0, 0x41, 0x53, 0x19, 0x79, 0xf8, - 0xd0, 0x9d, 0x46, 0xef, 0xc6, 0x51, 0x2c, 0x72, 0x43, 0x9b, 0xd7, 0x2c, 0x45, 0xc2, 0xa4, 0x49, 0x3e, 0xbd, 0x6c, - 0xd6, 0xb3, 0x66, 0xd2, 0xb2, 0x15, 0x7a, 0xd5, 0x78, 0x63, 0x20, 0x52, 0xd4, 0x6f, 0xbe, 0x4e, 0x7a, 0xb5, 0x9e, - 0xc3, 0xec, 0x47, 0xc2, 0xf2, 0xa2, 0xe8, 0x7a, 0xa6, 0xdb, 0xbc, 0x6a, 0xa3, 0x3b, 0x73, 0xaa, 0xaf, 0xd4, 0x60, - 0x08, 0xf8, 0x95, 0x73, 0x79, 0x50, 0x26, 0xa8, 0x9c, 0xd8, 0x76, 0x0f, 0x6d, 0x46, 0x40, 0x07, 0xcf, 0xb2, 0xd3, - 0xcc, 0x97, 0xaf, 0x96, 0x49, 0x31, 0xac, 0x77, 0xa9, 0x43, 0x81, 0x97, 0x7b, 0x95, 0xfe, 0x81, 0x46, 0x95, 0x32, - 0xf2, 0x82, 0xa8, 0x3a, 0xd1, 0x5e, 0x70, 0x10, 0xc7, 0x1d, 0xfe, 0x3d, 0xe2, 0x70, 0xc9, 0x3d, 0x87, 0x1d, 0x40, - 0x4e, 0x59, 0x44, 0x3a, 0xca, 0xc7, 0x77, 0x8f, 0xbe, 0x65, 0xcc, 0x31, 0xd2, 0x65, 0xf5, 0x53, 0x11, 0x6d, 0x1f, - 0x51, 0x12, 0xe9, 0x0e, 0x07, 0xfb, 0x14, 0x21, 0xde, 0x6c, 0x8a, 0x41, 0x00, 0x2b, 0x74, 0xbe, 0x44, 0x74, 0x42, - 0x5a, 0xd4, 0x03, 0x0a, 0x87, 0xad, 0x82, 0xcf, 0x72, 0xc1, 0x09, 0x96, 0xfe, 0x10, 0x13, 0xab, 0x52, 0x24, 0x3b, - 0x34, 0xcb, 0xbf, 0x4c, 0x6d, 0xaf, 0x96, 0xa6, 0x51, 0x6d, 0x1e, 0xc1, 0x7d, 0xe3, 0xb2, 0xa4, 0x68, 0x05, 0x76, - 0x97, 0xbd, 0x54, 0xc8, 0xc2, 0x86, 0x6b, 0x2f, 0x79, 0xa6, 0x6d, 0x4b, 0x5e, 0x34, 0x78, 0x40, 0x12, 0xd8, 0x7c, - 0x01, 0xac, 0xff, 0x71, 0xb5, 0x2c, 0x43, 0x2d, 0x54, 0x35, 0x30, 0x42, 0xbe, 0xdb, 0x75, 0x04, 0xd1, 0x9e, 0x55, - 0x37, 0xbf, 0x06, 0x26, 0x5a, 0xf6, 0x26, 0xb0, 0x74, 0x90, 0x45, 0x0b, 0x81, 0x60, 0xe7, 0xfe, 0x7c, 0xed, 0xb2, - 0xd8, 0xce, 0x78, 0x8c, 0x35, 0x61, 0xe1, 0x11, 0xb9, 0x71, 0x80, 0x95, 0xc7, 0x65, 0x09, 0x42, 0x56, 0x94, 0x61, - 0x57, 0xee, 0x1c, 0x50, 0x8f, 0x85, 0x1a, 0x55, 0x08, 0xb2, 0xd6, 0x67, 0xaf, 0xa7, 0x8a, 0x35, 0xc9, 0xfd, 0x3e, - 0x28, 0x30, 0x38, 0x83, 0xbb, 0x4d, 0x45, 0x28, 0x7d, 0x48, 0xe1, 0x4f, 0x6d, 0xba, 0x3e, 0x4b, 0x7b, 0x9e, 0x82, - 0x49, 0xb1, 0x20, 0x5e, 0x2b, 0xf9, 0xe7, 0xe9, 0x2f, 0x12, 0xa8, 0x83, 0x94, 0xdc, 0x98, 0x3e, 0xe2, 0xb5, 0x11, - 0x42, 0x64, 0xac, 0xe7, 0xa0, 0x71, 0x20, 0x9c, 0x52, 0x30, 0xa8, 0x9c, 0xd9, 0x32, 0x8b, 0xe9, 0x78, 0x67, 0x4b, - 0x9d, 0x90, 0x6d, 0x0d, 0x3f, 0xf0, 0x66, 0x1a, 0xfb, 0x89, 0x70, 0xdd, 0xdc, 0xe4, 0x5b, 0x83, 0x67, 0xe8, 0x14, - 0x33, 0x7e, 0x93, 0x31, 0x14, 0xd3, 0xd6, 0x3d, 0x17, 0x4f, 0x4f, 0x4f, 0xc5, 0xa8, 0xb2, 0xb9, 0xe2, 0x61, 0xbc, - 0x1c, 0xab, 0x6a, 0x55, 0x15, 0xd3, 0x42, 0x2b, 0xab, 0xcf, 0x7f, 0x16, 0xc3, 0x25, 0xba, 0xc5, 0x70, 0xb6, 0x08, - 0x6d, 0xa2, 0x88, 0x16, 0x8d, 0x74, 0xcd, 0xd5, 0xfd, 0x4e, 0xdd, 0x95, 0xec, 0xe3, 0xab, 0x77, 0xfb, 0x1f, 0x12, - 0x46, 0xad, 0x97, 0xee, 0x14, 0x90, 0x57, 0x23, 0x9e, 0xf7, 0x5f, 0xcf, 0x29, 0xaf, 0x5a, 0x5e, 0x1a, 0x7d, 0x14, - 0x3c, 0x67, 0xfa, 0xdc, 0xd0, 0xf8, 0x45, 0xd3, 0x28, 0xcd, 0x3e, 0x50, 0x23, 0xbb, 0x81, 0xd6, 0x9b, 0xb4, 0x43, - 0xc6, 0x3b, 0x12, 0x7c, 0xb2, 0x42, 0x78, 0x69, 0xdc, 0x9e, 0x38, 0x89, 0x94, 0x62, 0x34, 0x55, 0x29, 0x54, 0xb5, - 0xce, 0x0a, 0x4d, 0x5b, 0x55, 0x21, 0xc9, 0x81, 0x03, 0xa5, 0x93, 0x21, 0xcc, 0xf1, 0xa4, 0x9c, 0xc4, 0x93, 0xa4, - 0x59, 0xcd, 0x43, 0x4e, 0x79, 0x51, 0x92, 0x86, 0xf4, 0x75, 0xe6, 0x14, 0x80, 0x66, 0x03, 0x25, 0x70, 0x28, 0x49, - 0x01, 0x66, 0x1a, 0xd2, 0x33, 0x44, 0x14, 0x82, 0x01, 0x7a, 0x73, 0x15, 0x13, 0x8f, 0x13, 0x6f, 0x1b, 0xed, 0xb2, - 0xa6, 0x20, 0x9e, 0x7c, 0xec, 0x7d, 0xb3, 0x98, 0xd6, 0x9d, 0x5c, 0x50, 0xc9, 0xf3, 0xc5, 0xd4, 0xd2, 0x04, 0xee, - 0x13, 0x32, 0xd5, 0x8c, 0xa9, 0x42, 0xfe, 0x4d, 0xee, 0xdb, 0xd1, 0x7e, 0x2c, 0x8e, 0xc5, 0xbb, 0x33, 0x34, 0xdd, - 0xcd, 0x55, 0x8e, 0xdc, 0x37, 0x23, 0xb9, 0xd5, 0xb2, 0xa6, 0x11, 0x84, 0x2c, 0x7c, 0xe1, 0x7a, 0xed, 0xf5, 0xf1, - 0x7d, 0xd6, 0xfd, 0xab, 0x0d, 0xc7, 0x8b, 0xe6, 0x25, 0x1f, 0xd2, 0x5d, 0x31, 0xb1, 0x68, 0xaf, 0xfc, 0x24, 0xa9, - 0x77, 0x6a, 0x3d, 0x66, 0xc2, 0xdd, 0x3f, 0x94, 0xa6, 0x31, 0xd3, 0x3b, 0xea, 0x78, 0x3f, 0xba, 0xc3, 0x1c, 0x8a, - 0x98, 0x6a, 0x58, 0xdd, 0x48, 0xa5, 0x5c, 0x98, 0x9e, 0x61, 0x63, 0xae, 0x4e, 0x3b, 0x4a, 0x4a, 0xd0, 0xa9, 0x5a, - 0xff, 0x51, 0x1e, 0xe1, 0x34, 0x55, 0xc1, 0x4f, 0x5e, 0x6d, 0xc7, 0xa2, 0x6b, 0x2f, 0x47, 0x6b, 0xd1, 0xb3, 0x1d, - 0xe5, 0x84, 0x7d, 0x7c, 0x8f, 0x50, 0x75, 0x7d, 0xb1, 0x3e, 0xfd, 0xb5, 0xfe, 0x56, 0xee, 0x06, 0x2a, 0x81, 0x3a, - 0x1b, 0xcb, 0xec, 0x5a, 0x13, 0x17, 0xb6, 0xbf, 0x6e, 0x53, 0xab, 0x06, 0x4e, 0xf6, 0x6a, 0xc3, 0xca, 0x9a, 0xcf, - 0x84, 0x6c, 0x7c, 0x93, 0xb2, 0x5f, 0x88, 0xe1, 0x27, 0xa9, 0x4d, 0x4d, 0x9b, 0xa4, 0xb5, 0xfc, 0x2c, 0xd7, 0xcd, - 0xdb, 0x56, 0xc4, 0xe9, 0xbe, 0x28, 0x82, 0x9c, 0x22, 0x09, 0xd9, 0xc6, 0x78, 0x84, 0xb0, 0x85, 0x0e, 0xe2, 0x5c, - 0xba, 0x88, 0xb1, 0x2c, 0x62, 0x78, 0x2f, 0x8f, 0x7d, 0x12, 0x6a, 0xda, 0x68, 0xa7, 0x2c, 0xb2, 0xff, 0x3e, 0xd3, - 0x8f, 0x8b, 0x2a, 0xa8, 0x03, 0x30, 0xbd, 0xbf, 0x6a, 0x7b, 0xb9, 0x38, 0xea, 0x37, 0x15, 0x07, 0x57, 0xff, 0x94, - 0x36, 0x37, 0x6c, 0xaa, 0xf9, 0x86, 0xa8, 0x54, 0xca, 0xbe, 0x18, 0xf4, 0x8c, 0xec, 0x55, 0xa3, 0x51, 0xcc, 0xa7, - 0xd0, 0xb2, 0x44, 0xfc, 0xf1, 0x54, 0x28, 0x6a, 0xa8, 0xe6, 0x2e, 0xe4, 0xe4, 0xd8, 0x30, 0xf6, 0x27, 0x93, 0xdd, - 0x9e, 0xb6, 0xea, 0xa7, 0xac, 0x67, 0x48, 0x87, 0x87, 0x82, 0x1f, 0xb8, 0xdc, 0x75, 0xf1, 0xa6, 0xec, 0xdd, 0xaa, - 0x45, 0x2a, 0x51, 0x10, 0x2a, 0x9b, 0x7d, 0xf5, 0x86, 0xa9, 0x81, 0x1e, 0x6a, 0xf4, 0x40, 0x19, 0x4c, 0xf1, 0x09, - 0x80, 0x9a, 0xd6, 0xe1, 0xd3, 0xd4, 0x42, 0xd9, 0x48, 0xdf, 0x0b, 0xcc, 0x30, 0xfd, 0xd7, 0x61, 0xb2, 0x42, 0x06, - 0xfc, 0xea, 0x69, 0x79, 0x33, 0xce, 0xbf, 0xe7, 0xb6, 0x87, 0xde, 0xa7, 0x7e, 0xfa, 0x2a, 0x89, 0x71, 0x0f, 0xf6, - 0xf7, 0x69, 0xe6, 0x4c, 0xc9, 0xd8, 0x51, 0x01, 0x24, 0x54, 0xdc, 0x4c, 0x61, 0x08, 0x4f, 0x17, 0x82, 0x22, 0x86, - 0xae, 0x6f, 0xd7, 0xf3, 0x3b, 0xbe, 0x62, 0x1e, 0x51, 0xbb, 0x4c, 0xd5, 0x50, 0xd2, 0xfa, 0x30, 0x1b, 0x10, 0xd6, - 0x04, 0x4f, 0x8e, 0x70, 0xc3, 0xd2, 0x55, 0x44, 0x66, 0xc1, 0x0a, 0xcf, 0xc0, 0xa9, 0x09, 0xb8, 0x6e, 0x8a, 0xcc, - 0x7b, 0x9c, 0x00, 0xce, 0xc7, 0x63, 0x1c, 0xed, 0x29, 0xe0, 0xed, 0xb2, 0xba, 0xda, 0x5b, 0x6a, 0xbd, 0x73, 0x1e, - 0xda, 0x44, 0x10, 0x95, 0xf8, 0x79, 0x36, 0x91, 0xfb, 0x07, 0x6f, 0xce, 0xab, 0xc9, 0x96, 0xa4, 0x43, 0xc9, 0xdf, - 0x41, 0xd1, 0x9b, 0xac, 0xb0, 0x92, 0x1b, 0xc5, 0x22, 0x99, 0x34, 0x02, 0x20, 0x30, 0xaf, 0xf2, 0x1d, 0x11, 0xc0, - 0x55, 0x58, 0x68, 0x34, 0x45, 0x51, 0x5e, 0x51, 0x6d, 0x9e, 0xd1, 0xee, 0xd8, 0xaf, 0xe7, 0xb8, 0x2c, 0xd7, 0x96, - 0xd4, 0x6a, 0x2c, 0xeb, 0x48, 0x8a, 0x66, 0x18, 0xbc, 0x39, 0x2f, 0x05, 0x2f, 0xf1, 0xc1, 0x3c, 0x6f, 0x89, 0xaf, - 0x54, 0x5a, 0x41, 0x23, 0xd7, 0x6b, 0x8a, 0x99, 0x03, 0x9a, 0xd3, 0x65, 0x7a, 0x97, 0xe2, 0xfd, 0xeb, 0x15, 0xbf, - 0x2c, 0x5b, 0xaa, 0xba, 0xee, 0xfa, 0x93, 0x15, 0x71, 0x5c, 0x64, 0xb1, 0x6f, 0x59, 0xb4, 0x19, 0xec, 0x10, 0xfb, - 0x31, 0xed, 0xf3, 0x28, 0xcf, 0xb5, 0xcf, 0x36, 0x3f, 0x97, 0x10, 0x47, 0x96, 0x68, 0xbd, 0x3a, 0x62, 0x3f, 0xb5, - 0x64, 0x63, 0xb9, 0xef, 0x44, 0x29, 0x76, 0xb4, 0xb8, 0x90, 0xe6, 0x42, 0x1f, 0x3c, 0xd7, 0x83, 0xa5, 0x0c, 0x7f, - 0x16, 0x57, 0xb6, 0xf4, 0xaa, 0x1c, 0xad, 0xf4, 0x9f, 0x75, 0xa3, 0x87, 0x53, 0x9b, 0x62, 0xea, 0xde, 0x47, 0xc2, - 0x34, 0xa1, 0xf9, 0xbe, 0x21, 0x36, 0x55, 0x4c, 0x14, 0x44, 0x23, 0x6d, 0x03, 0xc7, 0xfb, 0xe7, 0xf5, 0x95, 0xa7, - 0xbc, 0x94, 0xfc, 0xe1, 0x3a, 0x6e, 0x79, 0x63, 0x68, 0x32, 0xf1, 0x06, 0xad, 0x07, 0x39, 0x81, 0x6d, 0x6c, 0x9f, - 0x1e, 0x69, 0x8f, 0xc2, 0x09, 0xe9, 0x4e, 0x39, 0xb4, 0x0e, 0xd7, 0x27, 0xef, 0xd0, 0x85, 0x28, 0x8d, 0x4c, 0xfc, - 0x84, 0xf4, 0xc6, 0x69, 0x74, 0xaa, 0xab, 0x7f, 0xf2, 0xbc, 0xb3, 0xd8, 0x37, 0xb0, 0xa0, 0xde, 0xff, 0xe9, 0xc6, - 0x50, 0x62, 0x3c, 0x6f, 0x19, 0x71, 0x4c, 0x84, 0xa4, 0xdc, 0x4a, 0xbe, 0x4f, 0x22, 0x2a, 0xb5, 0x52, 0x38, 0xa3, - 0x17, 0xf4, 0x88, 0x1a, 0x2c, 0x9e, 0x9f, 0x5a, 0xe7, 0xc0, 0xa4, 0x1b, 0xe5, 0xa5, 0x51, 0x20, 0x0d, 0x22, 0x4f, - 0xcd, 0xf4, 0x0c, 0x9a, 0xb7, 0x0f, 0xaf, 0x03, 0xf7, 0x9e, 0x20, 0x9f, 0xff, 0xfe, 0x30, 0xdc, 0xde, 0x1a, 0x68, - 0x96, 0xf5, 0x39, 0x76, 0x51, 0xeb, 0x8b, 0x15, 0x7a, 0x58, 0x80, 0xdd, 0x13, 0x92, 0xeb, 0x3f, 0x05, 0xe8, 0x1a, - 0xcc, 0xb2, 0x55, 0xc7, 0xbc, 0x6d, 0xfb, 0xb7, 0xf3, 0x2a, 0xdc, 0x1d, 0x33, 0x10, 0x68, 0x77, 0xc6, 0x38, 0x87, - 0xff, 0x67, 0x89, 0x64, 0x15, 0xc6, 0xe4, 0xa2, 0xbd, 0x6e, 0x0f, 0x97, 0xc4, 0x6e, 0xb5, 0x66, 0x39, 0xd3, 0x76, - 0x60, 0xeb, 0x39, 0x2f, 0xa2, 0xd2, 0x20, 0xc1, 0x4e, 0x6a, 0x43, 0x03, 0x44, 0x32, 0xe8, 0xf6, 0x52, 0xc6, 0xbd, - 0x20, 0x9f, 0x01, 0x7d, 0x6d, 0x67, 0x2e, 0xbd, 0x31, 0x35, 0xae, 0x70, 0x52, 0x97, 0x9d, 0xbb, 0xc9, 0x70, 0xd6, - 0x3e, 0x16, 0xca, 0xd7, 0x63, 0x81, 0x2f, 0xac, 0x8f, 0xd3, 0xf4, 0xc1, 0x1d, 0xd9, 0x47, 0x93, 0x63, 0x2f, 0xa6, - 0xa4, 0x2a, 0x33, 0x18, 0x65, 0x08, 0xb4, 0x74, 0x2d, 0xcb, 0x94, 0x62, 0x8f, 0xde, 0x3e, 0x9c, 0x32, 0x6e, 0xfa, - 0x79, 0x98, 0x73, 0xd0, 0x89, 0x65, 0x8b, 0xe7, 0x15, 0xd9, 0xc3, 0xd4, 0x9d, 0x00, 0x89, 0x04, 0x61, 0xa2, 0x0b, - 0x95, 0x7a, 0x90, 0x61, 0x4d, 0x78, 0x84, 0x34, 0x71, 0x71, 0x3a, 0x32, 0x61, 0x77, 0xe4, 0x49, 0x07, 0x51, 0x07, - 0x86, 0xca, 0xd5, 0x73, 0xfe, 0xd0, 0x63, 0xb2, 0x17, 0x14, 0xd9, 0xf6, 0x48, 0xe1, 0x9c, 0x79, 0xf3, 0x21, 0x7b, - 0xe8, 0x5f, 0x37, 0xbd, 0xe6, 0x88, 0x05, 0xf7, 0xb7, 0x50, 0x81, 0x32, 0x04, 0xdc, 0x1f, 0xfa, 0xee, 0x36, 0x47, - 0xad, 0xa0, 0x33, 0x30, 0x7d, 0xb2, 0xcf, 0xf4, 0x62, 0x4d, 0x69, 0xb8, 0x6f, 0x46, 0xce, 0xe0, 0x4e, 0xd0, 0xb5, - 0x33, 0xa9, 0xb4, 0xbb, 0x7c, 0x21, 0xa8, 0xf0, 0xe1, 0x1a, 0xb4, 0x3a, 0x88, 0x9c, 0x92, 0xfe, 0x4e, 0x48, 0x75, - 0xb5, 0x29, 0x26, 0xdc, 0x40, 0xcd, 0x06, 0x8a, 0xa3, 0x70, 0xe3, 0x07, 0x89, 0x01, 0x66, 0x6e, 0xa4, 0x61, 0x25, - 0xaf, 0x9d, 0x87, 0x5f, 0xec, 0x07, 0x39, 0xcf, 0x63, 0x2a, 0xd1, 0x43, 0x9f, 0x56, 0x75, 0xfd, 0x21, 0xe6, 0x1b, - 0x6a, 0x9f, 0x41, 0x6d, 0x93, 0x10, 0xa2, 0x4e, 0xd3, 0x3e, 0xe6, 0x59, 0xf9, 0xd1, 0xc1, 0x84, 0x98, 0x7b, 0x32, - 0xd0, 0xaa, 0x5d, 0x81, 0xa5, 0xec, 0x52, 0x95, 0x70, 0xed, 0xd4, 0x6f, 0x2a, 0x69, 0x17, 0xab, 0x95, 0x57, 0xa7, - 0xd8, 0xb3, 0x7f, 0xe7, 0xda, 0xfb, 0x90, 0xf1, 0x99, 0xe8, 0x58, 0xb3, 0xda, 0xbd, 0xee, 0x27, 0xce, 0x69, 0xbc, - 0xc4, 0x46, 0x09, 0xe5, 0x87, 0x69, 0x40, 0x3c, 0x78, 0x83, 0x78, 0xd7, 0x4f, 0x6c, 0xf6, 0xe2, 0xaa, 0x2f, 0x35, - 0x5a, 0xa8, 0x3f, 0xe9, 0xc3, 0xa3, 0x1a, 0x9c, 0x3c, 0x5c, 0x86, 0x27, 0x5f, 0x79, 0x3b, 0x19, 0xe0, 0xb1, 0x12, - 0xf8, 0xdc, 0x5a, 0x02, 0x4a, 0x47, 0x24, 0xaf, 0xe4, 0x03, 0xfa, 0x7f, 0x0e, 0xcf, 0x87, 0x5d, 0x8f, 0x9f, 0x2d, - 0x6d, 0xa8, 0x45, 0x27, 0x1d, 0x61, 0x09, 0x6a, 0x7b, 0x48, 0x43, 0x88, 0x8c, 0x1d, 0x81, 0x69, 0xcc, 0x9f, 0x14, - 0x61, 0x1e, 0x81, 0xf7, 0x39, 0x03, 0x8e, 0xda, 0x96, 0xf8, 0xc2, 0x09, 0x77, 0xef, 0xf2, 0xe1, 0x37, 0xf0, 0xbd, - 0xb2, 0x4b, 0x58, 0x6e, 0xab, 0x1d, 0xbb, 0xd9, 0x04, 0x9a, 0xa3, 0x28, 0x6e, 0xbf, 0x99, 0x68, 0xd1, 0xb3, 0xc3, - 0x7e, 0x0e, 0xba, 0x97, 0xa1, 0x42, 0xf9, 0x98, 0xf6, 0x99, 0xdc, 0xaf, 0x47, 0x80, 0x22, 0xe0, 0x10, 0x43, 0x6c, - 0xff, 0xd8, 0x2b, 0x0f, 0xb5, 0x9e, 0x05, 0x04, 0x14, 0xc3, 0x9f, 0x5c, 0x70, 0x66, 0xfa, 0xe0, 0x18, 0x30, 0x39, - 0x00, 0xd4, 0x06, 0x17, 0x8d, 0xc5, 0x29, 0xfe, 0xbf, 0xf3, 0x8d, 0xe4, 0xed, 0xba, 0x38, 0x1d, 0xf1, 0x2e, 0x9f, - 0x51, 0x54, 0xcc, 0x90, 0x42, 0x0b, 0xbf, 0xe8, 0x06, 0xc2, 0x4a, 0x11, 0x0b, 0x7a, 0x2b, 0x1f, 0xdb, 0xcb, 0x63, - 0x14, 0xaa, 0xff, 0xab, 0x97, 0xec, 0x8f, 0x5a, 0xf0, 0xd8, 0xa5, 0x58, 0xde, 0xf0, 0x91, 0x53, 0xaa, 0x87, 0xbb, - 0x78, 0xb3, 0x1d, 0x06, 0x05, 0xbd, 0x1d, 0x10, 0x6f, 0xfd, 0x9f, 0x25, 0x49, 0xb6, 0xdc, 0x6a, 0x86, 0x24, 0xb9, - 0xae, 0x8e, 0x3b, 0xe2, 0xdf, 0x8f, 0x78, 0x57, 0x1b, 0x1d, 0xaa, 0xf6, 0x7c, 0x5c, 0x67, 0xfe, 0x2b, 0xce, 0xf2, - 0x86, 0xa4, 0xd3, 0xcc, 0xee, 0x6b, 0x5c, 0xce, 0x65, 0x3b, 0x99, 0x2f, 0x66, 0x77, 0xb3, 0xfd, 0xf2, 0xfd, 0x96, - 0x2a, 0x63, 0xeb, 0xf9, 0x45, 0xf3, 0x31, 0xc7, 0x1d, 0x91, 0x94, 0x65, 0x18, 0xcb, 0xf9, 0xb9, 0x4b, 0xf3, 0xe3, - 0x0f, 0xc2, 0x9b, 0x1f, 0xbf, 0x78, 0x28, 0x38, 0x9d, 0x62, 0x2a, 0x23, 0x4e, 0x95, 0xce, 0x9c, 0x24, 0x86, 0xa9, - 0x14, 0x68, 0x26, 0xba, 0xbe, 0x06, 0xc9, 0x00, 0xbd, 0x82, 0xa6, 0xc3, 0xd0, 0x9f, 0xf1, 0x01, 0xae, 0x3a, 0x79, - 0xa6, 0x92, 0xcc, 0x17, 0x8c, 0x31, 0x5e, 0xf0, 0x43, 0xbf, 0xf0, 0xe4, 0x5e, 0x3b, 0x32, 0x80, 0x21, 0x15, 0x7b, - 0xfc, 0x78, 0xd1, 0x7c, 0x79, 0x69, 0x44, 0x08, 0x55, 0xc8, 0x52, 0x80, 0xa7, 0x3c, 0x7f, 0x26, 0xab, 0xeb, 0xd9, - 0x6f, 0x36, 0x5d, 0x69, 0xb8, 0xaf, 0xa6, 0x9e, 0x2a, 0x60, 0x6c, 0xb9, 0x91, 0x8f, 0x29, 0x66, 0xd6, 0x06, 0xeb, - 0x74, 0x50, 0xab, 0xc7, 0x1c, 0xe3, 0xa9, 0xa0, 0x2e, 0xa6, 0xd4, 0x93, 0x3c, 0xd6, 0xd9, 0xf4, 0x41, 0x36, 0xb8, - 0x81, 0x71, 0xc5, 0xc9, 0x47, 0x10, 0x45, 0x13, 0x60, 0x39, 0x4f, 0x5b, 0x44, 0x11, 0x7c, 0x87, 0x66, 0x14, 0xc1, - 0x10, 0xb1, 0x88, 0x2d, 0xef, 0x56, 0xc9, 0xbc, 0xbd, 0xec, 0x72, 0x92, 0xe9, 0xb7, 0xa5, 0xcc, 0x49, 0xa2, 0xc1, - 0xc1, 0x2a, 0x9f, 0xb5, 0xea, 0xa6, 0x1f, 0xec, 0x4b, 0x28, 0x00, 0x8e, 0xcc, 0xc0, 0x81, 0x92, 0x62, 0x56, 0xaa, - 0x8a, 0x1a, 0x39, 0x08, 0x70, 0xf2, 0xc3, 0x3f, 0x54, 0x5f, 0x84, 0xa5, 0xb3, 0xdb, 0x29, 0x08, 0x3d, 0xc1, 0x08, - 0x91, 0x40, 0xe3, 0x27, 0x97, 0x6c, 0xfa, 0xef, 0xdc, 0xcc, 0x48, 0x5f, 0xfe, 0xbd, 0x9e, 0xec, 0x6d, 0x6b, 0x50, - 0x30, 0xb9, 0x1e, 0xed, 0xeb, 0x58, 0x2b, 0x96, 0x4e, 0xa8, 0x4b, 0x7f, 0x71, 0x05, 0x3e, 0xa9, 0x09, 0x91, 0xb1, - 0x62, 0xa6, 0x32, 0x6b, 0x29, 0x78, 0xae, 0x7e, 0xcc, 0x65, 0x60, 0x26, 0x52, 0xda, 0x15, 0x93, 0xa6, 0x34, 0xf3, - 0x29, 0x17, 0xd1, 0xb3, 0x67, 0x5d, 0xa7, 0xa1, 0xb5, 0x0e, 0xac, 0xcb, 0x7e, 0x88, 0xb7, 0xf9, 0xd5, 0x99, 0xa6, - 0x30, 0xca, 0xf9, 0xab, 0xf3, 0x0e, 0x8b, 0x72, 0xb3, 0xbe, 0x62, 0x3e, 0xec, 0x1d, 0xda, 0x69, 0x65, 0xf4, 0xf1, - 0x5c, 0xad, 0x70, 0xdf, 0x81, 0x90, 0xf3, 0xe8, 0x7b, 0x03, 0x1e, 0xff, 0x0a, 0xff, 0xbf, 0x3e, 0x04, 0xda, 0xb1, - 0x15, 0x0c, 0xdd, 0xf0, 0x89, 0x4d, 0x70, 0x8f, 0x86, 0x99, 0xd3, 0xd9, 0xca, 0xef, 0x43, 0x22, 0xea, 0x16, 0x70, - 0xb7, 0x8b, 0x1f, 0xd7, 0x3e, 0xc3, 0xd5, 0xc8, 0xc6, 0x18, 0x0e, 0xb9, 0x01, 0xb2, 0x84, 0xf0, 0x09, 0x09, 0x63, - 0xdd, 0x39, 0x3f, 0x38, 0xa3, 0x31, 0xbe, 0xfb, 0x5b, 0xe7, 0xf9, 0x66, 0xbc, 0x8d, 0xf9, 0x75, 0xf2, 0x4d, 0xe7, - 0x7a, 0xa0, 0xf3, 0xf4, 0xa0, 0xd6, 0x6a, 0xfd, 0xc3, 0x4d, 0xef, 0x5d, 0x0c, 0x4b, 0xb8, 0x9f, 0x3a, 0xba, 0xb9, - 0x7b, 0x13, 0x11, 0x11, 0xa8, 0x3f, 0x78, 0x68, 0xd1, 0xf3, 0x09, 0xd4, 0xe9, 0x12, 0x22, 0xfa, 0xa3, 0xcd, 0x9e, - 0xdb, 0xc9, 0x9c, 0x3a, 0x79, 0xb2, 0x8d, 0xae, 0x45, 0x25, 0x5f, 0x58, 0x2c, 0xf3, 0x3e, 0x6d, 0xdd, 0x88, 0xc8, - 0x81, 0xc4, 0x64, 0xc5, 0x36, 0xc3, 0xd4, 0xd0, 0x71, 0xea, 0x22, 0xf1, 0x3f, 0xef, 0xeb, 0xc4, 0x50, 0xf2, 0xb2, - 0xd4, 0x02, 0x0b, 0x4b, 0x55, 0xd8, 0x3e, 0xee, 0x39, 0x95, 0x85, 0x55, 0x37, 0x46, 0xbc, 0x75, 0xdf, 0x76, 0x4d, - 0xc7, 0x26, 0x8a, 0xd7, 0x5f, 0xbf, 0x02, 0xad, 0x21, 0x3d, 0x16, 0xf1, 0x7e, 0x91, 0x8e, 0x63, 0x00, 0xde, 0x31, - 0x74, 0x0b, 0x77, 0xcb, 0xb2, 0x6a, 0xcf, 0xfb, 0x74, 0x0c, 0x25, 0x45, 0xb1, 0x94, 0xdc, 0x3d, 0x62, 0xeb, 0x71, - 0x94, 0xe0, 0xa9, 0xee, 0x3d, 0xbd, 0x45, 0x2a, 0x91, 0xa5, 0xa3, 0xf4, 0xd8, 0xcf, 0x29, 0x60, 0xea, 0xa5, 0xf8, - 0x7d, 0xf4, 0x68, 0x59, 0x32, 0x40, 0x8b, 0x8d, 0x58, 0xe5, 0x1d, 0x5b, 0xc1, 0x5a, 0x9c, 0x92, 0x63, 0xbc, 0xed, - 0xdd, 0x97, 0x54, 0xca, 0x5d, 0xcc, 0x6c, 0x94, 0x76, 0x6a, 0xbc, 0x1c, 0x1c, 0xa7, 0xa5, 0xb0, 0x22, 0xc6, 0x18, - 0x39, 0xbb, 0x12, 0xb4, 0x35, 0x42, 0x77, 0xb8, 0x66, 0x89, 0xff, 0xbe, 0xac, 0x2d, 0x6e, 0x25, 0x90, 0x91, 0x2f, - 0xc3, 0x37, 0xe5, 0x9b, 0xa0, 0xad, 0xfe, 0x62, 0x8f, 0xbe, 0x56, 0x10, 0x32, 0xe1, 0x57, 0x7c, 0x35, 0xba, 0xe6, - 0xf6, 0x7d, 0xd9, 0x4d, 0x56, 0x69, 0x92, 0x9d, 0x40, 0x6b, 0x93, 0xca, 0xb9, 0xf0, 0xf0, 0x39, 0x77, 0x47, 0x92, - 0x3e, 0x7d, 0x2a, 0xcc, 0x28, 0x79, 0xc9, 0x54, 0x50, 0x3a, 0xc8, 0x66, 0x7f, 0x82, 0x25, 0xa8, 0x87, 0x7c, 0x41, - 0x6d, 0xdd, 0xe3, 0xe9, 0xf3, 0x1a, 0x88, 0xeb, 0x65, 0xc3, 0x0a, 0x44, 0x22, 0xfa, 0x6f, 0xb3, 0x8f, 0x3e, 0x64, - 0x73, 0x42, 0xf6, 0xfa, 0x66, 0x8e, 0xd3, 0x9d, 0x44, 0x28, 0xca, 0x1d, 0xb7, 0x03, 0x4a, 0x29, 0x0e, 0x4a, 0xd5, - 0xf0, 0xd8, 0x2c, 0x91, 0x63, 0xe6, 0x07, 0xa7, 0xbb, 0xd8, 0x4f, 0x5c, 0x8b, 0x5f, 0xd8, 0xb1, 0x53, 0x79, 0xf3, - 0xcf, 0xbe, 0x7c, 0xd9, 0xc7, 0x83, 0xc8, 0xe8, 0x0f, 0x42, 0x11, 0x5f, 0xf6, 0x9b, 0x26, 0xf1, 0xe2, 0x17, 0xdf, - 0xd2, 0x53, 0x3c, 0xf7, 0x6b, 0x75, 0x11, 0xb7, 0x75, 0xf7, 0xbe, 0x8a, 0x76, 0x29, 0xb1, 0xe1, 0x36, 0x0c, 0x4f, - 0x93, 0xe4, 0xf4, 0x00, 0xe0, 0x03, 0xce, 0xe5, 0x3f, 0x73, 0x14, 0xca, 0x47, 0x2e, 0xc3, 0xf9, 0x62, 0x11, 0x62, - 0x4c, 0xfe, 0xc6, 0x18, 0xa5, 0x35, 0x6f, 0x9f, 0xb7, 0x77, 0xbf, 0x71, 0x6c, 0x78, 0x6d, 0xbc, 0x89, 0x86, 0x8a, - 0x16, 0xe5, 0x4d, 0xe1, 0x53, 0x5e, 0x17, 0x76, 0x79, 0xaf, 0xf0, 0x98, 0xf7, 0x0b, 0x4f, 0xf9, 0x60, 0xed, 0xd1, - 0x68, 0x45, 0x48, 0xc1, 0xb5, 0x40, 0xd6, 0x85, 0x42, 0x97, 0x71, 0x04, 0xf7, 0x94, 0x17, 0x6d, 0xcd, 0xef, 0xd0, - 0x44, 0x96, 0xff, 0x07, 0x62, 0x85, 0xd5, 0xe9, 0x07, 0x4d, 0xf1, 0x0a, 0xc4, 0x58, 0xe6, 0x58, 0x8a, 0xd5, 0xed, - 0x7f, 0xd6, 0x52, 0x31, 0x1e, 0x73, 0xb6, 0x99, 0x81, 0xbe, 0x5a, 0xbe, 0xc2, 0xc6, 0x40, 0xe3, 0xeb, 0x4d, 0x69, - 0xf5, 0x1a, 0x58, 0x8b, 0xfd, 0x7c, 0x4d, 0x23, 0x59, 0x89, 0xb0, 0x52, 0xe5, 0x61, 0x60, 0xa2, 0x2a, 0xf3, 0x8c, - 0x74, 0x04, 0xc5, 0xf3, 0xe9, 0x0b, 0xbe, 0x72, 0xd4, 0xda, 0x67, 0x05, 0xa8, 0x86, 0xc7, 0x42, 0x47, 0x2f, 0x8c, - 0xec, 0xea, 0xba, 0xa5, 0xa6, 0xb6, 0x67, 0x5f, 0x12, 0x6b, 0xe4, 0xb7, 0xe3, 0x67, 0x52, 0x24, 0xb4, 0x6c, 0xfc, - 0x3e, 0x8f, 0x77, 0xb1, 0xf7, 0x95, 0x86, 0x34, 0x40, 0x68, 0x9d, 0x90, 0x59, 0xd4, 0x74, 0xc1, 0x4b, 0xc2, 0xa7, - 0xa5, 0x8f, 0xe9, 0x47, 0xc7, 0xfb, 0x8b, 0xaf, 0xf0, 0x00, 0x47, 0x5a, 0xbb, 0xd8, 0xe4, 0xc7, 0xe3, 0x02, 0x7e, - 0xed, 0x37, 0x1d, 0x0a, 0x6b, 0xc6, 0x2a, 0x97, 0xde, 0xb4, 0xab, 0x8b, 0xe0, 0x6b, 0x4b, 0x9f, 0xf1, 0xb8, 0x7f, - 0xec, 0x4d, 0x1d, 0xef, 0x4f, 0x7a, 0x04, 0xbe, 0x01, 0x28, 0x15, 0x35, 0x88, 0x7d, 0x10, 0x7a, 0xbc, 0xb3, 0x2a, - 0x82, 0xcb, 0xf0, 0x38, 0xa4, 0xed, 0xf9, 0x32, 0xb3, 0xab, 0xc7, 0xf8, 0x8d, 0x90, 0x04, 0xdd, 0xf0, 0x4e, 0x5a, - 0x12, 0xa0, 0xf4, 0x51, 0x09, 0x93, 0x1c, 0xb1, 0xcf, 0x2f, 0x5a, 0xf6, 0xa6, 0x8d, 0x4e, 0xe1, 0x5b, 0x8f, 0x98, - 0x67, 0x6d, 0x99, 0xf3, 0x9f, 0x06, 0x71, 0x30, 0x93, 0xa3, 0xf8, 0xfd, 0x10, 0xe7, 0x45, 0x15, 0x75, 0xe9, 0xc5, - 0x6c, 0x6f, 0x03, 0xb6, 0xf0, 0xbb, 0x0f, 0xb3, 0x81, 0xef, 0x4f, 0x7d, 0xb9, 0xd6, 0xa1, 0x9e, 0xd1, 0xfd, 0x56, - 0x75, 0xdb, 0xc7, 0x91, 0x75, 0xf2, 0x9c, 0xc5, 0xc3, 0xe8, 0xdd, 0xf7, 0x85, 0xaf, 0x71, 0x66, 0xb4, 0xf8, 0x24, - 0x2a, 0x0a, 0x2b, 0x97, 0x41, 0xb9, 0x7c, 0x4d, 0x55, 0xb5, 0x47, 0x9b, 0x2f, 0x62, 0x74, 0x5e, 0xfc, 0x5e, 0xa7, - 0x8f, 0xba, 0xc6, 0xeb, 0x48, 0xf9, 0x68, 0x5f, 0x16, 0xc3, 0x1f, 0xac, 0x20, 0xb4, 0x98, 0xd8, 0xec, 0xb1, 0x5f, - 0x8e, 0x16, 0xa7, 0x67, 0x69, 0x33, 0xec, 0x34, 0x6d, 0xb5, 0x71, 0x3b, 0xd8, 0x6f, 0x1d, 0xd2, 0x92, 0xc4, 0x8b, - 0xf1, 0x15, 0x2a, 0x7f, 0xc0, 0x43, 0xec, 0x39, 0x48, 0xd0, 0x88, 0x35, 0xe7, 0xb7, 0xc8, 0x75, 0xba, 0x16, 0x48, - 0x5d, 0xf8, 0x7a, 0xe8, 0x61, 0xd2, 0x22, 0xd5, 0x41, 0x59, 0x06, 0xba, 0x89, 0x02, 0xfa, 0x9e, 0xba, 0x2d, 0xc8, - 0x45, 0xf6, 0xf7, 0x9c, 0x9d, 0xbe, 0xc6, 0xfb, 0x73, 0x0b, 0x3b, 0x51, 0xf8, 0xcd, 0x1f, 0x93, 0x18, 0xd6, 0xdc, - 0x76, 0x91, 0x2d, 0x82, 0xde, 0x6c, 0x5a, 0x3e, 0x28, 0x07, 0x6c, 0x7e, 0x69, 0xa1, 0xca, 0xc8, 0x11, 0xeb, 0xf9, - 0x6f, 0xf7, 0x63, 0x97, 0x98, 0x57, 0x41, 0xa8, 0x5e, 0xa9, 0x2a, 0x31, 0x80, 0x3e, 0xa9, 0x3d, 0x03, 0x75, 0x66, - 0x76, 0x55, 0xe9, 0xf5, 0xeb, 0xac, 0x3e, 0xd4, 0xee, 0x02, 0xf7, 0x4e, 0xc3, 0xb3, 0x13, 0x6b, 0x25, 0x8b, 0xe8, - 0x23, 0x24, 0x61, 0x02, 0xfd, 0x7e, 0xd7, 0xb5, 0xaf, 0x7b, 0x3a, 0x96, 0x05, 0x94, 0x89, 0x3a, 0x5c, 0x9c, 0x20, - 0x18, 0x3f, 0xc8, 0x71, 0x80, 0x6d, 0xe4, 0xc7, 0x2e, 0x8b, 0xab, 0xfe, 0x1c, 0x28, 0x92, 0xa0, 0xb9, 0x96, 0xfb, - 0x35, 0xb8, 0xaf, 0xef, 0x74, 0x93, 0x15, 0xd9, 0x65, 0x98, 0x33, 0xde, 0x30, 0xc6, 0x08, 0x51, 0xc5, 0x22, 0x9e, - 0xe7, 0xb8, 0x81, 0xe5, 0x71, 0x09, 0xde, 0x58, 0xce, 0x3b, 0xa3, 0xda, 0xf2, 0x6c, 0x80, 0xa6, 0xb4, 0x62, 0x1b, - 0x95, 0x6a, 0x65, 0x0c, 0x0c, 0x64, 0xcb, 0x4e, 0xa6, 0xef, 0xa9, 0x2c, 0xc6, 0xfb, 0x77, 0x47, 0x04, 0x37, 0x3d, - 0xca, 0x7c, 0x7d, 0x10, 0xc6, 0xd0, 0xdc, 0xc3, 0xa0, 0x62, 0xb7, 0x4d, 0x39, 0x06, 0x17, 0x5c, 0x74, 0xa2, 0x26, - 0x35, 0x94, 0x45, 0xb5, 0x8c, 0x14, 0x5e, 0xcd, 0x8a, 0xbe, 0xee, 0x69, 0xf1, 0x5a, 0x84, 0x18, 0x94, 0xe1, 0xba, - 0x24, 0x21, 0x54, 0x26, 0x08, 0x7d, 0xa8, 0x30, 0xa5, 0xc2, 0xeb, 0x94, 0x80, 0xfd, 0x3d, 0xcf, 0x79, 0xdd, 0xfb, - 0x5d, 0x3b, 0x2c, 0xb3, 0xe4, 0xb8, 0xd7, 0x70, 0xbb, 0x82, 0xbb, 0x23, 0xcf, 0x46, 0x76, 0x6b, 0x64, 0xf2, 0xbe, - 0x56, 0x0c, 0xe9, 0xb6, 0x60, 0x2a, 0x2e, 0x8a, 0x68, 0x95, 0xc5, 0xb8, 0x1d, 0xf8, 0x95, 0xbb, 0x45, 0xb3, 0x9e, - 0x3a, 0x93, 0xf5, 0x86, 0x21, 0x7c, 0x1a, 0x96, 0xb1, 0x84, 0x58, 0xbd, 0x1e, 0xf9, 0x7f, 0x97, 0x85, 0x47, 0x45, - 0xbb, 0x4f, 0x28, 0xc4, 0xbd, 0xc9, 0x8c, 0x37, 0x03, 0x70, 0x90, 0x63, 0x88, 0x63, 0x70, 0xa0, 0xb5, 0xac, 0xd0, - 0xa9, 0x91, 0x80, 0x88, 0xb5, 0x25, 0x7f, 0xd3, 0x5b, 0xec, 0x2a, 0x7a, 0x6d, 0xdb, 0x77, 0x8e, 0x7f, 0xfe, 0xb6, - 0xda, 0xd6, 0x4d, 0x2c, 0xe4, 0x9d, 0x91, 0x41, 0x3d, 0xb0, 0xbf, 0xef, 0x88, 0x13, 0x6d, 0x81, 0xc0, 0xd5, 0x07, - 0xd3, 0x62, 0x7d, 0xbc, 0x10, 0x31, 0x3f, 0xf8, 0x18, 0x26, 0xf1, 0x14, 0x1d, 0x7d, 0xc6, 0xe7, 0x86, 0x8f, 0xc2, - 0x0f, 0xff, 0xb3, 0x1c, 0x58, 0x99, 0x74, 0x24, 0xa7, 0x8e, 0xa9, 0x8e, 0x02, 0x02, 0xe8, 0x4c, 0xee, 0x91, 0xef, - 0xbf, 0x3a, 0xb4, 0x54, 0xb1, 0x6c, 0x3a, 0x43, 0xb3, 0x93, 0x4e, 0xac, 0x5b, 0xcc, 0x06, 0x9f, 0x38, 0xf7, 0x8b, - 0xcb, 0x0f, 0xe9, 0xc9, 0x61, 0x7f, 0x7b, 0xd2, 0x68, 0xd3, 0x63, 0x46, 0x03, 0x60, 0x0c, 0x2b, 0xfd, 0x78, 0x90, - 0xd2, 0xeb, 0x27, 0x6a, 0xa2, 0x65, 0x43, 0x78, 0x66, 0x3c, 0xba, 0x0c, 0x91, 0xfe, 0xc3, 0xa0, 0x78, 0xd8, 0x6c, - 0xbd, 0x32, 0x5f, 0xb0, 0x9a, 0x83, 0xd1, 0x0b, 0x82, 0x66, 0xc3, 0x16, 0x8b, 0xca, 0xea, 0x71, 0x7e, 0x84, 0x59, - 0x50, 0x00, 0x3e, 0x65, 0x6d, 0x80, 0xfe, 0x39, 0xe6, 0x98, 0x0b, 0x88, 0x46, 0xa3, 0x36, 0x52, 0x6d, 0xf5, 0xbc, - 0xe2, 0x9f, 0xa9, 0x38, 0x50, 0xeb, 0x3d, 0x39, 0x66, 0x7b, 0xca, 0xea, 0x6a, 0x93, 0x4a, 0x03, 0xb4, 0xbe, 0x4c, - 0xf0, 0xb5, 0x0e, 0xb5, 0x04, 0x72, 0x56, 0xc0, 0x67, 0x96, 0x56, 0x97, 0xd9, 0x3d, 0xe7, 0xf8, 0xbd, 0x78, 0xf7, - 0xa0, 0x33, 0xee, 0x36, 0xdf, 0x6d, 0x06, 0x3b, 0x2b, 0x91, 0xdf, 0x0f, 0x1c, 0xb0, 0xf5, 0xce, 0xf1, 0xb2, 0x16, - 0x78, 0xbf, 0x85, 0x41, 0x00, 0xf2, 0x7e, 0x81, 0x5d, 0xd2, 0x38, 0x0d, 0xf3, 0x95, 0xb6, 0x94, 0xc6, 0xb8, 0x72, - 0xfc, 0x94, 0x33, 0xff, 0x3f, 0xd4, 0x58, 0x19, 0xc7, 0x4f, 0x6c, 0x80, 0x76, 0x15, 0x20, 0xc9, 0x01, 0xd1, 0xc1, - 0x93, 0x16, 0x8f, 0xdf, 0x08, 0x0a, 0xfd, 0x6f, 0xae, 0xf9, 0xf5, 0x86, 0x41, 0x6c, 0x7b, 0x84, 0xf0, 0x0b, 0x6d, - 0xd8, 0xfc, 0x4d, 0x67, 0xcd, 0x25, 0x44, 0x72, 0xfd, 0x1d, 0x29, 0xa9, 0xab, 0xe7, 0x91, 0xfb, 0x93, 0x06, 0xc0, - 0xa4, 0xb2, 0xfa, 0x3a, 0xed, 0xf9, 0xc2, 0xeb, 0x79, 0x07, 0xb1, 0x19, 0xc7, 0xef, 0x8e, 0x98, 0xf8, 0x50, 0x54, - 0xd5, 0x59, 0xd4, 0xb4, 0x3a, 0xf6, 0xd6, 0x49, 0x07, 0x3a, 0x71, 0x41, 0xf0, 0x18, 0xbf, 0x04, 0xfb, 0x79, 0xf3, - 0x43, 0x42, 0x1d, 0xbf, 0xeb, 0x87, 0xe4, 0x7a, 0x37, 0x85, 0x07, 0x76, 0xc0, 0xf7, 0xf0, 0xc1, 0xda, 0x44, 0xd3, - 0xb9, 0x10, 0x1f, 0x42, 0x52, 0x11, 0x90, 0xf5, 0x24, 0x4e, 0x6e, 0x4a, 0x92, 0x60, 0xc3, 0x5e, 0xd6, 0xb6, 0x82, - 0xc3, 0xb9, 0x76, 0x87, 0x22, 0x9c, 0x46, 0x07, 0xdd, 0x0c, 0x8f, 0x38, 0xe3, 0xa4, 0x6e, 0x65, 0xea, 0xb3, 0x6d, - 0x10, 0x89, 0x91, 0x70, 0x05, 0x04, 0x9f, 0x08, 0x1e, 0x8c, 0x98, 0x1a, 0x20, 0xa9, 0x08, 0x70, 0xfd, 0xb0, 0x8d, - 0x50, 0x76, 0x3f, 0xe5, 0x27, 0x7c, 0x12, 0x43, 0x0e, 0x39, 0xac, 0xc3, 0xf3, 0xe7, 0x70, 0xd1, 0x50, 0x2c, 0xce, - 0x1c, 0x67, 0x5e, 0x94, 0xd5, 0xb4, 0x50, 0x9c, 0x58, 0xf9, 0x82, 0x07, 0x5c, 0x6f, 0xc0, 0xbc, 0x9d, 0x0a, 0x76, - 0xc6, 0x33, 0x5e, 0x61, 0x4a, 0x4c, 0x6f, 0x77, 0xce, 0x2b, 0x5d, 0xb9, 0x55, 0x14, 0xaf, 0x1a, 0xb4, 0x67, 0x46, - 0x5c, 0xf8, 0x3b, 0xad, 0x8d, 0x6e, 0xd9, 0xa5, 0x71, 0xf8, 0x37, 0x4a, 0x24, 0x04, 0x9b, 0x9f, 0x78, 0xe3, 0x3d, - 0xb4, 0x6b, 0xdf, 0x05, 0x87, 0x59, 0x7e, 0xfb, 0x1a, 0xfd, 0xe9, 0x4d, 0xcf, 0xb0, 0x28, 0xbd, 0x9f, 0x99, 0x83, - 0xea, 0x40, 0x56, 0x57, 0x87, 0x03, 0x0c, 0xda, 0xe1, 0x8e, 0x57, 0x90, 0x6e, 0xc5, 0x2c, 0x43, 0xa4, 0x33, 0x19, - 0xfd, 0xdd, 0x8b, 0x79, 0xc1, 0x3a, 0x04, 0x66, 0x1f, 0x0d, 0x73, 0x02, 0x17, 0xab, 0x0c, 0x0a, 0xa1, 0x0a, 0x21, - 0x7c, 0x1c, 0xe6, 0x8a, 0x9c, 0x06, 0x52, 0xe1, 0x8a, 0x9c, 0xfa, 0xa4, 0x83, 0x72, 0x1d, 0x3a, 0x5f, 0xad, 0x71, - 0x3c, 0xc5, 0x84, 0xbe, 0x18, 0x78, 0xa8, 0xaf, 0xd8, 0x2c, 0x3e, 0xf7, 0x42, 0x64, 0xfd, 0x0d, 0x98, 0xdc, 0xe0, - 0x65, 0x75, 0x9f, 0x85, 0x10, 0xb3, 0x70, 0x99, 0x19, 0xa9, 0x5f, 0x8a, 0x5a, 0x4f, 0xa3, 0x11, 0xa0, 0xd6, 0x3c, - 0xa0, 0x55, 0xcb, 0x10, 0x61, 0xfc, 0x25, 0xb4, 0xf4, 0x7b, 0xed, 0xe0, 0x86, 0x5f, 0xc5, 0x34, 0x1c, 0xc3, 0xfc, - 0x47, 0x11, 0x7a, 0x88, 0x01, 0x97, 0x71, 0x4d, 0xad, 0x5c, 0x8d, 0x06, 0xb9, 0x62, 0x7c, 0x01, 0x90, 0x32, 0x18, - 0x60, 0xac, 0x59, 0x28, 0x9e, 0x7f, 0xc7, 0x1f, 0x82, 0x08, 0xf5, 0x6a, 0x1f, 0xfb, 0xd1, 0x0d, 0x31, 0xa6, 0x36, - 0x3e, 0x26, 0x38, 0xf8, 0xd8, 0x5a, 0x69, 0xdf, 0x74, 0x95, 0x35, 0xc2, 0x09, 0xb4, 0xe0, 0xca, 0x3c, 0x88, 0x0f, - 0xa7, 0x36, 0xff, 0x2f, 0xc5, 0xaa, 0x1e, 0xbb, 0xfb, 0xfb, 0x23, 0x5c, 0x0f, 0x9d, 0x72, 0x90, 0x57, 0xb8, 0x00, - 0x2e, 0xbb, 0xea, 0x9c, 0x57, 0xbe, 0xb2, 0x4c, 0xfe, 0x16, 0x0e, 0x96, 0x0f, 0xca, 0x71, 0x3a, 0xfd, 0xcb, 0xb5, - 0x8b, 0xa3, 0x3d, 0x98, 0x4f, 0xd3, 0x30, 0xfe, 0x49, 0x2c, 0x7d, 0x5e, 0xd0, 0xd9, 0x6f, 0x48, 0x1b, 0x3f, 0x2e, - 0xb2, 0x7d, 0xe8, 0xba, 0x3c, 0x7f, 0x8d, 0xb7, 0xe7, 0x76, 0x4d, 0x9b, 0xce, 0xf7, 0x3f, 0xa5, 0xb3, 0x71, 0xcf, - 0xf8, 0x6f, 0xf4, 0x44, 0x27, 0xdf, 0x18, 0x7f, 0x48, 0x6b, 0xe3, 0xd3, 0x20, 0xbe, 0x6c, 0x0b, 0xb2, 0x87, 0x73, - 0x78, 0x1a, 0xce, 0x17, 0x94, 0x5f, 0x64, 0x71, 0xd1, 0x9f, 0xbe, 0xc6, 0x8b, 0x73, 0xcf, 0xcb, 0xb5, 0xd6, 0x7c, - 0x6a, 0x6d, 0xc0, 0xd6, 0x02, 0xe7, 0x46, 0xed, 0x96, 0x49, 0xaa, 0x56, 0xde, 0x88, 0xe9, 0x6c, 0x1a, 0x51, 0x07, - 0xfb, 0x7d, 0x7b, 0xdc, 0xf1, 0x40, 0xff, 0xb3, 0x79, 0x5d, 0x71, 0x6d, 0xd5, 0x4d, 0x77, 0x56, 0xe0, 0x0d, 0x93, - 0xa5, 0x23, 0x3c, 0x2b, 0x88, 0x34, 0xd2, 0x07, 0xa4, 0x65, 0x6d, 0xdb, 0x12, 0x43, 0xbb, 0x59, 0xc9, 0x34, 0x71, - 0x5b, 0x33, 0x5c, 0xe2, 0x4c, 0x08, 0x10, 0x49, 0xa6, 0x18, 0xba, 0xd6, 0x0c, 0x90, 0xde, 0x41, 0x49, 0x88, 0x65, - 0xbf, 0x04, 0x8a, 0x25, 0x83, 0x4f, 0xff, 0x61, 0x45, 0x4c, 0x8e, 0x37, 0x74, 0x70, 0x2a, 0x68, 0xf6, 0xd8, 0x8e, - 0xb9, 0x08, 0xc2, 0x97, 0x28, 0xf4, 0x4c, 0x63, 0x27, 0x57, 0x6d, 0x8e, 0x9e, 0xd8, 0x09, 0x6b, 0x1a, 0x05, 0x55, - 0xbb, 0xdf, 0xde, 0x2a, 0x15, 0x37, 0x57, 0x9c, 0xcf, 0x60, 0x8c, 0x27, 0x1d, 0x41, 0xe4, 0xcf, 0xfe, 0x02, 0xca, - 0xd0, 0x25, 0x8c, 0xb2, 0x65, 0xde, 0x8f, 0x26, 0xb7, 0x52, 0xc7, 0x92, 0xd0, 0xd4, 0xf5, 0xea, 0x8a, 0x54, 0xe1, - 0xfe, 0x2e, 0xfc, 0xb3, 0x06, 0x71, 0x87, 0x38, 0x87, 0x64, 0x01, 0x51, 0x3d, 0x63, 0x25, 0xc5, 0x20, 0x66, 0x36, - 0x28, 0x61, 0x4a, 0x9f, 0xb4, 0xda, 0x6a, 0x9d, 0x1c, 0x7b, 0x5c, 0xae, 0xea, 0x42, 0xd6, 0x2d, 0x7f, 0xa4, 0x45, - 0x22, 0x2d, 0x70, 0x85, 0xef, 0x2c, 0x00, 0x5d, 0x09, 0xe0, 0x29, 0x04, 0x72, 0x98, 0x84, 0xbf, 0x95, 0x55, 0xf4, - 0xe0, 0xfe, 0x6d, 0x98, 0x5b, 0x8e, 0x40, 0xc2, 0x87, 0xb9, 0x69, 0x8d, 0x3a, 0x8d, 0x4c, 0x6b, 0xd8, 0xba, 0x04, - 0xe2, 0x24, 0x41, 0x0b, 0x35, 0xf6, 0x71, 0x28, 0x1c, 0x7a, 0x1e, 0xb9, 0x49, 0xae, 0xe5, 0xca, 0x97, 0xa2, 0x39, - 0x89, 0x3d, 0x52, 0xd1, 0xb1, 0x9f, 0x91, 0xe3, 0xbc, 0x10, 0xe4, 0xe2, 0x48, 0x9a, 0x9e, 0x6a, 0x92, 0x43, 0x9b, - 0x0c, 0x2a, 0x94, 0xdb, 0x2c, 0x68, 0x73, 0x1b, 0xb1, 0xbf, 0x8e, 0x88, 0x0b, 0x1b, 0x40, 0x22, 0x9c, 0x5c, 0x55, - 0xfd, 0x2d, 0xb9, 0xbe, 0x6e, 0x7c, 0x55, 0x0b, 0x19, 0x0f, 0x28, 0x19, 0x4e, 0xea, 0xed, 0x19, 0x0a, 0xc3, 0xc5, - 0xfc, 0xb4, 0xbe, 0xb0, 0xd6, 0xd4, 0x6e, 0xa5, 0x48, 0x0a, 0x43, 0x9a, 0xf2, 0x44, 0xe2, 0x87, 0x65, 0x77, 0xb1, - 0x49, 0xc5, 0x8a, 0xc0, 0xfb, 0x9c, 0xf9, 0x73, 0xe1, 0xd4, 0x1a, 0xff, 0x21, 0xc0, 0xad, 0x39, 0x38, 0xa8, 0xbf, - 0x8b, 0xdc, 0x64, 0xab, 0x1e, 0x38, 0x4d, 0x7e, 0x74, 0x45, 0x3f, 0x8b, 0x62, 0xdc, 0x83, 0x41, 0x9e, 0xb3, 0x46, - 0x1c, 0x27, 0x5e, 0xa1, 0xc8, 0xa6, 0x12, 0xba, 0xdb, 0x75, 0xa6, 0x88, 0xeb, 0x90, 0xa3, 0x19, 0x72, 0x72, 0x38, - 0x4e, 0x5a, 0xcd, 0xa3, 0xb2, 0x49, 0x12, 0x9e, 0xe2, 0x47, 0xee, 0x13, 0x8a, 0x5d, 0x9f, 0x85, 0x32, 0x23, 0xce, - 0x19, 0x67, 0xdb, 0x0b, 0xae, 0xd1, 0x5b, 0x73, 0x90, 0x8e, 0x1d, 0xf6, 0xfc, 0x89, 0x22, 0x4c, 0x21, 0x65, 0xa7, - 0x26, 0x6d, 0xd2, 0x55, 0x97, 0x71, 0x9f, 0x0e, 0x75, 0x1c, 0x52, 0x3d, 0x3b, 0x1c, 0xea, 0xa5, 0x2d, 0x4f, 0x1c, - 0xe2, 0xca, 0x87, 0xfe, 0x38, 0xf2, 0xeb, 0xc2, 0x7a, 0x51, 0xc8, 0xf8, 0xa4, 0xd0, 0x49, 0x4b, 0x95, 0x78, 0x00, - 0xb7, 0x95, 0x4d, 0x6f, 0xcb, 0xd4, 0xda, 0xd0, 0x71, 0xe9, 0x6f, 0x02, 0xa4, 0x90, 0xc5, 0xa9, 0x5c, 0x0a, 0xe5, - 0x9a, 0xf1, 0xe2, 0xb0, 0xe2, 0xf6, 0xd5, 0x7d, 0xda, 0x57, 0x14, 0x1d, 0x20, 0x10, 0x11, 0x5a, 0x01, 0xc2, 0x17, - 0x26, 0x70, 0x75, 0x95, 0xa5, 0xb0, 0x8e, 0x09, 0xc1, 0x53, 0xf8, 0x46, 0x6a, 0xa5, 0x55, 0x46, 0xc4, 0x05, 0xdb, - 0x8d, 0x50, 0xf6, 0x00, 0x1a, 0x10, 0xc3, 0x49, 0xfc, 0x2f, 0x4f, 0x55, 0xcb, 0xb4, 0x5b, 0xc9, 0xa5, 0x91, 0x76, - 0xa3, 0x2d, 0xde, 0x98, 0x56, 0x14, 0x14, 0x13, 0x92, 0xbe, 0xd2, 0xa0, 0xd5, 0xb1, 0xf5, 0x9b, 0xbd, 0x5e, 0xbc, - 0x3a, 0xbe, 0xe3, 0xe4, 0x60, 0x94, 0x63, 0xc9, 0x20, 0x53, 0x11, 0xca, 0xc5, 0x45, 0xd8, 0x7a, 0xd8, 0xd9, 0x16, - 0xda, 0x69, 0xd0, 0x71, 0xb7, 0x82, 0x1a, 0x84, 0xf9, 0xd0, 0x73, 0xa7, 0xdb, 0x3e, 0x5d, 0x19, 0xb7, 0x8b, 0x78, - 0x95, 0xe3, 0x54, 0x55, 0x09, 0xa4, 0x64, 0xf3, 0x31, 0x48, 0x95, 0x24, 0x47, 0xa6, 0x0a, 0xeb, 0x1e, 0x6c, 0xef, - 0x98, 0x30, 0x09, 0x79, 0xe4, 0x7d, 0xf8, 0x27, 0x84, 0x5a, 0x8a, 0x7e, 0xdb, 0xf6, 0x6d, 0xc9, 0xe1, 0x95, 0xa3, - 0x55, 0x83, 0x80, 0xd8, 0x88, 0x00, 0x35, 0x8f, 0x8f, 0xf6, 0x26, 0x6e, 0xbd, 0xa3, 0x72, 0x37, 0x35, 0x7e, 0xcf, - 0x56, 0x76, 0x1e, 0xf9, 0x1d, 0xaf, 0xec, 0xe3, 0x42, 0x15, 0xec, 0x92, 0x12, 0x3d, 0xc9, 0xfa, 0xf1, 0xca, 0xa6, - 0x35, 0xfb, 0x79, 0x7d, 0x41, 0xc8, 0xe6, 0x55, 0xf6, 0xc8, 0xab, 0x42, 0xbd, 0x18, 0x09, 0x63, 0xaa, 0x43, 0x78, - 0xe3, 0xc8, 0xd8, 0x9f, 0x17, 0x32, 0x8d, 0x81, 0x05, 0x28, 0xb4, 0xd4, 0xbb, 0x11, 0x4f, 0x8f, 0x65, 0x56, 0xa4, - 0x75, 0x27, 0x5c, 0xc5, 0x7a, 0x09, 0x3f, 0xba, 0x0d, 0x58, 0x58, 0x29, 0xdd, 0x22, 0x97, 0x77, 0x75, 0x91, 0xf5, - 0xd9, 0x6b, 0x13, 0x43, 0xef, 0x92, 0x42, 0x85, 0xb2, 0x63, 0xca, 0x8a, 0xf9, 0x0a, 0x69, 0x8e, 0x05, 0x6f, 0x42, - 0xfd, 0xbc, 0x2d, 0x7f, 0x87, 0x2a, 0x16, 0x7f, 0x5d, 0xd1, 0x5b, 0xa7, 0x6a, 0xb6, 0xcf, 0x14, 0x33, 0x65, 0x3b, - 0x17, 0xee, 0x8b, 0xfb, 0x8d, 0x6f, 0x88, 0xa7, 0x62, 0xd5, 0x77, 0x45, 0x71, 0xe4, 0xa0, 0xc9, 0x20, 0xaa, 0x93, - 0xb5, 0x10, 0x77, 0x5d, 0x19, 0x92, 0x70, 0xe7, 0x09, 0x33, 0x48, 0xe7, 0xb0, 0x71, 0x55, 0x23, 0xd3, 0xa0, 0xe6, - 0x40, 0x9d, 0x54, 0x83, 0x15, 0xb4, 0x42, 0x52, 0xf6, 0x14, 0x33, 0x51, 0x07, 0xee, 0xf7, 0x9a, 0xfd, 0x3f, 0xa0, - 0x12, 0x7d, 0xdd, 0xf5, 0x57, 0x7d, 0x0b, 0xe7, 0x82, 0x05, 0x4b, 0x2a, 0xfb, 0x72, 0x5b, 0x6f, 0xfc, 0x29, 0x6c, - 0xea, 0xd4, 0xad, 0xbb, 0x5d, 0xea, 0x72, 0x9a, 0x0d, 0xce, 0x3b, 0x47, 0x31, 0x77, 0x0a, 0x1f, 0x62, 0x2e, 0x2f, - 0xd9, 0x44, 0x25, 0x57, 0x71, 0xe2, 0x45, 0x0d, 0x00, 0xf3, 0x0e, 0x90, 0x9c, 0x29, 0x61, 0x94, 0xf8, 0x73, 0x52, - 0x01, 0xd5, 0x94, 0xae, 0xb3, 0xb3, 0xee, 0x17, 0x7b, 0xfe, 0x8a, 0xbc, 0xbe, 0x72, 0x0c, 0xea, 0xe6, 0xbc, 0x20, - 0xa7, 0x98, 0x5f, 0x34, 0x25, 0x63, 0x4f, 0xb7, 0xad, 0xaa, 0x93, 0xb5, 0xcb, 0x8b, 0xda, 0x44, 0x89, 0x74, 0xc9, - 0x0d, 0x2f, 0xf5, 0xb6, 0xbc, 0x66, 0xcb, 0x93, 0x75, 0x7a, 0x2a, 0xd6, 0xd8, 0xbe, 0x08, 0x63, 0x7d, 0x18, 0x5d, - 0xe9, 0x49, 0x07, 0x39, 0x2d, 0x4b, 0x4b, 0xb9, 0x8b, 0x9c, 0x5b, 0xba, 0x5d, 0x3a, 0xcc, 0x8f, 0x19, 0x6b, 0x6f, - 0x8d, 0x8d, 0xad, 0xe5, 0xe6, 0xbf, 0xae, 0x6c, 0xc3, 0x54, 0xa1, 0x68, 0x01, 0x4c, 0xcf, 0x26, 0x87, 0xf5, 0x01, - 0x35, 0x53, 0x6f, 0x51, 0xbb, 0xe2, 0xf5, 0x4e, 0xf4, 0xbc, 0xfb, 0x0e, 0x6a, 0x86, 0x5a, 0x8f, 0x92, 0x68, 0xa9, - 0x7d, 0xef, 0x5b, 0x4a, 0x5b, 0xe6, 0xb1, 0xf2, 0xa2, 0xd4, 0x43, 0xfd, 0xea, 0x8f, 0xd3, 0xda, 0xb8, 0x27, 0xbc, - 0x65, 0xa3, 0xae, 0xe2, 0x63, 0x9f, 0xe7, 0xc2, 0xcc, 0x2c, 0x3e, 0x97, 0xd6, 0x83, 0x5f, 0x4e, 0xbb, 0x99, 0x39, - 0x3d, 0xbe, 0xa7, 0x83, 0xc4, 0x5c, 0x7a, 0x2f, 0x43, 0xa0, 0x68, 0x85, 0x66, 0x1d, 0x35, 0xcc, 0x79, 0x9f, 0x3a, - 0xc6, 0xcf, 0x7b, 0x4c, 0xc9, 0x1d, 0x3f, 0xe3, 0xf5, 0xd0, 0xa6, 0x9f, 0x3e, 0x66, 0xce, 0x87, 0x89, 0xf0, 0x6a, - 0x57, 0xa3, 0x13, 0x56, 0xe0, 0xeb, 0xa5, 0xc7, 0xc9, 0xa7, 0xbd, 0xaa, 0x5a, 0x5a, 0xdf, 0x7f, 0x6b, 0x62, 0x80, - 0xa9, 0x52, 0x7e, 0x45, 0xfb, 0xb9, 0xc6, 0x62, 0x86, 0x97, 0x74, 0xd9, 0xcb, 0x00}; + 0x1b, 0x4d, 0x98, 0xa3, 0x10, 0xd8, 0x38, 0x00, 0x40, 0x74, 0x87, 0x5d, 0x14, 0x95, 0xac, 0x9e, 0x80, 0x5a, 0x16, + 0xd8, 0x44, 0x44, 0xed, 0xc1, 0xec, 0xbf, 0x23, 0x3c, 0x6d, 0x9a, 0x66, 0x3e, 0x6c, 0xc2, 0x28, 0x09, 0x3c, 0x6e, + 0x5f, 0x78, 0xfd, 0x3f, 0x03, 0xf2, 0xf9, 0xd8, 0x63, 0x23, 0xd6, 0x47, 0x4b, 0xb8, 0x52, 0xbd, 0x19, 0x5a, 0x7e, + 0xdb, 0x62, 0xfe, 0x45, 0xae, 0x2e, 0x1e, 0x2d, 0x75, 0x91, 0x2c, 0x66, 0xae, 0xa8, 0x95, 0x97, 0xe5, 0x64, 0x1c, + 0xa1, 0xb1, 0x4f, 0x72, 0x37, 0x53, 0xad, 0xd7, 0x77, 0xc5, 0xf3, 0xe4, 0xa0, 0x14, 0x9b, 0xb4, 0xb7, 0x34, 0x79, + 0x53, 0x5a, 0x99, 0x3d, 0x9b, 0xa1, 0x10, 0x13, 0x89, 0x05, 0x2a, 0x24, 0x94, 0xa6, 0xe4, 0xff, 0xb9, 0xff, 0x75, + 0xd9, 0xfb, 0x75, 0xa6, 0xa9, 0x98, 0x1b, 0x1f, 0x5b, 0x7a, 0x5c, 0x81, 0xb1, 0xb3, 0x0a, 0xae, 0xc9, 0xb2, 0xec, + 0x1e, 0x07, 0x18, 0x23, 0x63, 0x4e, 0x30, 0xf8, 0x20, 0x79, 0x99, 0x27, 0x6e, 0xfa, 0xe2, 0x57, 0x65, 0x8a, 0xea, + 0x5b, 0x96, 0x99, 0xfe, 0xf3, 0x79, 0x49, 0xca, 0x42, 0xe8, 0x32, 0x2b, 0xe3, 0xe3, 0xd9, 0xb3, 0x25, 0xe6, 0xbc, + 0x95, 0xbc, 0x08, 0x22, 0xcb, 0xac, 0xb8, 0x36, 0x11, 0x41, 0x34, 0xc4, 0xb6, 0x9d, 0x84, 0x6a, 0x33, 0x6d, 0x59, + 0xb5, 0x0e, 0xb0, 0xb4, 0x63, 0x81, 0x75, 0xba, 0x40, 0x8f, 0x35, 0x96, 0x4f, 0x21, 0x76, 0xb0, 0x49, 0x5c, 0x24, + 0xdf, 0x39, 0xe5, 0x80, 0xb5, 0x79, 0x5b, 0x9a, 0xb4, 0x82, 0x0b, 0x5c, 0xab, 0x35, 0x95, 0x7d, 0xa0, 0x4c, 0x23, + 0x72, 0x77, 0x0f, 0x25, 0x36, 0x0e, 0xd8, 0x95, 0x65, 0x67, 0xff, 0xde, 0x54, 0xab, 0xb4, 0x01, 0x9a, 0xb3, 0x36, + 0x35, 0x2e, 0x35, 0x4e, 0x19, 0xc4, 0x95, 0xce, 0xf9, 0x24, 0xb8, 0x20, 0x64, 0xbf, 0xf7, 0xfe, 0x7f, 0xcb, 0x76, + 0x18, 0xa2, 0xbb, 0x89, 0x1d, 0x38, 0x8d, 0xe8, 0xb0, 0xf4, 0x35, 0xb4, 0x72, 0x9c, 0xf9, 0xff, 0x77, 0x03, 0xec, + 0x06, 0x28, 0x2d, 0x40, 0x51, 0x5b, 0x24, 0x87, 0x5b, 0x25, 0xb7, 0xc6, 0x6b, 0xe6, 0xac, 0xd3, 0x4c, 0xd5, 0x69, + 0xce, 0xd9, 0xc8, 0x46, 0x89, 0xf1, 0xd9, 0x55, 0x6e, 0xa3, 0xd0, 0xd8, 0xf4, 0xb2, 0x0b, 0x2f, 0x3d, 0x9d, 0x63, + 0x9b, 0xa9, 0x66, 0x65, 0x06, 0x9c, 0xbf, 0x4d, 0x72, 0xd6, 0x90, 0x03, 0xc2, 0x41, 0xca, 0x7d, 0xd8, 0x75, 0x91, + 0x65, 0x59, 0x72, 0x91, 0x0b, 0x9b, 0x37, 0x91, 0xcd, 0x94, 0x37, 0xa3, 0x12, 0x2a, 0xa9, 0x25, 0xac, 0xdc, 0xc4, + 0xf4, 0xc4, 0xbd, 0x7f, 0x17, 0x64, 0x62, 0x6c, 0x71, 0x00, 0x3e, 0x05, 0x71, 0xe8, 0xc8, 0xa6, 0xeb, 0x1c, 0xe7, + 0x18, 0xbd, 0x61, 0x21, 0x98, 0x66, 0x7b, 0x08, 0x0e, 0x7d, 0x38, 0xb0, 0x01, 0x8d, 0x3c, 0xcf, 0x1d, 0x5e, 0x7d, + 0x60, 0xeb, 0x7e, 0xf9, 0x68, 0x18, 0xf4, 0x78, 0xb3, 0x2a, 0x01, 0xb7, 0x91, 0x43, 0xab, 0x65, 0x64, 0x23, 0x50, + 0xcd, 0x6a, 0xec, 0xe7, 0xf0, 0x67, 0xf3, 0x7f, 0x5f, 0x9c, 0x1c, 0x55, 0x14, 0x4c, 0xff, 0x84, 0xbe, 0xbd, 0xef, + 0xf0, 0x0a, 0xe9, 0x92, 0xb5, 0x05, 0xec, 0x67, 0x14, 0xe5, 0x61, 0xe4, 0xa1, 0x0a, 0x5e, 0x7b, 0xd9, 0x4d, 0x97, + 0x39, 0x9e, 0x19, 0x33, 0x36, 0x5d, 0x70, 0x3d, 0x30, 0x73, 0x65, 0xe5, 0x78, 0xb4, 0xa5, 0x1a, 0x9c, 0x67, 0x0a, + 0x0d, 0xda, 0x74, 0xa3, 0xe5, 0xf4, 0x21, 0x1e, 0x3f, 0x4c, 0x29, 0x3d, 0xf7, 0x93, 0xad, 0x2b, 0xe3, 0xbe, 0xb2, + 0x24, 0x6b, 0x3a, 0xfc, 0x39, 0xe7, 0xab, 0x09, 0x91, 0x84, 0x95, 0x9e, 0x46, 0xa4, 0x5c, 0x1d, 0x75, 0xdc, 0xb2, + 0x8c, 0xb2, 0xf4, 0xe6, 0x77, 0x94, 0x69, 0xd2, 0x96, 0x8a, 0x14, 0x88, 0x37, 0xd7, 0xf9, 0xc3, 0x64, 0xcc, 0xab, + 0xa6, 0x3e, 0x3e, 0x1e, 0x6f, 0x7b, 0x37, 0xb4, 0x6c, 0x78, 0x8e, 0x66, 0x3d, 0x98, 0xba, 0x7d, 0xf3, 0x59, 0x1a, + 0x37, 0xe3, 0xe6, 0x42, 0xb6, 0x36, 0xfd, 0x75, 0x1e, 0x3e, 0x85, 0xeb, 0x66, 0xec, 0x96, 0xe5, 0xa1, 0xc3, 0xcd, + 0x5c, 0x51, 0xb2, 0x7a, 0x68, 0x77, 0xd0, 0xe7, 0xaa, 0x4b, 0xb8, 0xe9, 0xe5, 0x22, 0xcc, 0xd7, 0x63, 0x6c, 0x52, + 0xd8, 0xae, 0x5a, 0xbe, 0x4e, 0x57, 0x28, 0x32, 0x13, 0x85, 0x8a, 0xff, 0x08, 0x65, 0xe5, 0xe5, 0x95, 0x9e, 0x8c, + 0x63, 0x3f, 0x17, 0x5c, 0x47, 0x58, 0x4d, 0x2d, 0x92, 0x70, 0xfb, 0xa7, 0xb9, 0x9f, 0x41, 0x01, 0xf9, 0xb7, 0xa6, + 0xa2, 0x30, 0x95, 0xf6, 0x73, 0x10, 0xf9, 0x28, 0x93, 0x31, 0x0c, 0x01, 0x8a, 0x4c, 0x47, 0x00, 0x9e, 0x79, 0x42, + 0x9e, 0x8e, 0xe7, 0x18, 0x25, 0x06, 0xde, 0xef, 0xf8, 0x7a, 0x79, 0xb4, 0x30, 0xdc, 0x08, 0xd2, 0x7e, 0xee, 0xa3, + 0x24, 0x57, 0x37, 0x28, 0x40, 0x76, 0x76, 0x0a, 0x92, 0x27, 0x05, 0x26, 0xd9, 0x35, 0xcb, 0x51, 0x26, 0xc8, 0x9b, + 0x8e, 0x43, 0x49, 0x43, 0x5f, 0xe3, 0x25, 0x29, 0xfe, 0xce, 0x27, 0x15, 0x2a, 0xc5, 0xdf, 0xba, 0x6b, 0x93, 0xa7, + 0x06, 0x00, 0x21, 0x9d, 0x66, 0xba, 0xde, 0xf8, 0x41, 0x90, 0xd8, 0x2d, 0x25, 0xa0, 0xe1, 0x8a, 0x99, 0x6c, 0x0a, + 0xeb, 0xbe, 0x36, 0x75, 0xba, 0x77, 0x29, 0x73, 0x78, 0x3c, 0xd3, 0x80, 0xc8, 0x8c, 0xd0, 0x70, 0x6a, 0x44, 0xd0, + 0x30, 0xca, 0x7b, 0x84, 0xdb, 0x54, 0x31, 0x7c, 0xc0, 0xe9, 0x87, 0x1b, 0x62, 0x59, 0xc0, 0x54, 0x08, 0xb4, 0xf6, + 0x36, 0x36, 0x9a, 0xaf, 0xa4, 0x21, 0x16, 0x93, 0x3f, 0xe3, 0x96, 0x36, 0xfe, 0x55, 0xd5, 0x84, 0x06, 0x48, 0x82, + 0xcf, 0xcf, 0xda, 0x21, 0x61, 0x8c, 0x26, 0x75, 0xb1, 0x49, 0x7a, 0x42, 0xca, 0x1c, 0x48, 0x20, 0xa1, 0x86, 0x4c, + 0xe1, 0x9c, 0x4d, 0x2e, 0xc7, 0x3b, 0xde, 0x3e, 0x08, 0x47, 0x6b, 0x4b, 0x62, 0x79, 0x23, 0x49, 0xa9, 0x86, 0xb7, + 0x86, 0x1a, 0x8e, 0x6d, 0xaa, 0x47, 0xcc, 0x4f, 0x71, 0xb7, 0xa7, 0x35, 0x8e, 0x25, 0x7d, 0x6e, 0x86, 0x4b, 0xb9, + 0x9b, 0xaf, 0xfa, 0x85, 0x60, 0x57, 0x12, 0x4d, 0x2a, 0x51, 0x85, 0x9f, 0x7f, 0xfd, 0x1d, 0xc8, 0x5a, 0x2d, 0x0f, + 0x53, 0xfc, 0x1c, 0xf8, 0x71, 0xac, 0x4c, 0x61, 0x3d, 0x48, 0x2f, 0xd5, 0xe9, 0x4d, 0x6d, 0xde, 0x91, 0x41, 0x2a, + 0xdc, 0x4a, 0xb0, 0xbf, 0x19, 0x21, 0x62, 0x87, 0x99, 0xf8, 0xd7, 0x8d, 0x84, 0x44, 0xd2, 0x85, 0xc2, 0x39, 0xab, + 0xe1, 0xe2, 0x3f, 0xa4, 0x0c, 0xd1, 0x9c, 0x10, 0x0d, 0x13, 0xc6, 0x57, 0x46, 0x29, 0x48, 0xef, 0xe6, 0xab, 0x4d, + 0xd3, 0x86, 0xee, 0x88, 0x3f, 0xa8, 0x57, 0xb9, 0x8e, 0xb1, 0x21, 0xf2, 0x55, 0x22, 0x79, 0xf6, 0x38, 0x0c, 0xe3, + 0x60, 0x39, 0xc1, 0x53, 0x95, 0x11, 0xfe, 0x75, 0xaa, 0x0a, 0xee, 0x39, 0x9a, 0x55, 0xce, 0xdd, 0x17, 0xb9, 0xe6, + 0x5b, 0x50, 0xe3, 0xf3, 0x66, 0x6e, 0x86, 0xca, 0x68, 0xeb, 0x58, 0x4b, 0x06, 0xc9, 0x95, 0x65, 0x57, 0x80, 0x8d, + 0xe9, 0x28, 0x8e, 0x2c, 0x5a, 0x60, 0x6c, 0xf6, 0xd7, 0x30, 0xfd, 0x4f, 0xd0, 0x09, 0x91, 0xb6, 0x97, 0x12, 0x6a, + 0x65, 0xc6, 0x0f, 0x46, 0xa8, 0xb7, 0xb2, 0xf3, 0x29, 0x8b, 0x20, 0xc3, 0xf7, 0xac, 0xe5, 0x39, 0x9c, 0xc7, 0xe1, + 0xe2, 0x49, 0xa9, 0xfd, 0x22, 0x5c, 0x76, 0xbb, 0x55, 0xa2, 0xb8, 0xd5, 0x08, 0x89, 0x0d, 0xd7, 0x3f, 0x51, 0xad, + 0x15, 0x0c, 0x57, 0x54, 0x5c, 0x6b, 0xad, 0xf5, 0x31, 0x76, 0x29, 0xa5, 0x6c, 0x4c, 0x4f, 0xff, 0x59, 0x4a, 0x7d, + 0xb5, 0x94, 0x94, 0x21, 0x26, 0xef, 0x09, 0x75, 0xc7, 0x49, 0x15, 0x0c, 0x0b, 0xcf, 0xdc, 0x52, 0x71, 0x26, 0x51, + 0x09, 0x06, 0x46, 0xe5, 0xb4, 0x3f, 0x78, 0xbe, 0xeb, 0x5d, 0xb0, 0x04, 0x88, 0xb7, 0xc8, 0xf5, 0xbf, 0x15, 0x05, + 0x2b, 0xe6, 0x56, 0x00, 0x4e, 0xcc, 0x82, 0x84, 0x2b, 0x3a, 0x18, 0x24, 0xd4, 0x1b, 0x98, 0xc9, 0x35, 0xa2, 0x77, + 0x82, 0xf5, 0x22, 0xb7, 0xfa, 0x25, 0x0a, 0xb9, 0x4d, 0x49, 0x45, 0x02, 0xb7, 0xd5, 0xda, 0x30, 0xcd, 0x7a, 0xa6, + 0x99, 0x38, 0x3d, 0x67, 0x31, 0x65, 0x76, 0xd4, 0x5c, 0xbb, 0xba, 0x04, 0xb3, 0xbb, 0x3b, 0x3d, 0x33, 0xf4, 0xc3, + 0x32, 0x44, 0xdf, 0xcd, 0xbe, 0x26, 0x49, 0x0c, 0xa9, 0x6c, 0xc3, 0xda, 0xf4, 0xff, 0x78, 0xda, 0x09, 0x05, 0x7f, + 0xad, 0xd5, 0x0d, 0xc4, 0xbd, 0x88, 0x4e, 0x5d, 0x4e, 0x84, 0xf9, 0xfa, 0x62, 0x60, 0x3f, 0x29, 0x67, 0xb8, 0x46, + 0x3c, 0xb2, 0xb1, 0x37, 0xd4, 0x5b, 0x23, 0x5a, 0x86, 0xe4, 0xf3, 0x7e, 0x95, 0xf6, 0x0d, 0x65, 0x66, 0x5f, 0xec, + 0x87, 0x8b, 0x77, 0xda, 0x4c, 0x0e, 0xb8, 0xd3, 0xd0, 0xf3, 0xa6, 0xf9, 0x06, 0xf2, 0xac, 0x3d, 0x38, 0x61, 0x4f, + 0x27, 0xdd, 0xe9, 0xc6, 0xd5, 0x24, 0x6b, 0xe3, 0xa1, 0xc4, 0x90, 0xc0, 0xaf, 0x59, 0x4e, 0x00, 0x39, 0x10, 0x7b, + 0xc4, 0xda, 0xe4, 0x52, 0xb8, 0x7e, 0xa3, 0x45, 0x37, 0x30, 0xaf, 0x9b, 0xbf, 0xc8, 0x21, 0x95, 0xc5, 0x1b, 0x10, + 0x32, 0x32, 0x3f, 0xa3, 0x1c, 0x59, 0xc1, 0xa3, 0xf2, 0xf5, 0x21, 0x52, 0x87, 0x2f, 0xaf, 0xf6, 0x43, 0x63, 0xdf, + 0x22, 0xf3, 0xa2, 0x68, 0x2a, 0x33, 0x47, 0xb9, 0x0f, 0x90, 0xc4, 0x92, 0x67, 0x58, 0x61, 0x7c, 0xd5, 0xda, 0x88, + 0x08, 0xbe, 0x11, 0xc0, 0x7e, 0xf7, 0x49, 0x70, 0x6c, 0x63, 0x12, 0x28, 0xd0, 0xee, 0x66, 0x20, 0x41, 0x01, 0x99, + 0x38, 0x92, 0xd4, 0x8e, 0x06, 0x89, 0xfd, 0x09, 0xda, 0x76, 0x71, 0x45, 0x24, 0x1b, 0xfb, 0x39, 0x60, 0x21, 0x8d, + 0x0f, 0xa8, 0xcc, 0x80, 0x08, 0x4b, 0x01, 0x3a, 0x7a, 0xfe, 0xa9, 0x42, 0x5c, 0xcd, 0xb0, 0xf0, 0x9c, 0xc1, 0x5d, + 0x99, 0xaf, 0xfb, 0x79, 0xf6, 0xe0, 0x4c, 0x05, 0xc4, 0xe3, 0x89, 0x5f, 0x6e, 0x17, 0x28, 0x32, 0x10, 0xb4, 0x42, + 0x3c, 0x14, 0x84, 0x16, 0x8a, 0x18, 0xb4, 0xf9, 0x8f, 0x7d, 0xae, 0x8a, 0x91, 0x0a, 0x85, 0xa8, 0x68, 0x4d, 0xc6, + 0x70, 0x45, 0x9d, 0x23, 0x06, 0xdf, 0xcc, 0xd8, 0xa1, 0x65, 0xa2, 0x52, 0xbe, 0x54, 0xf1, 0x58, 0x07, 0xeb, 0x89, + 0x14, 0x32, 0x32, 0x52, 0x93, 0x9d, 0x6f, 0x21, 0x49, 0xf0, 0x4e, 0xad, 0x3c, 0x83, 0x14, 0x5e, 0xe9, 0xb0, 0x4f, + 0xa2, 0x5f, 0x86, 0x28, 0x8c, 0xda, 0xd7, 0x94, 0xbe, 0x9a, 0x89, 0xc4, 0xd8, 0x13, 0x79, 0x50, 0xa2, 0xe5, 0x1f, + 0xd9, 0x84, 0x91, 0x84, 0xe4, 0xd8, 0xf3, 0xe1, 0xdf, 0xe7, 0x04, 0xe9, 0xe3, 0xac, 0x87, 0xb4, 0x25, 0x11, 0x3e, + 0x51, 0x96, 0x03, 0xba, 0xee, 0x80, 0xa4, 0x00, 0xde, 0x75, 0xc1, 0xed, 0x7d, 0xdb, 0x21, 0x8e, 0x4e, 0xce, 0xa9, + 0x19, 0xe3, 0x65, 0x0a, 0x1b, 0x39, 0x1c, 0x6f, 0x93, 0x20, 0x6c, 0x44, 0xaf, 0x4c, 0xd3, 0xb1, 0xc0, 0x2c, 0x81, + 0x44, 0x88, 0xf4, 0x7e, 0x71, 0xce, 0x85, 0x98, 0xd7, 0x49, 0x66, 0xa8, 0x78, 0x6a, 0x95, 0xa9, 0x09, 0x32, 0x1c, + 0xe7, 0x2a, 0xbe, 0x27, 0x29, 0xc9, 0x13, 0xee, 0x62, 0xb2, 0x5f, 0x61, 0x1d, 0x25, 0x4f, 0x49, 0x41, 0xc9, 0xa8, + 0xe1, 0x7f, 0x99, 0xd2, 0x44, 0x62, 0x57, 0x76, 0x87, 0x24, 0x80, 0x94, 0x60, 0xa9, 0xce, 0xe0, 0x71, 0x44, 0x3c, + 0x17, 0x82, 0x86, 0x88, 0x44, 0xe1, 0x33, 0xdb, 0xcb, 0xcf, 0x22, 0x87, 0x04, 0xcf, 0x4c, 0x89, 0xce, 0xe2, 0x0f, + 0xd6, 0x71, 0x8f, 0x8c, 0x37, 0x1a, 0x46, 0x35, 0xaf, 0x0f, 0xda, 0x3e, 0x62, 0x6e, 0x7a, 0xfe, 0x30, 0x30, 0xd3, + 0xb1, 0xc9, 0x26, 0x95, 0x70, 0x56, 0x6e, 0xfe, 0xc6, 0x05, 0x8a, 0x8d, 0x5a, 0xa1, 0xe5, 0x67, 0x3d, 0xb5, 0x21, + 0xea, 0x9c, 0x13, 0xe2, 0x80, 0x03, 0x56, 0xb3, 0x60, 0x9e, 0x6b, 0xfc, 0xcf, 0x65, 0x72, 0x97, 0x1c, 0xc1, 0x99, + 0x1b, 0x4b, 0x73, 0x79, 0x15, 0xc9, 0xa1, 0x0b, 0xb6, 0x02, 0x55, 0x40, 0x39, 0xc9, 0x18, 0x23, 0xcb, 0x01, 0x23, + 0x96, 0x48, 0x2e, 0x17, 0x20, 0xb4, 0xc8, 0xba, 0x0a, 0xc2, 0x50, 0xa8, 0x9c, 0x46, 0xda, 0x70, 0x28, 0xe3, 0x18, + 0x99, 0xd6, 0x55, 0xdf, 0x19, 0x42, 0x96, 0xf2, 0xae, 0x01, 0xed, 0x28, 0x95, 0xbc, 0x94, 0xef, 0xa2, 0xdc, 0x9d, + 0xf0, 0x52, 0x18, 0x20, 0xcf, 0x1f, 0x15, 0x1b, 0x75, 0x47, 0x81, 0x17, 0x83, 0xf1, 0x42, 0x96, 0x0d, 0x77, 0x52, + 0xc9, 0x12, 0x13, 0x25, 0x08, 0x9c, 0x32, 0xd2, 0xd8, 0xa7, 0x2c, 0xed, 0xca, 0xfb, 0x2b, 0x4c, 0x2c, 0x4f, 0xca, + 0x28, 0x46, 0x3c, 0x39, 0xab, 0xb2, 0xae, 0x59, 0x3c, 0xc4, 0xfc, 0xc9, 0xdb, 0x24, 0xe5, 0x37, 0x3d, 0xd3, 0xe8, + 0x8d, 0x49, 0x67, 0x0d, 0x39, 0x9c, 0x4e, 0xc5, 0xe9, 0xec, 0x59, 0x5c, 0x35, 0x48, 0x55, 0x40, 0x11, 0x08, 0x87, + 0x3c, 0xf9, 0x26, 0x33, 0xda, 0x37, 0x01, 0x4b, 0xa5, 0x63, 0x28, 0x4f, 0xaa, 0x31, 0x26, 0x24, 0x2d, 0xf7, 0x3f, + 0x82, 0xe2, 0x4a, 0x8d, 0x24, 0x4b, 0xf0, 0xe1, 0x1d, 0x4a, 0x08, 0x4a, 0xc9, 0xa1, 0x83, 0x6e, 0x43, 0x45, 0x13, + 0x28, 0xa2, 0x27, 0x41, 0x9e, 0xaf, 0x37, 0x76, 0xaa, 0x14, 0x03, 0x9c, 0x98, 0xec, 0xca, 0xe3, 0x68, 0x66, 0x95, + 0x3e, 0xfb, 0x4f, 0x11, 0x1c, 0x0e, 0x87, 0x17, 0x34, 0x48, 0xa4, 0xf7, 0x5c, 0x91, 0x9b, 0x5a, 0x70, 0x7e, 0xba, + 0x10, 0x93, 0x59, 0x5b, 0x16, 0xd2, 0x72, 0x84, 0x62, 0x24, 0x87, 0x8e, 0xc0, 0xb6, 0x0c, 0xb9, 0xad, 0x91, 0xc8, + 0xe4, 0x5b, 0xfe, 0x1d, 0x87, 0x4c, 0x52, 0x32, 0xa5, 0xc9, 0x78, 0x2f, 0xa7, 0x22, 0xbb, 0x12, 0x45, 0x25, 0x32, + 0x2a, 0xa6, 0x41, 0x0c, 0xa9, 0xac, 0xde, 0xd3, 0x82, 0xa5, 0xba, 0x23, 0xb8, 0x3b, 0x27, 0xa4, 0x60, 0x19, 0x54, + 0xdd, 0x8e, 0xce, 0x38, 0xda, 0x20, 0x66, 0x5d, 0x92, 0xec, 0x27, 0xc5, 0x20, 0x9b, 0x48, 0xa1, 0x44, 0x3d, 0x61, + 0x37, 0x6e, 0x4b, 0x08, 0xfb, 0xdd, 0xc0, 0xc4, 0xd2, 0xb2, 0x4c, 0x93, 0x3e, 0x45, 0x62, 0xa7, 0x14, 0x8f, 0x50, + 0xf9, 0x14, 0xba, 0x77, 0xd3, 0x48, 0x48, 0x75, 0x92, 0x27, 0x08, 0xda, 0x73, 0x30, 0x76, 0x4c, 0xc0, 0x7c, 0x7f, + 0x0a, 0xd6, 0x8f, 0xd3, 0xb4, 0x60, 0xe1, 0xe0, 0x21, 0xc5, 0x9e, 0x99, 0xdd, 0xfc, 0xcb, 0x7c, 0x8e, 0x72, 0xce, + 0x0c, 0x9d, 0xcc, 0x53, 0x48, 0x66, 0xe3, 0xec, 0xe4, 0x5f, 0x90, 0xe6, 0xbd, 0x83, 0xdd, 0x91, 0xb6, 0xe1, 0xf7, + 0x99, 0xe0, 0xfa, 0x44, 0x0e, 0x23, 0xf8, 0xaa, 0x4b, 0x62, 0x37, 0x1f, 0x23, 0x8c, 0x22, 0x45, 0xaf, 0x1d, 0x07, + 0xe2, 0xb2, 0xda, 0x7d, 0x79, 0x10, 0x00, 0xb0, 0xa8, 0xf4, 0xef, 0x95, 0x88, 0x4c, 0xcc, 0x83, 0x5c, 0x06, 0x5b, + 0x19, 0xf0, 0xb3, 0x4a, 0xe2, 0x01, 0x97, 0x80, 0x4b, 0xe8, 0xb3, 0x02, 0x66, 0xa8, 0x01, 0xd4, 0xde, 0x79, 0x53, + 0x18, 0x46, 0x3a, 0x68, 0x4e, 0xb5, 0x86, 0xe2, 0x2d, 0x8a, 0x28, 0x1f, 0xfa, 0xb0, 0xf7, 0x61, 0x91, 0x01, 0x1d, + 0xfc, 0x38, 0x33, 0xa1, 0x3c, 0x4c, 0x9a, 0x31, 0x9a, 0x98, 0xe7, 0x19, 0xc5, 0xbd, 0xe1, 0xc2, 0xa4, 0xb7, 0x24, + 0x10, 0xd3, 0xbe, 0x6f, 0x4b, 0x45, 0x7c, 0xbf, 0x1b, 0x97, 0xfe, 0xd5, 0x7a, 0x04, 0xbd, 0x64, 0x16, 0x4a, 0xe4, + 0x5b, 0x2a, 0xd4, 0x91, 0x07, 0x86, 0xdb, 0x76, 0x6c, 0x98, 0x75, 0xa7, 0x95, 0xf4, 0x7a, 0x55, 0x35, 0xec, 0x80, + 0x71, 0x54, 0x5a, 0x7a, 0xaa, 0x5f, 0x1c, 0xd4, 0xe4, 0xf5, 0x62, 0xfd, 0xd5, 0x8e, 0xbd, 0x3c, 0x01, 0x99, 0x19, + 0xa3, 0xc1, 0x9c, 0x92, 0xc6, 0x0e, 0xa8, 0x85, 0x34, 0x94, 0x75, 0xb8, 0x8b, 0xa7, 0xb5, 0x12, 0x0e, 0x44, 0xe0, + 0x6c, 0xba, 0x4d, 0xac, 0x97, 0xdc, 0x0f, 0x1d, 0x40, 0x19, 0x1d, 0x3e, 0x77, 0x9b, 0x5a, 0x0c, 0xeb, 0x01, 0x6f, + 0x10, 0xd1, 0x42, 0x93, 0x0a, 0x2e, 0xb1, 0x43, 0xca, 0xa6, 0xca, 0xd0, 0x41, 0xe7, 0x5c, 0x53, 0x66, 0x65, 0xa5, + 0xf2, 0x2e, 0xaf, 0xa4, 0x9f, 0x66, 0x21, 0x1b, 0xeb, 0x2a, 0x68, 0x2c, 0xc8, 0x6f, 0x21, 0x00, 0xce, 0xa3, 0x99, + 0xbe, 0xd9, 0x00, 0x73, 0xb2, 0x64, 0xf9, 0xad, 0x3c, 0xaa, 0x2c, 0x56, 0xee, 0x2d, 0x47, 0xea, 0xc8, 0xc8, 0xa4, + 0xef, 0x4a, 0x01, 0x92, 0x0e, 0xc6, 0xe5, 0x8e, 0xd5, 0x9e, 0x31, 0x25, 0xba, 0x5f, 0x30, 0xc4, 0xda, 0xe1, 0xf0, + 0xcb, 0x91, 0xc3, 0xa1, 0x66, 0x90, 0x1d, 0x69, 0xf4, 0x20, 0x45, 0xf0, 0x22, 0x57, 0xb8, 0xe2, 0x8f, 0x65, 0xdb, + 0x96, 0x08, 0xe2, 0x29, 0xc2, 0xdf, 0x33, 0x49, 0xe8, 0xe3, 0x01, 0xa1, 0xbb, 0x90, 0xf6, 0xf9, 0x34, 0x93, 0xf5, + 0x23, 0x94, 0x91, 0x64, 0xfa, 0x3e, 0xd4, 0x54, 0xa6, 0xc1, 0x37, 0xbf, 0xe6, 0xa9, 0x41, 0xe5, 0x36, 0x98, 0x44, + 0x83, 0x92, 0x3b, 0x07, 0x18, 0x7e, 0xa4, 0x5c, 0xd5, 0xab, 0xa2, 0x93, 0x56, 0x66, 0x6a, 0x7f, 0x90, 0x39, 0x02, + 0x93, 0xd3, 0x43, 0x33, 0xd2, 0x40, 0x88, 0x00, 0x2f, 0x10, 0x88, 0xbc, 0x04, 0xca, 0x00, 0xb6, 0xe9, 0x5e, 0x1b, + 0x34, 0xc6, 0xe3, 0xf1, 0x33, 0xa2, 0x98, 0x48, 0x2a, 0xdf, 0x13, 0xc7, 0xd1, 0x68, 0xb1, 0x88, 0x54, 0xd0, 0x84, + 0x62, 0x06, 0xfe, 0xdc, 0x7c, 0xb0, 0x3c, 0xeb, 0x7d, 0xd6, 0x0c, 0x63, 0x4c, 0xb3, 0x74, 0xd3, 0x26, 0xe7, 0xc8, + 0xdd, 0x4f, 0x58, 0x5a, 0x33, 0x42, 0x46, 0x09, 0x9b, 0x32, 0xb4, 0xea, 0x5a, 0x57, 0x8a, 0x63, 0x38, 0x46, 0xe3, + 0xfc, 0x1d, 0x59, 0x74, 0xf8, 0x53, 0x8d, 0x4f, 0x1f, 0x63, 0xa4, 0xe5, 0xf9, 0xd9, 0xb7, 0x09, 0xc4, 0x2f, 0xa3, + 0x1a, 0x75, 0x25, 0xc2, 0xa2, 0x65, 0x82, 0xd4, 0x61, 0x43, 0x2f, 0x23, 0x5e, 0x5e, 0xb3, 0xb8, 0x23, 0x41, 0x0f, + 0x4a, 0xec, 0x89, 0x86, 0xd4, 0xed, 0x99, 0xd8, 0xda, 0x26, 0xf5, 0xfa, 0xf3, 0xc9, 0x4f, 0xf3, 0x64, 0x7f, 0x5c, + 0x26, 0x75, 0x8e, 0x0a, 0xc4, 0x51, 0x7b, 0xbb, 0xcc, 0x77, 0xc6, 0x5c, 0x79, 0xf4, 0xdd, 0x56, 0x32, 0x46, 0xd1, + 0x8c, 0xb4, 0x6c, 0x1c, 0x18, 0xd5, 0xc5, 0x0e, 0xd5, 0x77, 0x0a, 0xcb, 0x8f, 0xaf, 0xe4, 0xc8, 0x23, 0x4a, 0x02, + 0x55, 0xd7, 0x8f, 0x24, 0x94, 0x86, 0x71, 0x7e, 0x35, 0xd6, 0x3e, 0x26, 0xd7, 0x06, 0xc5, 0xd2, 0x9e, 0xc7, 0x8c, + 0x8f, 0xb8, 0xf9, 0xcb, 0xb5, 0x1e, 0x64, 0x45, 0xed, 0x39, 0xf1, 0x74, 0xd4, 0xa1, 0x6d, 0x16, 0x93, 0x4d, 0x30, + 0xc0, 0x07, 0x68, 0xc2, 0xda, 0x63, 0x51, 0xeb, 0x8f, 0xc1, 0xd7, 0x3e, 0x40, 0x80, 0x6b, 0x21, 0xac, 0x9c, 0xa2, + 0x40, 0xe9, 0xda, 0x96, 0x5c, 0x1f, 0xef, 0xda, 0xfd, 0x28, 0x23, 0x91, 0xed, 0x2a, 0x29, 0x51, 0x6c, 0xa7, 0x29, + 0xf5, 0x77, 0x4a, 0x7d, 0xf0, 0x28, 0x22, 0x3e, 0xe3, 0x44, 0x8f, 0x4f, 0x56, 0xdd, 0x3c, 0x69, 0x4f, 0x7a, 0xa1, + 0x74, 0x03, 0x5e, 0x5c, 0x56, 0xdd, 0x14, 0x9f, 0x1c, 0x7b, 0x29, 0x62, 0x6b, 0x09, 0xb2, 0x45, 0x14, 0x6d, 0x07, + 0x39, 0xe4, 0x7b, 0x16, 0x29, 0xa4, 0x66, 0xd3, 0x29, 0xee, 0x00, 0x3b, 0xc5, 0x78, 0xe5, 0x88, 0x59, 0xab, 0xc9, + 0x9c, 0x6b, 0x14, 0xc8, 0xcb, 0xa0, 0xea, 0xf7, 0xf6, 0x03, 0x79, 0x37, 0x9e, 0x3f, 0x09, 0xa9, 0x5c, 0x85, 0x9d, + 0xf9, 0xbd, 0xd0, 0xf8, 0x77, 0xa7, 0x3d, 0x89, 0x3c, 0x3c, 0xcc, 0x2f, 0x49, 0xef, 0xf7, 0x71, 0x5f, 0x90, 0x6b, + 0xf8, 0x59, 0x88, 0x84, 0x26, 0x7e, 0xb3, 0x29, 0x90, 0x3c, 0x56, 0x08, 0xb8, 0x50, 0x49, 0x35, 0x8b, 0xb5, 0x25, + 0x9c, 0xd3, 0x83, 0xfb, 0x19, 0x73, 0xee, 0x30, 0x3c, 0xc8, 0x95, 0xd0, 0xb8, 0xbc, 0xc6, 0xdd, 0xa0, 0xb6, 0xfe, + 0x45, 0x58, 0xc2, 0x6b, 0x64, 0x89, 0xb4, 0x2c, 0x9f, 0x51, 0xea, 0x04, 0x0d, 0x5f, 0xba, 0x50, 0x8c, 0xd7, 0x21, + 0x4e, 0xf5, 0x10, 0xdd, 0xdf, 0xb7, 0x23, 0xb5, 0x32, 0xde, 0x7e, 0x7a, 0xe3, 0xe1, 0xe9, 0xf0, 0x34, 0xee, 0x4a, + 0x3c, 0xa3, 0x5e, 0x06, 0x7f, 0x34, 0x64, 0x4a, 0x4d, 0x4f, 0xf1, 0xf6, 0xbf, 0x4a, 0xbd, 0xfe, 0x70, 0xe1, 0x7a, + 0x87, 0x49, 0x20, 0x9f, 0x94, 0x6f, 0x27, 0x53, 0xab, 0x9b, 0x27, 0xbb, 0x7b, 0xf5, 0xfc, 0x33, 0xcf, 0xa4, 0x8c, + 0x1b, 0x9c, 0x38, 0xea, 0x29, 0xb5, 0x89, 0x0a, 0x15, 0x3c, 0x47, 0xcf, 0x74, 0x6b, 0x7b, 0xdc, 0x3c, 0xde, 0x4c, + 0x33, 0x7f, 0xc4, 0x94, 0x27, 0xc5, 0xd6, 0xd3, 0x8d, 0x50, 0x6e, 0x28, 0xde, 0x85, 0x52, 0xd8, 0x84, 0xcf, 0xe8, + 0x3f, 0x9b, 0x30, 0x59, 0x45, 0x48, 0xfe, 0x40, 0xa0, 0x7c, 0x2a, 0xb3, 0x21, 0xed, 0x26, 0xa1, 0xa6, 0x85, 0x9c, + 0xa4, 0x9c, 0x66, 0xb2, 0x44, 0xd5, 0x00, 0x70, 0xe4, 0xa8, 0xb7, 0x88, 0x1b, 0xbc, 0xf3, 0x0b, 0x50, 0x38, 0x98, + 0xfa, 0x5b, 0x4f, 0xa2, 0x36, 0x77, 0x92, 0x72, 0x04, 0x93, 0xa2, 0xd8, 0x9d, 0x14, 0xb6, 0x5b, 0xe4, 0x2c, 0x6e, + 0xf1, 0x21, 0xa9, 0x2a, 0x42, 0x64, 0x31, 0x30, 0xc4, 0xab, 0x89, 0x76, 0x94, 0xe1, 0xc0, 0x37, 0x0b, 0x33, 0x9d, + 0xf0, 0xea, 0xb1, 0x8b, 0x04, 0x95, 0xc2, 0xcf, 0xd2, 0xc8, 0x12, 0xa7, 0xf4, 0xe0, 0x84, 0x01, 0xb7, 0xdc, 0x8a, + 0xd5, 0xf7, 0x57, 0x94, 0x99, 0x50, 0x9a, 0x89, 0xb1, 0xa2, 0x7e, 0x40, 0xc0, 0x3d, 0x49, 0x98, 0x78, 0x22, 0xf4, + 0xd6, 0x76, 0xcd, 0x3f, 0xa9, 0x3e, 0x5b, 0xf8, 0x42, 0x6c, 0x01, 0xf3, 0x86, 0xc0, 0x04, 0x1a, 0x37, 0x9b, 0x51, + 0x2c, 0xa1, 0xf1, 0x03, 0xca, 0xa2, 0xdb, 0x59, 0x82, 0xaa, 0xb7, 0x8a, 0x0e, 0x43, 0x5d, 0x00, 0x2d, 0xad, 0x9e, + 0xfd, 0x98, 0xeb, 0x7d, 0x1e, 0xe5, 0x56, 0x1f, 0x60, 0xac, 0x6e, 0x00, 0x1d, 0x69, 0xd8, 0xf6, 0x6a, 0x78, 0xb9, + 0xa7, 0x9a, 0x88, 0x33, 0x9e, 0x2c, 0xaf, 0x0c, 0xfd, 0x86, 0x6c, 0x3d, 0xee, 0x3c, 0x51, 0xbb, 0xa8, 0xbc, 0xec, + 0x00, 0x91, 0x5a, 0x58, 0xd9, 0x8c, 0x7a, 0x81, 0x64, 0x5d, 0xdf, 0xac, 0x4a, 0x48, 0xd2, 0x23, 0xec, 0x13, 0xfe, + 0x7a, 0x19, 0x49, 0xa8, 0xa0, 0xd5, 0x4c, 0x65, 0xe9, 0xda, 0x6c, 0x40, 0x2b, 0xc0, 0x40, 0x67, 0xe2, 0x21, 0x70, + 0xf4, 0xba, 0x5e, 0x7a, 0xe4, 0x33, 0x4c, 0x7d, 0x18, 0x4a, 0x6a, 0x96, 0x8d, 0xb6, 0x9e, 0xc4, 0xcf, 0xe9, 0x58, + 0x62, 0x43, 0x0b, 0x09, 0x6b, 0xd2, 0xde, 0x16, 0x7e, 0xd5, 0x99, 0xdd, 0xd4, 0xfb, 0xce, 0xe7, 0x22, 0x44, 0x58, + 0x79, 0x7e, 0x51, 0xaa, 0xb1, 0xa4, 0x10, 0xe1, 0xdd, 0xec, 0x85, 0x95, 0x58, 0xd6, 0x36, 0xef, 0x2b, 0xd3, 0xfc, + 0x4c, 0x4e, 0x7f, 0xed, 0x18, 0xa8, 0xa0, 0x5f, 0xf3, 0x72, 0x6b, 0x76, 0x22, 0x82, 0x47, 0xa5, 0x20, 0x1f, 0x68, + 0xe2, 0xb4, 0x29, 0x47, 0xdd, 0xbe, 0x8b, 0x55, 0x69, 0xbf, 0x01, 0x07, 0x6e, 0xff, 0x0d, 0xb0, 0x02, 0x29, 0x40, + 0xc0, 0xcc, 0xbd, 0xac, 0xb2, 0x1e, 0x84, 0x36, 0xc8, 0xa0, 0xcf, 0x49, 0xfc, 0xc1, 0xc7, 0x3d, 0xcb, 0x92, 0x81, + 0xad, 0x40, 0x0b, 0x08, 0x40, 0xe1, 0x36, 0xa2, 0x9f, 0xdf, 0x40, 0xbe, 0x62, 0x7e, 0xd4, 0xe0, 0x84, 0xfa, 0x2c, + 0xba, 0x2e, 0x82, 0xf3, 0x31, 0xb2, 0xf1, 0x07, 0x56, 0x43, 0x68, 0x22, 0xe2, 0xa8, 0x0d, 0x8a, 0x94, 0xa8, 0xa1, + 0x23, 0x3f, 0x35, 0x06, 0xda, 0xaa, 0xe2, 0x35, 0x7e, 0xd6, 0x66, 0xb7, 0x2e, 0x60, 0x91, 0x1f, 0x9c, 0x1e, 0xb9, + 0x20, 0xcc, 0x1e, 0xdc, 0x34, 0xfd, 0xbf, 0xa5, 0x70, 0xf9, 0x40, 0xcf, 0xc6, 0x63, 0x4d, 0xf1, 0x54, 0x39, 0xd3, + 0xc1, 0x8d, 0x91, 0x1f, 0xa5, 0xce, 0x21, 0xac, 0x14, 0xfe, 0x5b, 0xe6, 0x73, 0xbb, 0xf5, 0x61, 0xb2, 0xbb, 0x2c, + 0x88, 0xe0, 0xe2, 0x92, 0xbd, 0x41, 0xc5, 0x1b, 0x90, 0x39, 0x04, 0xd9, 0x3b, 0x9f, 0x6a, 0x7f, 0x6c, 0x28, 0x95, + 0x5f, 0xd7, 0x36, 0xdf, 0x86, 0x37, 0x07, 0xe9, 0x16, 0x48, 0xac, 0xd7, 0x04, 0x6d, 0xe5, 0xf9, 0x12, 0xcd, 0x06, + 0x0d, 0x45, 0x63, 0x66, 0xf7, 0x17, 0x75, 0xe6, 0x2a, 0xb8, 0x75, 0xf7, 0x42, 0xa3, 0xa2, 0x58, 0xe8, 0x7b, 0x95, + 0x4d, 0xe0, 0xa2, 0x87, 0x57, 0x32, 0x4f, 0xb7, 0x2b, 0x12, 0xb5, 0xd8, 0x08, 0x31, 0xcb, 0x1b, 0xdc, 0xde, 0x55, + 0xf6, 0x67, 0xb8, 0x93, 0x0d, 0x30, 0x5b, 0xd0, 0x5b, 0x76, 0x48, 0x90, 0xfa, 0xd4, 0x29, 0xe5, 0x97, 0xf5, 0x47, + 0x99, 0xb6, 0xc0, 0xab, 0xf5, 0x40, 0x15, 0x73, 0x30, 0x43, 0x9d, 0x56, 0xdc, 0xeb, 0x44, 0x32, 0xf4, 0xae, 0x28, + 0xcd, 0x20, 0x11, 0xf6, 0x09, 0x2f, 0x61, 0xfa, 0x01, 0x2b, 0x2f, 0xb7, 0xf0, 0xc6, 0xb1, 0xec, 0xb5, 0x3a, 0x28, + 0x09, 0xaa, 0x80, 0xfc, 0x61, 0x78, 0xd6, 0xb2, 0x26, 0x77, 0x87, 0x23, 0x81, 0x2f, 0x17, 0x32, 0x11, 0xcc, 0x0d, + 0xe4, 0xcb, 0xb9, 0xb8, 0x10, 0x89, 0x2a, 0xc4, 0x78, 0xc9, 0xd2, 0xd1, 0xbb, 0x71, 0xd2, 0xa8, 0xd5, 0xf4, 0xa1, + 0x50, 0x71, 0x1b, 0xd7, 0x7a, 0x74, 0xbc, 0x60, 0x39, 0x1b, 0x8d, 0xee, 0x8a, 0x75, 0x4b, 0x79, 0x0b, 0xa5, 0x11, + 0x36, 0x52, 0x5f, 0x90, 0x65, 0x69, 0x16, 0x58, 0x2f, 0xc0, 0x16, 0xc1, 0x62, 0xc0, 0xf2, 0xd6, 0x59, 0x16, 0xb1, + 0xfa, 0xbd, 0xaf, 0x55, 0x8e, 0xc3, 0x90, 0x25, 0x21, 0x89, 0xe6, 0x55, 0x14, 0xc6, 0x18, 0x6a, 0x1c, 0x4d, 0x51, + 0xa5, 0x84, 0x31, 0x77, 0x23, 0xc3, 0x2e, 0xd6, 0x39, 0xc6, 0xd2, 0x48, 0xd2, 0xf0, 0x4d, 0x39, 0xa6, 0x27, 0xab, + 0xb1, 0x36, 0x22, 0x1b, 0x39, 0x34, 0x9e, 0xcb, 0xd5, 0x8c, 0x55, 0xee, 0xd0, 0xdd, 0x5a, 0xa9, 0xec, 0x42, 0x13, + 0x0a, 0xa3, 0xbd, 0xc6, 0x35, 0xc9, 0xa2, 0x5d, 0x83, 0x55, 0xfa, 0x92, 0x66, 0x8f, 0x38, 0x94, 0x6f, 0xc3, 0x56, + 0x55, 0xea, 0x02, 0xcd, 0xf9, 0xd0, 0x2b, 0xfc, 0x8d, 0x74, 0x72, 0x8e, 0x8a, 0x1e, 0xdc, 0x74, 0xdb, 0xc5, 0xbf, + 0x68, 0xa1, 0xfb, 0x2c, 0x7f, 0xce, 0x3c, 0x16, 0x2a, 0x54, 0xab, 0xab, 0x89, 0x2d, 0x99, 0xa1, 0xe1, 0x6b, 0x02, + 0xae, 0x44, 0xbe, 0x18, 0x60, 0x67, 0x94, 0xce, 0x25, 0xed, 0x54, 0x0e, 0x49, 0x4b, 0x36, 0x4e, 0xdc, 0x64, 0x23, + 0xda, 0xe5, 0x8f, 0xb1, 0xc5, 0xca, 0x4b, 0xd6, 0xad, 0x0f, 0xac, 0xf3, 0xf8, 0x3c, 0xab, 0xbc, 0x75, 0x6f, 0xc6, + 0xbf, 0xda, 0x0c, 0x13, 0xf6, 0xce, 0x6e, 0x70, 0xa9, 0xec, 0xd8, 0xa8, 0x91, 0xd3, 0x13, 0x3b, 0x5a, 0xe6, 0x22, + 0xc3, 0x6b, 0xb4, 0xaa, 0xb1, 0x90, 0xe3, 0x16, 0x7e, 0x0e, 0x34, 0x12, 0x8b, 0xa4, 0x58, 0x40, 0xe7, 0xfb, 0xd5, + 0x87, 0x17, 0x58, 0xcd, 0x63, 0xae, 0xc9, 0xd4, 0xa2, 0xce, 0x9c, 0xba, 0x50, 0x7d, 0x5e, 0x75, 0x5f, 0xd7, 0x2a, + 0xb8, 0x10, 0xd7, 0x9f, 0xa0, 0xe9, 0xaa, 0x9e, 0xfb, 0x96, 0x83, 0xd4, 0x94, 0x67, 0x10, 0xc7, 0xfa, 0xd3, 0x73, + 0x73, 0x23, 0x5b, 0xad, 0x8f, 0xd6, 0x51, 0x26, 0x5e, 0x8c, 0xc4, 0x16, 0x7e, 0xc7, 0x19, 0xd4, 0xa2, 0xbe, 0xcf, + 0x2a, 0x8a, 0x93, 0x80, 0xcb, 0x70, 0x05, 0x27, 0x30, 0xd5, 0x02, 0x03, 0x25, 0x39, 0xd1, 0x80, 0x46, 0xd6, 0xb9, + 0x3a, 0x78, 0xb9, 0x33, 0xdf, 0x34, 0x09, 0xa1, 0x83, 0x39, 0x83, 0x7b, 0x25, 0xdf, 0xec, 0xbb, 0x4a, 0x1d, 0x4c, + 0xb5, 0xf3, 0xda, 0x84, 0xad, 0x66, 0x7a, 0xda, 0x35, 0xb4, 0x42, 0xf4, 0x5c, 0x52, 0xcf, 0x90, 0x32, 0x56, 0x91, + 0xaa, 0x59, 0x1a, 0x87, 0x77, 0x8f, 0x84, 0x94, 0x29, 0xdb, 0x9d, 0x83, 0xf3, 0x0e, 0xa2, 0x12, 0xa9, 0xb2, 0x6e, + 0x0b, 0x23, 0x03, 0x3d, 0xe7, 0x58, 0x57, 0x51, 0xac, 0xa0, 0x18, 0x82, 0x5c, 0xe8, 0xa4, 0x15, 0x49, 0xa5, 0x1f, + 0x77, 0x16, 0x96, 0x51, 0x67, 0x65, 0x2e, 0x96, 0xcd, 0x75, 0xd4, 0xbb, 0x51, 0xff, 0xcc, 0xbb, 0x76, 0x39, 0x1d, + 0x9b, 0xc0, 0x4c, 0x28, 0x85, 0x05, 0xd2, 0x2c, 0x7f, 0x8b, 0xd3, 0xfb, 0xf1, 0xae, 0xe8, 0xd7, 0xc3, 0x66, 0x21, + 0x73, 0xb6, 0x02, 0x07, 0x90, 0xe9, 0xb8, 0xfa, 0x9d, 0x23, 0xa3, 0x8c, 0x42, 0x21, 0xad, 0xef, 0x41, 0x31, 0xd8, + 0x8e, 0xa9, 0x84, 0xe8, 0xd8, 0xdc, 0xcd, 0x00, 0x1d, 0xb4, 0xb1, 0xd5, 0x7b, 0x08, 0x36, 0x93, 0xb4, 0xe2, 0x2c, + 0x81, 0x8e, 0xd5, 0x4f, 0x2d, 0x55, 0x2f, 0x0d, 0x81, 0x41, 0xbf, 0x05, 0x82, 0xc0, 0x0b, 0x11, 0x7e, 0x66, 0x5e, + 0xd9, 0x20, 0xc2, 0x43, 0xf7, 0x06, 0xa0, 0x0c, 0xb1, 0xd6, 0x51, 0x2f, 0x8b, 0x85, 0xf7, 0x97, 0x05, 0x6d, 0xd1, + 0xcc, 0x51, 0x24, 0xa0, 0x7f, 0x85, 0x13, 0x57, 0x96, 0xf1, 0x09, 0x20, 0xa0, 0xcf, 0x91, 0xa4, 0xf8, 0xe8, 0x7d, + 0xaf, 0x9f, 0xa6, 0x94, 0x48, 0x9d, 0xf3, 0xd2, 0x93, 0xdc, 0xe0, 0xef, 0x3b, 0xcf, 0x1b, 0xaf, 0xac, 0x4a, 0x9e, + 0xfb, 0x7b, 0xba, 0x64, 0x71, 0x3d, 0x70, 0x7c, 0xb5, 0x94, 0xc9, 0xe6, 0xca, 0xc5, 0x04, 0x59, 0xb0, 0xf1, 0xbe, + 0x67, 0x46, 0x61, 0xdf, 0x40, 0xbe, 0x2b, 0xe6, 0x23, 0x8c, 0x6b, 0x2b, 0x9e, 0xbd, 0x15, 0x0f, 0x73, 0x4e, 0x49, + 0x91, 0xd4, 0x76, 0x4e, 0x81, 0x54, 0x67, 0x54, 0x5b, 0x90, 0x21, 0xe6, 0x02, 0x59, 0xf5, 0x29, 0x0e, 0xce, 0x96, + 0xa6, 0x81, 0x28, 0x5a, 0xca, 0x8f, 0x0a, 0x15, 0x82, 0xff, 0x1a, 0x88, 0x99, 0x46, 0x15, 0x60, 0x6e, 0x24, 0xd4, + 0xe1, 0x20, 0x9e, 0xf0, 0x74, 0x2f, 0x4d, 0x2b, 0x4d, 0x27, 0xee, 0xb4, 0x88, 0xa8, 0xfe, 0x72, 0x6e, 0x93, 0xa0, + 0x59, 0xf5, 0x2a, 0x0a, 0x97, 0x62, 0x49, 0x04, 0xd7, 0xcb, 0xea, 0xaa, 0x1f, 0x51, 0xaa, 0x7b, 0x65, 0xc1, 0x75, + 0xce, 0x02, 0x83, 0xe3, 0x5b, 0x8f, 0x74, 0x7b, 0x9e, 0x2e, 0xaf, 0x91, 0xdb, 0xa6, 0xc0, 0x8d, 0x8f, 0x99, 0xd0, + 0x95, 0xb8, 0x9a, 0x0d, 0x74, 0x85, 0x79, 0xdb, 0xae, 0xf8, 0x4a, 0xb0, 0x36, 0xff, 0x75, 0x3f, 0x03, 0xef, 0x8b, + 0x17, 0x61, 0xc1, 0x4c, 0x15, 0x8c, 0x62, 0xe2, 0x17, 0x61, 0x89, 0x30, 0xbc, 0x68, 0x6e, 0xce, 0xf6, 0xf9, 0xe6, + 0x3c, 0x02, 0x1c, 0x16, 0xe5, 0x09, 0x73, 0x7b, 0x06, 0x14, 0x54, 0x9b, 0xb0, 0xa9, 0xd6, 0x80, 0xb1, 0x3d, 0x4b, + 0xf3, 0x31, 0xdf, 0x9b, 0x0e, 0x50, 0x4f, 0xad, 0x39, 0xc5, 0x60, 0x0c, 0x61, 0xa2, 0xdb, 0x80, 0x02, 0xa4, 0x26, + 0x0b, 0x87, 0xcc, 0xfa, 0x5b, 0xca, 0x0b, 0x6d, 0x62, 0x43, 0x5f, 0x92, 0xa5, 0xb5, 0x56, 0xf0, 0x13, 0x34, 0x4d, + 0xc1, 0x29, 0x0e, 0x3f, 0x48, 0xbc, 0xe7, 0xde, 0x79, 0x8d, 0x44, 0x46, 0x3d, 0x17, 0x7e, 0x21, 0xc2, 0xca, 0x7d, + 0xc4, 0x9c, 0x73, 0x53, 0x13, 0xb2, 0x2f, 0x5d, 0xb2, 0x96, 0xd5, 0x24, 0xe0, 0xd1, 0x73, 0xa1, 0x42, 0x3b, 0x23, + 0xde, 0x5d, 0x5b, 0x79, 0xab, 0x7a, 0x34, 0x03, 0x56, 0x73, 0xdc, 0xb6, 0x98, 0x86, 0xa9, 0x28, 0xa9, 0x84, 0x20, + 0x6e, 0x09, 0x91, 0x85, 0x61, 0xcb, 0x1a, 0x7b, 0x9f, 0x58, 0xad, 0xa7, 0x24, 0x00, 0x70, 0x25, 0x0d, 0xdd, 0x33, + 0x94, 0x09, 0xa9, 0x97, 0xb4, 0x40, 0x39, 0xe4, 0x6a, 0xe2, 0xe5, 0xc6, 0x3d, 0x86, 0x81, 0x1b, 0xb3, 0xb5, 0xc8, + 0x34, 0x26, 0x44, 0x96, 0x81, 0x00, 0x71, 0x68, 0x5e, 0x9a, 0xca, 0xa2, 0xd3, 0x4d, 0x50, 0x74, 0x51, 0x8f, 0x33, + 0x5c, 0x59, 0x88, 0xbb, 0x64, 0xe8, 0x1c, 0x78, 0x39, 0x5d, 0xe3, 0xe5, 0x24, 0x15, 0x02, 0xaf, 0x82, 0x95, 0x07, + 0x12, 0xd9, 0x03, 0xed, 0xa0, 0x6c, 0x00, 0x24, 0xb9, 0x13, 0x5c, 0x29, 0x48, 0x6b, 0x2b, 0xc8, 0x21, 0xfe, 0xa7, + 0xb6, 0x1c, 0xa5, 0x02, 0xf2, 0xd4, 0xb1, 0xe5, 0xa4, 0xf1, 0x3c, 0x5c, 0x0a, 0x6f, 0xa4, 0x36, 0xcc, 0x60, 0xc5, + 0x0a, 0x16, 0x22, 0x33, 0x25, 0xcf, 0xad, 0x60, 0x1b, 0xaf, 0xde, 0xc4, 0x8c, 0x44, 0x85, 0xe9, 0xa3, 0xd8, 0x59, + 0xdd, 0x0d, 0x13, 0x6c, 0x2b, 0x9e, 0xb2, 0xdb, 0x8f, 0xc8, 0x7f, 0x4c, 0x50, 0x92, 0xa6, 0xc3, 0x97, 0x4a, 0xa6, + 0x93, 0xf2, 0xe2, 0x9d, 0x16, 0x46, 0x4b, 0x0e, 0x01, 0x17, 0x7c, 0x06, 0xde, 0x9d, 0x89, 0xfc, 0xcb, 0xa6, 0x35, + 0xc9, 0x1c, 0xa3, 0xaa, 0x8a, 0x16, 0x12, 0x8d, 0x71, 0x51, 0xb6, 0x26, 0x16, 0x0f, 0x16, 0x57, 0x03, 0x48, 0xa6, + 0x31, 0x2c, 0xf0, 0xf2, 0xc8, 0x7c, 0xcd, 0xe6, 0x45, 0xf5, 0x44, 0x96, 0x8a, 0x2e, 0xc8, 0xe5, 0x67, 0x18, 0x9b, + 0x99, 0x32, 0xac, 0x16, 0xcb, 0x70, 0x38, 0x2b, 0x08, 0x7b, 0x3f, 0xe0, 0x82, 0xe7, 0xa6, 0x8f, 0xbe, 0x5c, 0x30, + 0xa9, 0x1c, 0x46, 0x26, 0x7f, 0x96, 0x5c, 0xbc, 0x22, 0xac, 0x9e, 0x6c, 0xe7, 0x00, 0x72, 0xa1, 0x06, 0xe5, 0x08, + 0x87, 0x96, 0x13, 0xd8, 0xc4, 0x58, 0x44, 0x67, 0xd5, 0x54, 0x35, 0xc2, 0xd2, 0x7c, 0xe9, 0x46, 0x99, 0x37, 0xd9, + 0x76, 0x86, 0x4c, 0xd8, 0xba, 0x1f, 0x89, 0x20, 0x37, 0x1e, 0x0c, 0xa5, 0x31, 0xef, 0xc3, 0x1a, 0xac, 0xfa, 0x44, + 0x5e, 0xce, 0xa3, 0xaa, 0x11, 0x32, 0xc3, 0x29, 0xf9, 0x69, 0xf4, 0x14, 0x4d, 0x77, 0x92, 0x13, 0x2a, 0xda, 0x24, + 0x2a, 0x0a, 0x6c, 0xe2, 0x45, 0x29, 0x24, 0x82, 0xe2, 0x2e, 0xc7, 0x43, 0x0d, 0xf3, 0x0f, 0x27, 0x07, 0x57, 0x22, + 0x56, 0x07, 0x6e, 0xef, 0x1b, 0x48, 0xf2, 0x33, 0x99, 0xf4, 0x46, 0xba, 0x77, 0x37, 0xe5, 0xe1, 0x69, 0x22, 0x33, + 0xf7, 0x91, 0xb8, 0x8e, 0x8b, 0x8a, 0x42, 0x05, 0xdc, 0x6f, 0xd5, 0x5e, 0x7c, 0x92, 0x74, 0x82, 0xcc, 0x30, 0x73, + 0x6d, 0x7c, 0x6e, 0x8a, 0x91, 0x9a, 0x93, 0x54, 0x81, 0x7c, 0x72, 0xf7, 0x97, 0x46, 0x90, 0x9b, 0xf0, 0x17, 0xdf, + 0x3b, 0xaf, 0x93, 0xf2, 0x1c, 0xed, 0xe7, 0x44, 0xf4, 0xba, 0x1c, 0x6d, 0x31, 0x68, 0x63, 0x4e, 0x8c, 0x7c, 0xbb, + 0xbb, 0x88, 0x7c, 0xb9, 0xe1, 0x14, 0xc3, 0x54, 0x05, 0x32, 0x79, 0xf8, 0x43, 0x2d, 0x0f, 0x84, 0xfd, 0x29, 0x99, + 0x61, 0xa6, 0x89, 0xa8, 0xc2, 0x16, 0x38, 0x05, 0x0e, 0xd4, 0x5c, 0x39, 0x51, 0xf3, 0x70, 0xa0, 0x4a, 0x71, 0xf6, + 0x49, 0x22, 0xce, 0x5c, 0xc6, 0x36, 0x7e, 0x25, 0x5d, 0x30, 0x97, 0xd5, 0x48, 0x5b, 0x11, 0x2d, 0x8f, 0x14, 0x02, + 0x82, 0x5a, 0x8a, 0xa5, 0xd8, 0x12, 0x40, 0x30, 0xbe, 0xc5, 0xf3, 0xfb, 0x18, 0xb1, 0x0a, 0xc5, 0xcb, 0x34, 0xb2, + 0xa2, 0x5d, 0x7e, 0x63, 0x17, 0xa6, 0x0b, 0x26, 0xe0, 0x66, 0x24, 0xc5, 0xc8, 0x73, 0x87, 0x57, 0xe5, 0x46, 0xea, + 0x74, 0xbf, 0x2d, 0x0a, 0x05, 0x4f, 0x8b, 0x46, 0xe7, 0xc6, 0x54, 0x11, 0x5c, 0x35, 0x2a, 0xb6, 0x38, 0x38, 0x9c, + 0x7f, 0xa8, 0x99, 0x85, 0x74, 0x4d, 0x94, 0x23, 0x89, 0xfc, 0x7e, 0x11, 0x1c, 0x6a, 0x94, 0x17, 0xa2, 0x10, 0xa9, + 0x9f, 0x18, 0x72, 0x59, 0xc4, 0xec, 0x30, 0x37, 0xaa, 0xcb, 0x16, 0xc0, 0x96, 0xae, 0xc3, 0xc8, 0x50, 0x88, 0x3c, + 0x62, 0x98, 0x99, 0x26, 0xf5, 0x71, 0xe5, 0x20, 0x8b, 0xae, 0x52, 0x83, 0x34, 0xef, 0xb8, 0x91, 0x37, 0x49, 0xa2, + 0x84, 0x0c, 0xf1, 0xcc, 0x7c, 0x52, 0x67, 0x27, 0xb1, 0x57, 0x69, 0x29, 0xa4, 0x23, 0xd5, 0x4d, 0xa2, 0xd8, 0xe9, + 0x3e, 0x13, 0x7a, 0x5f, 0xb5, 0xf7, 0xb1, 0x18, 0xbc, 0x6e, 0x9b, 0x30, 0x7f, 0xf4, 0xf9, 0x4d, 0x7c, 0x47, 0x4d, + 0xd5, 0x13, 0x69, 0x41, 0x27, 0xa1, 0x35, 0x00, 0xee, 0xf3, 0xe6, 0xce, 0xf6, 0x60, 0xb8, 0x4d, 0x00, 0x5a, 0xc1, + 0x59, 0x4e, 0x37, 0x42, 0x56, 0xb7, 0x4f, 0x5a, 0xa7, 0x89, 0x8b, 0x38, 0xd8, 0x01, 0xd2, 0x10, 0xb8, 0x0a, 0x3e, + 0x67, 0x5f, 0x21, 0xf5, 0x23, 0x35, 0xb1, 0xb3, 0x4d, 0xd2, 0x83, 0xb6, 0xf7, 0xc9, 0xe5, 0xbc, 0x9f, 0x67, 0x2c, + 0x26, 0x0b, 0xf7, 0x4e, 0xfe, 0x10, 0xae, 0xe2, 0xa8, 0x16, 0x23, 0xe6, 0x0a, 0x01, 0xa6, 0xaa, 0x61, 0xb8, 0xd9, + 0x57, 0x4a, 0x63, 0xdc, 0x62, 0x0a, 0x40, 0x05, 0xc7, 0xa4, 0x5e, 0x7d, 0x0c, 0x55, 0xeb, 0xfe, 0xe7, 0x0a, 0xd6, + 0xd5, 0x6e, 0x59, 0xef, 0xf4, 0x00, 0x98, 0x00, 0xfc, 0x01, 0xa8, 0xaa, 0xe7, 0xe5, 0xce, 0xbf, 0xb0, 0x57, 0x10, + 0xa4, 0x24, 0xe0, 0x5e, 0x25, 0xfd, 0xdf, 0x6a, 0x1a, 0x08, 0x9a, 0xaf, 0x97, 0xf5, 0xb1, 0xcf, 0x44, 0x22, 0xf7, + 0x3c, 0x69, 0xf1, 0xf1, 0x1e, 0x78, 0x0b, 0x38, 0x7e, 0x19, 0x5b, 0x97, 0x74, 0xce, 0xfc, 0x41, 0x02, 0xcb, 0x1b, + 0xb5, 0xaf, 0x1e, 0x5f, 0xd2, 0x89, 0x60, 0xa7, 0x28, 0x50, 0x1f, 0x22, 0x02, 0x4a, 0x04, 0x4a, 0x8e, 0xb4, 0x84, + 0xee, 0x27, 0x9f, 0xa0, 0x5a, 0x40, 0x48, 0x9d, 0x12, 0x16, 0xf5, 0xed, 0xa0, 0x8e, 0xe0, 0x6d, 0x33, 0x72, 0xe2, + 0xc0, 0xb9, 0x81, 0x93, 0xf2, 0x39, 0xec, 0x6a, 0x84, 0xcb, 0xe3, 0x0d, 0x9e, 0xc0, 0x97, 0xe8, 0x37, 0x8e, 0x6f, + 0xe2, 0x79, 0x8b, 0x41, 0xe4, 0x1c, 0xb2, 0x9c, 0x7c, 0x21, 0xa2, 0x46, 0x24, 0x09, 0x75, 0xd8, 0x85, 0x90, 0xd6, + 0x17, 0x30, 0x38, 0x5e, 0x31, 0x8d, 0xa1, 0x7a, 0x18, 0x83, 0xc1, 0xe6, 0xf9, 0xed, 0xe9, 0x74, 0xeb, 0x21, 0xf9, + 0x20, 0xea, 0x8b, 0x88, 0x77, 0x4d, 0xa9, 0x51, 0x64, 0x79, 0xd8, 0xb4, 0xae, 0x53, 0xc3, 0x7b, 0x88, 0xc3, 0xbf, + 0x0a, 0x90, 0x00, 0xc5, 0x6e, 0xd3, 0xe7, 0x5c, 0xb0, 0xd1, 0x3b, 0x4d, 0x44, 0x68, 0xa1, 0x19, 0xa4, 0x70, 0xd5, + 0x7c, 0x81, 0x95, 0x69, 0xa7, 0xff, 0x45, 0xe7, 0xb6, 0x24, 0x01, 0x41, 0xb4, 0xd2, 0xef, 0xab, 0x30, 0x61, 0x89, + 0x31, 0x01, 0xde, 0x11, 0x62, 0xce, 0x33, 0x58, 0x49, 0x2c, 0x40, 0x72, 0xb4, 0x5e, 0x97, 0x1f, 0xcb, 0x74, 0x8a, + 0xd1, 0xe8, 0x4d, 0x9d, 0x64, 0xaa, 0xf5, 0xb5, 0x04, 0xf0, 0xc7, 0x79, 0x0d, 0x5b, 0xe6, 0x1e, 0x08, 0xb0, 0x63, + 0x25, 0xa1, 0x49, 0xb7, 0x64, 0xa7, 0xba, 0xe3, 0x66, 0x93, 0x9a, 0x72, 0x3f, 0x6f, 0x55, 0xb2, 0x54, 0x82, 0xc3, + 0xba, 0xf6, 0xa0, 0xfc, 0x21, 0x15, 0xe6, 0x32, 0x54, 0x56, 0x7b, 0x08, 0x90, 0xb0, 0x94, 0xe4, 0xa3, 0x9a, 0x21, + 0xe5, 0xe3, 0x53, 0x45, 0x91, 0x90, 0x33, 0x5e, 0x2c, 0x6b, 0xc0, 0x00, 0xef, 0xce, 0x5d, 0x4a, 0xeb, 0x1d, 0x7a, + 0xe4, 0xbd, 0x47, 0xbc, 0x80, 0xbd, 0x29, 0x61, 0x8f, 0x3b, 0x04, 0x69, 0x5f, 0x33, 0x14, 0xf2, 0x6f, 0x86, 0x92, + 0xc6, 0xfe, 0x7d, 0xcb, 0xe9, 0x41, 0xcf, 0xc8, 0xf4, 0x91, 0x0b, 0x7f, 0xae, 0xf1, 0xf6, 0x83, 0x7b, 0xb6, 0x61, + 0x3c, 0xad, 0x24, 0x30, 0x64, 0x13, 0x77, 0xf3, 0x92, 0x57, 0x6c, 0xb1, 0x7c, 0x77, 0xfe, 0x3a, 0x59, 0xa3, 0x20, + 0x70, 0x0b, 0x3e, 0xd0, 0x32, 0x52, 0x69, 0x90, 0x94, 0x14, 0xaf, 0xce, 0x81, 0x49, 0xa7, 0x9b, 0x5a, 0x25, 0x6a, + 0xd5, 0xa0, 0x57, 0x7d, 0x8a, 0x61, 0x59, 0xd2, 0xb6, 0xc4, 0x42, 0xb3, 0xdf, 0x87, 0x01, 0x26, 0x3f, 0x46, 0xce, + 0xb8, 0xbd, 0x03, 0xba, 0x07, 0x45, 0x6d, 0x19, 0x27, 0x41, 0x52, 0xaa, 0x20, 0x80, 0x74, 0xbf, 0xce, 0x63, 0x79, + 0xd5, 0x31, 0xd1, 0x61, 0xd1, 0xaa, 0x11, 0xc8, 0x09, 0x75, 0x63, 0x04, 0x86, 0x90, 0x3d, 0x53, 0xb1, 0xf4, 0xd0, + 0xeb, 0x30, 0x54, 0x7d, 0xee, 0xc7, 0xb0, 0xa6, 0xd5, 0x98, 0x25, 0x0f, 0x92, 0xcc, 0xa8, 0xfa, 0x46, 0xdf, 0xa2, + 0xd5, 0x59, 0xcf, 0xf1, 0x9e, 0x37, 0xd3, 0xd0, 0x4d, 0x65, 0xff, 0xd0, 0xd8, 0x81, 0x7f, 0x5b, 0x46, 0xa2, 0x5a, + 0x72, 0x96, 0xf6, 0x4a, 0xe6, 0x53, 0x8f, 0x02, 0x54, 0xdf, 0xf1, 0xee, 0xd2, 0x80, 0x28, 0x39, 0x3a, 0x77, 0x9b, + 0x1b, 0x70, 0xa9, 0x3e, 0xd0, 0x38, 0x3d, 0x86, 0x62, 0x60, 0xe7, 0xb7, 0xaf, 0xa7, 0xeb, 0x10, 0x23, 0x87, 0x51, + 0xe0, 0x28, 0xbd, 0xf4, 0x2e, 0x79, 0xb5, 0xe2, 0xc6, 0x15, 0xb6, 0xbb, 0x97, 0x96, 0xdf, 0x25, 0xdb, 0xb0, 0x3a, + 0xc9, 0xfe, 0x18, 0xd9, 0xd8, 0xc7, 0x74, 0xac, 0x8e, 0xd1, 0xb9, 0x73, 0x00, 0x5c, 0xb9, 0x94, 0xc0, 0xdd, 0x4a, + 0xae, 0x8e, 0x7f, 0xe5, 0xf6, 0x54, 0x4e, 0x37, 0xbd, 0x2e, 0x5f, 0x3e, 0x39, 0xbb, 0x8a, 0x07, 0xad, 0xd0, 0x50, + 0x66, 0xe9, 0xb2, 0x4a, 0xea, 0x02, 0x79, 0xd6, 0xf1, 0x5c, 0xb8, 0xeb, 0x2f, 0xbd, 0x8d, 0xd0, 0x80, 0x3d, 0x43, + 0x58, 0xcd, 0xa5, 0xa1, 0x3f, 0x97, 0xb3, 0x1e, 0x7b, 0x8b, 0x26, 0x13, 0xed, 0x2d, 0x7a, 0x4c, 0x69, 0x1c, 0x27, + 0xec, 0x0f, 0x38, 0x35, 0xde, 0x87, 0x74, 0xb5, 0x80, 0xd5, 0xc3, 0x2f, 0x0c, 0xc8, 0xcc, 0x01, 0x6e, 0xf7, 0xfc, + 0x73, 0xca, 0xd7, 0xbc, 0x8a, 0x42, 0x75, 0x93, 0x07, 0xd5, 0x94, 0x6c, 0x59, 0x07, 0x1b, 0xf6, 0xcf, 0x0a, 0x41, + 0x2d, 0x80, 0xe5, 0xd4, 0x74, 0xd9, 0xec, 0x7d, 0x12, 0xda, 0xb6, 0x5b, 0x4a, 0x78, 0x6f, 0x61, 0x4f, 0xec, 0xce, + 0xf2, 0x34, 0x29, 0x0f, 0xe3, 0x7f, 0x4c, 0xc8, 0x74, 0xc8, 0x5d, 0xb5, 0x92, 0x96, 0x29, 0xa6, 0xca, 0xde, 0x6f, + 0x1c, 0xbb, 0x39, 0x63, 0x24, 0x3e, 0x41, 0x0d, 0x1f, 0x2e, 0x3b, 0x7a, 0xb4, 0xe8, 0xed, 0x07, 0x47, 0x1a, 0x98, + 0xfa, 0x41, 0x46, 0x6e, 0x2a, 0x63, 0x1d, 0x00, 0x25, 0x4b, 0xf4, 0x67, 0xcb, 0x2e, 0x2d, 0x2a, 0x44, 0xa1, 0xc2, + 0xed, 0xec, 0x0f, 0xf7, 0x32, 0xab, 0x14, 0x11, 0xed, 0xde, 0x95, 0xe0, 0x0c, 0x71, 0x47, 0xbc, 0xe5, 0xa4, 0x01, + 0xc5, 0x68, 0xd1, 0x41, 0x4b, 0x8a, 0xb6, 0x47, 0xeb, 0xd5, 0x52, 0xca, 0xf3, 0xcc, 0x89, 0xec, 0x28, 0x60, 0xfd, + 0x70, 0x38, 0xf4, 0xed, 0x67, 0x55, 0xa4, 0xdd, 0x8f, 0xd9, 0x02, 0x77, 0x00, 0xf7, 0x5b, 0x16, 0xa6, 0x18, 0xa2, + 0xf3, 0x97, 0xd4, 0x18, 0x5d, 0x3f, 0x0a, 0x41, 0x1b, 0x8c, 0x21, 0x4f, 0x98, 0x5c, 0x93, 0x84, 0x86, 0x34, 0x46, + 0xad, 0x51, 0x20, 0x39, 0x27, 0xa6, 0x91, 0x98, 0x2d, 0x58, 0x4f, 0x23, 0x29, 0x5d, 0x44, 0xc8, 0x4c, 0x50, 0xd1, + 0x83, 0x22, 0x58, 0x92, 0x91, 0x16, 0xa9, 0xdc, 0x8b, 0x8e, 0xe2, 0x3d, 0x1f, 0x41, 0x73, 0xcd, 0xad, 0x1a, 0xd2, + 0x83, 0xe5, 0x8d, 0x86, 0x82, 0xac, 0xd2, 0xf1, 0x92, 0xfb, 0xa8, 0x0e, 0x22, 0x83, 0xa6, 0xad, 0xdf, 0xf6, 0x97, + 0xf1, 0x58, 0x93, 0x79, 0x46, 0x24, 0x18, 0x32, 0x0c, 0x39, 0x8c, 0x91, 0x7b, 0xab, 0xd2, 0xd3, 0x0f, 0x32, 0xf4, + 0xbb, 0xc5, 0x08, 0x60, 0xe2, 0x2b, 0x61, 0xb2, 0x2e, 0x77, 0x6a, 0xd4, 0x79, 0x97, 0x71, 0x22, 0x63, 0xe1, 0xfe, + 0xa3, 0xb0, 0x36, 0x24, 0x5a, 0xaf, 0x6e, 0xec, 0xf9, 0xc7, 0x0d, 0x7e, 0x52, 0x9a, 0x22, 0x6a, 0x4d, 0x52, 0xa7, + 0x03, 0x75, 0x4b, 0x1c, 0x83, 0xa3, 0x7c, 0x5c, 0xbc, 0xf0, 0xa0, 0xa5, 0x72, 0x43, 0x49, 0xac, 0x44, 0xdf, 0xdc, + 0x23, 0xfb, 0x02, 0x1a, 0x7b, 0x0a, 0xba, 0xd9, 0xe2, 0xa8, 0x56, 0xc6, 0x50, 0x8a, 0x39, 0x1c, 0xf6, 0xa1, 0xac, + 0x61, 0xa5, 0x3a, 0xb6, 0x5e, 0x1a, 0x77, 0xe3, 0x81, 0xc8, 0x50, 0x3b, 0x34, 0x0e, 0x71, 0x5f, 0x33, 0x23, 0x37, + 0x43, 0x13, 0xde, 0x21, 0x63, 0x70, 0x27, 0x8e, 0x97, 0x1a, 0x4b, 0xc2, 0x48, 0x88, 0x41, 0xbf, 0xb8, 0x17, 0xb3, + 0x45, 0x15, 0x24, 0x88, 0x6b, 0x1b, 0x15, 0x60, 0xe3, 0x15, 0xa2, 0x42, 0x7b, 0x6c, 0xeb, 0x78, 0x9e, 0x19, 0xb9, + 0x02, 0xc3, 0xc4, 0x1b, 0xd9, 0x8d, 0x9e, 0xa7, 0x72, 0xfc, 0x17, 0x61, 0xf5, 0x33, 0x16, 0x6c, 0xdd, 0x8a, 0x82, + 0x3f, 0x41, 0xe8, 0xe1, 0x41, 0xfb, 0x79, 0x89, 0x75, 0xfc, 0x8f, 0xad, 0xdf, 0x50, 0xd3, 0xaa, 0xd3, 0xd0, 0x0f, + 0xc7, 0x0f, 0x9d, 0x46, 0x07, 0xf9, 0xa7, 0xaf, 0x2e, 0x2d, 0x6e, 0x9a, 0xee, 0x6a, 0x5c, 0xbb, 0xaf, 0x50, 0x7d, + 0x38, 0xb6, 0x55, 0x17, 0xec, 0x0f, 0xe3, 0x38, 0xdc, 0x80, 0xc7, 0xc3, 0xf3, 0xe0, 0x06, 0x3c, 0xb8, 0xbf, 0x34, + 0xa6, 0xc7, 0xb3, 0xe7, 0x4b, 0xef, 0x2e, 0xc3, 0xb9, 0xc8, 0x35, 0x26, 0x7b, 0xea, 0xd7, 0xb6, 0x8b, 0x23, 0x8d, + 0xc0, 0xe8, 0xe8, 0xcd, 0x74, 0x41, 0x8d, 0x6b, 0x92, 0x51, 0x6b, 0x50, 0x7e, 0x42, 0x38, 0xbd, 0x7f, 0x7f, 0x6b, + 0x74, 0x84, 0x42, 0xc4, 0x8b, 0xc0, 0x7f, 0xdf, 0xc1, 0xdb, 0x7a, 0xd8, 0x99, 0x56, 0x67, 0xb9, 0xc4, 0x53, 0xd8, + 0x57, 0xa3, 0x5b, 0xd7, 0xe3, 0xc8, 0x28, 0xbd, 0xfc, 0xe0, 0x25, 0xc6, 0xc9, 0x4d, 0x7e, 0xc4, 0xb1, 0xaa, 0xdb, + 0x8b, 0xd5, 0x9f, 0x07, 0x41, 0x11, 0xfe, 0xf1, 0x82, 0x8c, 0x0f, 0x91, 0x8e, 0x72, 0x2a, 0x96, 0x62, 0x5a, 0x51, + 0x8d, 0x03, 0x50, 0x34, 0xfa, 0x25, 0xf4, 0xd5, 0x34, 0x18, 0x9b, 0xe7, 0x4a, 0x18, 0xdf, 0xf1, 0xbf, 0x1f, 0xbc, + 0xfb, 0x05, 0x9b, 0xe5, 0x2e, 0x18, 0xd6, 0x7d, 0x18, 0xa9, 0x4f, 0x02, 0xa8, 0xac, 0x9e, 0x65, 0x35, 0xd1, 0x76, + 0x50, 0xc7, 0xab, 0x99, 0xed, 0xbf, 0xef, 0x1c, 0x42, 0x4f, 0xab, 0x99, 0x52, 0x40, 0xe5, 0x96, 0x77, 0x88, 0x87, + 0xfa, 0x12, 0xbe, 0x8f, 0xf5, 0x55, 0xcc, 0xaf, 0xa8, 0xfa, 0x32, 0x56, 0x51, 0x10, 0x9a, 0x1f, 0x30, 0x34, 0xfc, + 0x90, 0x3c, 0xe3, 0x86, 0x83, 0xb9, 0x5f, 0x42, 0xff, 0xb2, 0xbe, 0x3f, 0x24, 0xf6, 0xb5, 0x8f, 0xdb, 0x75, 0xf3, + 0x35, 0xa7, 0x74, 0x18, 0x25, 0x78, 0x8e, 0xe3, 0xe6, 0xd0, 0x59, 0x1b, 0xed, 0xe8, 0xd4, 0x17, 0x69, 0x1d, 0x5d, + 0x60, 0xe8, 0xfb, 0xcc, 0x25, 0x5e, 0x39, 0xe2, 0xa0, 0x8f, 0xc4, 0x0d, 0x47, 0xdd, 0x5e, 0xd5, 0x8e, 0xd1, 0x31, + 0x06, 0x79, 0x29, 0x04, 0x90, 0x1c, 0xaa, 0xa7, 0xcd, 0xa2, 0x4d, 0x57, 0xce, 0x06, 0xe5, 0x9f, 0xeb, 0x5e, 0x3c, + 0xa0, 0x05, 0xa3, 0xba, 0xe1, 0x2f, 0x1e, 0xd2, 0xb8, 0xa1, 0xe5, 0x28, 0x2a, 0x25, 0x45, 0xa0, 0xb4, 0x8d, 0x0a, + 0x7a, 0xb3, 0x40, 0xf9, 0x60, 0xe9, 0x8f, 0x85, 0x2c, 0x75, 0x10, 0x2c, 0xe5, 0x34, 0xf5, 0x4a, 0x19, 0xd8, 0x63, + 0x23, 0xfe, 0xd3, 0x19, 0x1a, 0x44, 0xe6, 0xe6, 0x81, 0x1d, 0xe2, 0xe5, 0xa8, 0xa4, 0xa1, 0xbc, 0x61, 0xa0, 0x20, + 0xa8, 0xa9, 0x60, 0x11, 0xa4, 0xa8, 0x31, 0xed, 0x51, 0x31, 0xc8, 0xdc, 0xea, 0xb8, 0x81, 0x2e, 0x5f, 0x25, 0xb1, + 0x4b, 0xb5, 0xdb, 0x20, 0x57, 0x15, 0x3f, 0x06, 0xcf, 0x44, 0x5a, 0x07, 0xe9, 0x05, 0x8a, 0xa0, 0x2b, 0x8a, 0x48, + 0xaf, 0xca, 0x78, 0x11, 0xd6, 0xa2, 0xdc, 0x6a, 0xf4, 0xa0, 0x61, 0x18, 0x49, 0x85, 0xb7, 0x8d, 0x28, 0xc5, 0x7e, + 0x66, 0x5f, 0x61, 0x14, 0x3e, 0xe8, 0x50, 0x46, 0x9e, 0x2c, 0xda, 0xba, 0xf7, 0x6e, 0xd2, 0x88, 0x45, 0xa2, 0xce, + 0x6b, 0x1e, 0x99, 0xd2, 0x41, 0x93, 0x7c, 0x74, 0x5e, 0xce, 0xbc, 0x61, 0x32, 0xb2, 0x53, 0x72, 0x5c, 0x6a, 0x05, + 0x18, 0xb1, 0xf9, 0xdb, 0x6f, 0x1d, 0xc7, 0x33, 0x9f, 0x8e, 0x7e, 0x24, 0x3c, 0x5f, 0x66, 0x9e, 0x79, 0xba, 0x2d, + 0x0a, 0x97, 0x5c, 0x98, 0x53, 0xa1, 0x52, 0x83, 0x21, 0xf0, 0x57, 0x31, 0x78, 0x51, 0x26, 0xb8, 0x39, 0xb5, 0xeb, + 0x3e, 0xba, 0x8c, 0x88, 0x0e, 0xdf, 0x54, 0x68, 0xe6, 0xeb, 0xd7, 0xc9, 0x9d, 0x5c, 0x28, 0xa7, 0xd7, 0xaa, 0xc0, + 0xcb, 0x52, 0x65, 0x50, 0x8c, 0x51, 0xa5, 0xf4, 0xbc, 0xa0, 0x51, 0x9d, 0xa8, 0x14, 0x1c, 0x9a, 0xb1, 0xc0, 0x7f, + 0x48, 0xec, 0x2e, 0x79, 0xe8, 0x54, 0x00, 0x64, 0xca, 0xa2, 0xa1, 0xa3, 0x02, 0xf9, 0xdd, 0xc7, 0xd6, 0x8c, 0xb9, + 0x6a, 0x75, 0x59, 0x83, 0x14, 0x45, 0xdb, 0x53, 0x82, 0x34, 0x74, 0x87, 0x8b, 0x6d, 0x8a, 0x10, 0x6f, 0x0e, 0xc5, + 0x20, 0xa0, 0x15, 0x1a, 0x5f, 0x62, 0xaa, 0x95, 0x16, 0xf5, 0x80, 0xc2, 0xcb, 0x56, 0xc1, 0xdf, 0x72, 0xc1, 0x7d, + 0x81, 0x86, 0x43, 0x4c, 0x80, 0x00, 0x0c, 0x64, 0xb5, 0xfc, 0xfb, 0xa8, 0xa4, 0x98, 0xe9, 0x7b, 0xb5, 0xf9, 0x84, + 0xf7, 0xa5, 0x69, 0x72, 0x46, 0x30, 0x49, 0x71, 0x17, 0x32, 0x64, 0x11, 0xe1, 0xde, 0x2b, 0x3a, 0xa0, 0x6b, 0x2b, + 0x9a, 0x39, 0xf5, 0x88, 0x24, 0xb4, 0x05, 0x84, 0xd8, 0xe0, 0xc3, 0x6c, 0x59, 0x0e, 0x8d, 0x60, 0xd6, 0xc0, 0x8c, + 0xf9, 0x5e, 0xcb, 0x08, 0xa2, 0x92, 0x55, 0x2f, 0xbf, 0x07, 0x0e, 0xb4, 0xec, 0x4d, 0x60, 0xd1, 0x49, 0xda, 0x54, + 0x18, 0x08, 0x33, 0xf7, 0xe3, 0x07, 0xcf, 0x55, 0x32, 0x34, 0x7d, 0xac, 0x49, 0x0b, 0x8f, 0x86, 0x1b, 0x07, 0x5c, + 0xf9, 0xf8, 0x5c, 0xa2, 0x90, 0x37, 0xca, 0xb0, 0x2b, 0x77, 0x0e, 0xa8, 0x8f, 0x4c, 0x8d, 0x32, 0x04, 0x39, 0x01, + 0x19, 0xf0, 0xa0, 0xe3, 0x20, 0xf9, 0x3f, 0x20, 0x19, 0x19, 0x9c, 0xc0, 0xbd, 0x32, 0x23, 0x94, 0x2d, 0x28, 0xfc, + 0x91, 0x65, 0xdb, 0x07, 0xb4, 0xe7, 0x33, 0x9a, 0x14, 0x07, 0x92, 0x8d, 0x12, 0x3c, 0x8f, 0x7e, 0xa1, 0x84, 0x26, + 0x68, 0x93, 0x67, 0xe8, 0x23, 0xd9, 0x18, 0x29, 0x44, 0x26, 0x02, 0x07, 0x95, 0x03, 0xb1, 0x75, 0xc1, 0x40, 0x3e, + 0xb3, 0xe3, 0xce, 0xb8, 0xfd, 0x51, 0x70, 0x9d, 0x08, 0xdb, 0x1c, 0x7e, 0xa8, 0xd5, 0x61, 0xec, 0xa7, 0x81, 0xeb, + 0x16, 0xac, 0x6e, 0x95, 0x9e, 0xa1, 0xab, 0x8e, 0xf8, 0x4d, 0x4e, 0x8d, 0x98, 0xb6, 0xe9, 0xae, 0x6e, 0xb7, 0x9b, + 0xea, 0x55, 0xb6, 0xa0, 0x2e, 0x63, 0xf7, 0x5a, 0x55, 0x6b, 0xc6, 0xf2, 0xb0, 0xd0, 0xca, 0xec, 0xf3, 0x9f, 0xc5, + 0xd0, 0x99, 0x68, 0x3a, 0x34, 0x02, 0x25, 0x57, 0x51, 0xc4, 0xd3, 0x87, 0xd5, 0x35, 0xd7, 0x36, 0x99, 0xf8, 0x2b, + 0xa7, 0x8f, 0xaf, 0x1d, 0x37, 0xdf, 0x11, 0x46, 0xbd, 0xe7, 0x8e, 0x1b, 0x70, 0xae, 0x46, 0xbc, 0x1c, 0x3d, 0xf3, + 0x94, 0x57, 0xcb, 0xbb, 0xd2, 0x1c, 0x05, 0xcf, 0xb5, 0x9f, 0x5b, 0x4a, 0x3d, 0x2d, 0x4b, 0x1e, 0xb3, 0x0f, 0xb6, + 0x91, 0xdb, 0x30, 0xd6, 0x9b, 0x74, 0x43, 0xc6, 0x3b, 0x0e, 0xf8, 0x64, 0xa5, 0xa8, 0x2b, 0xfd, 0x9e, 0xaa, 0x49, + 0x0a, 0x1b, 0xcd, 0x6c, 0x37, 0xd4, 0x78, 0x17, 0x30, 0x4d, 0x87, 0xb7, 0x02, 0xc9, 0x81, 0x07, 0xe5, 0xda, 0x12, + 0xa6, 0x78, 0xdc, 0x9c, 0x0a, 0x48, 0x32, 0xac, 0xa6, 0x21, 0x37, 0xbf, 0x2a, 0xa4, 0x21, 0xa1, 0xce, 0xd5, 0x01, + 0x68, 0x95, 0x92, 0x07, 0x38, 0x94, 0x43, 0x01, 0xe6, 0xca, 0xa1, 0x67, 0x68, 0x50, 0x08, 0x46, 0xe8, 0xcd, 0xdb, + 0xe8, 0xf0, 0xd4, 0xe1, 0x43, 0x69, 0x5c, 0xe6, 0x14, 0xc4, 0x2f, 0x1f, 0xfb, 0x48, 0x3d, 0x1a, 0xeb, 0x4e, 0x3e, + 0x51, 0x87, 0xe7, 0x4b, 0xc8, 0xa5, 0x09, 0xdd, 0x27, 0x9c, 0x54, 0x33, 0x21, 0x0b, 0xf9, 0x37, 0x79, 0xaa, 0x46, + 0xb1, 0xa0, 0xf6, 0xea, 0xb9, 0x91, 0xec, 0x8e, 0x3e, 0xcb, 0x51, 0xf8, 0x6a, 0x1c, 0x6e, 0xb5, 0xc2, 0xae, 0x07, + 0x21, 0x2f, 0xbe, 0x70, 0x73, 0xbf, 0xf9, 0x9a, 0x53, 0xd0, 0xfd, 0xa9, 0x03, 0xcf, 0x6d, 0xf1, 0x8a, 0x66, 0x77, + 0x54, 0x07, 0x16, 0xed, 0xfd, 0xfb, 0xa0, 0x1c, 0xb7, 0xf5, 0x59, 0x07, 0xee, 0xfe, 0x91, 0xd8, 0x8d, 0x81, 0xbc, + 0x41, 0x19, 0xef, 0x67, 0x3f, 0xa5, 0x0f, 0x45, 0x42, 0x36, 0xac, 0x31, 0x40, 0x8e, 0x5c, 0x98, 0xf5, 0xb8, 0x31, + 0x67, 0xa7, 0x5d, 0x1e, 0x4a, 0xd0, 0xdd, 0xd6, 0xfe, 0xe3, 0x7a, 0x84, 0xb3, 0xb8, 0x15, 0x60, 0xf2, 0x77, 0x6e, + 0x2c, 0xbb, 0xaa, 0xdb, 0x0b, 0x87, 0x9e, 0x1e, 0xa9, 0xe0, 0xbd, 0xd1, 0x9c, 0x64, 0x5a, 0xb5, 0xbd, 0xda, 0x9f, + 0xfd, 0x92, 0x7f, 0xab, 0x74, 0xbf, 0x6d, 0x09, 0x39, 0x72, 0xb1, 0x82, 0x5d, 0x67, 0x92, 0xc2, 0xf6, 0xd7, 0x2d, + 0x77, 0xcc, 0x69, 0x70, 0xe2, 0x66, 0x4b, 0xe4, 0x3b, 0x7c, 0x1b, 0xc8, 0x26, 0x50, 0x94, 0xfd, 0x38, 0xc0, 0x3e, + 0x8c, 0xa9, 0xb4, 0x49, 0x46, 0x2b, 0x6f, 0xf4, 0xbe, 0x7d, 0x57, 0x28, 0x63, 0xcf, 0x8a, 0x05, 0xb9, 0x8a, 0x84, + 0x1c, 0xb0, 0x1e, 0xcb, 0x54, 0x42, 0x87, 0xc6, 0x73, 0x17, 0xd1, 0x97, 0x45, 0x74, 0xef, 0xe5, 0xbe, 0x4f, 0x62, + 0x9b, 0xd6, 0xdb, 0x29, 0x8f, 0xe4, 0x7f, 0xc4, 0xf8, 0x43, 0x56, 0x05, 0x79, 0x00, 0x1e, 0xef, 0xaf, 0x36, 0x74, + 0x9d, 0xa7, 0x41, 0x99, 0x71, 0x10, 0x45, 0x40, 0xe9, 0x72, 0x53, 0xe4, 0x9c, 0x6e, 0x68, 0x94, 0x4a, 0xd9, 0x16, + 0x83, 0xc0, 0xc8, 0x56, 0x35, 0xea, 0xc5, 0xfc, 0x10, 0x9a, 0x26, 0xa3, 0x3f, 0x9e, 0x49, 0x59, 0x0d, 0xe5, 0xdc, + 0xc5, 0x3a, 0x39, 0xb6, 0x8c, 0x7d, 0x0d, 0xd1, 0x07, 0x87, 0xad, 0xfa, 0x11, 0x33, 0x0f, 0x79, 0xf7, 0x50, 0x80, + 0x81, 0xf9, 0xae, 0x27, 0xdf, 0x94, 0xd2, 0xad, 0xca, 0x52, 0x69, 0x04, 0xa1, 0x0a, 0x6c, 0xb2, 0x37, 0x3c, 0x1a, + 0xe8, 0x89, 0x92, 0x8b, 0x91, 0xc1, 0x14, 0x48, 0x00, 0xd5, 0xb4, 0x0f, 0x7f, 0x4d, 0x2d, 0x94, 0x8c, 0xf4, 0x52, + 0x60, 0x0e, 0xe9, 0xbf, 0x21, 0x21, 0x60, 0x32, 0x00, 0xab, 0x2f, 0xfc, 0x66, 0x12, 0xff, 0x98, 0x0f, 0x7c, 0x04, + 0x9f, 0x30, 0x51, 0x23, 0x52, 0xfe, 0x41, 0x79, 0x9f, 0x8e, 0x9c, 0x29, 0x59, 0x3b, 0x2b, 0x05, 0x0e, 0x15, 0x57, + 0x53, 0x18, 0xc2, 0xd3, 0x83, 0xb0, 0x88, 0xa1, 0x1b, 0xc8, 0x7a, 0xb0, 0xe3, 0x09, 0xd3, 0x88, 0xda, 0x64, 0xaa, + 0x86, 0x92, 0xf6, 0x47, 0xc1, 0xe2, 0xc0, 0x9a, 0x00, 0xe4, 0x58, 0x68, 0x5a, 0x74, 0x19, 0x91, 0x79, 0xb0, 0x14, + 0x8e, 0xc0, 0xa9, 0x09, 0xb9, 0x9e, 0x55, 0xe6, 0x3d, 0x4f, 0x0a, 0x0e, 0xe2, 0x09, 0xf6, 0xce, 0x18, 0xf1, 0x4e, + 0x9e, 0x5d, 0xed, 0x4f, 0xb9, 0xde, 0x05, 0x2f, 0xb9, 0x8c, 0x20, 0x97, 0x39, 0x7e, 0x31, 0x98, 0x86, 0xfb, 0x07, + 0x30, 0x17, 0x19, 0x82, 0x7c, 0xe8, 0x50, 0x82, 0x3b, 0x2c, 0x46, 0x9b, 0xd5, 0xc0, 0xc3, 0x8d, 0x22, 0x4b, 0x26, + 0x83, 0x80, 0x08, 0x4c, 0xab, 0x7c, 0x47, 0x05, 0x70, 0x15, 0x17, 0xda, 0x98, 0xa2, 0xb8, 0x5e, 0x51, 0xed, 0x38, + 0xa3, 0xbd, 0x64, 0x33, 0xf3, 0x71, 0x9a, 0x96, 0x36, 0xd4, 0x6a, 0xe2, 0xd4, 0x91, 0x14, 0xcd, 0xd0, 0x79, 0x73, + 0x91, 0x8a, 0x64, 0xa6, 0x0f, 0xe6, 0x0f, 0x1d, 0x09, 0x6c, 0x94, 0x56, 0x30, 0xc8, 0xf9, 0x1a, 0x3b, 0x73, 0x97, + 0xb6, 0xbe, 0xce, 0xda, 0x30, 0xe7, 0xd3, 0x55, 0x3f, 0x4d, 0x09, 0x54, 0x3b, 0x4d, 0xfd, 0xd9, 0x8a, 0xd8, 0x2f, + 0xd2, 0x2e, 0xcb, 0x42, 0x93, 0xe5, 0xde, 0x8f, 0x1f, 0xee, 0xe3, 0x61, 0xa1, 0xba, 0x0b, 0x73, 0x29, 0x47, 0x38, + 0xb2, 0x58, 0x8b, 0xd5, 0x31, 0xfb, 0x19, 0x25, 0x1b, 0xcb, 0x7d, 0x0f, 0x4a, 0xb2, 0xe3, 0xe5, 0xa5, 0x34, 0x97, + 0x7a, 0xf1, 0x5d, 0x0c, 0x96, 0x03, 0xfc, 0x59, 0xa1, 0x9a, 0xe8, 0x5d, 0x59, 0xad, 0xf4, 0x9f, 0x75, 0xc9, 0x45, + 0x5d, 0x39, 0xe3, 0xda, 0x93, 0x21, 0x4c, 0x13, 0x9a, 0xef, 0x18, 0x62, 0x53, 0xc5, 0x44, 0x49, 0x34, 0xd2, 0x36, + 0x70, 0xbc, 0x7f, 0x5e, 0x9f, 0x45, 0x2a, 0x72, 0xd9, 0x2f, 0xd7, 0x71, 0xc7, 0x2f, 0x40, 0x15, 0xc0, 0x0d, 0x42, + 0x0f, 0x72, 0x02, 0xc3, 0xd8, 0x39, 0x3d, 0xd2, 0x88, 0xc2, 0x29, 0xe9, 0x4e, 0x59, 0x5a, 0x87, 0x37, 0x34, 0xde, + 0xa5, 0x07, 0x51, 0x1a, 0x15, 0xf1, 0x53, 0xd2, 0x1b, 0x9b, 0xd1, 0xa9, 0xae, 0xd1, 0x6f, 0x9a, 0x8b, 0x18, 0x1b, + 0x58, 0x50, 0xef, 0xff, 0x74, 0x00, 0x4a, 0x4c, 0xe6, 0x2d, 0x63, 0x8e, 0x89, 0x90, 0x32, 0xb7, 0x92, 0xef, 0x93, + 0x88, 0xca, 0x3c, 0x66, 0x38, 0xe3, 0x17, 0x19, 0x23, 0xea, 0x66, 0x71, 0x7c, 0x6a, 0xdd, 0x82, 0x49, 0x37, 0xf3, + 0xae, 0xcc, 0x40, 0x1a, 0x44, 0x9e, 0x6a, 0xe9, 0x29, 0xa8, 0x9e, 0x2e, 0xab, 0xae, 0x5e, 0x29, 0xf2, 0xf9, 0x1f, + 0x0c, 0xc3, 0xe1, 0x00, 0xe0, 0xc0, 0xea, 0x73, 0xae, 0xf6, 0xda, 0x9f, 0xad, 0x69, 0xeb, 0x80, 0xd3, 0x13, 0x92, + 0xa7, 0x3f, 0x04, 0xe8, 0x1a, 0xcc, 0x32, 0x54, 0xe7, 0x3c, 0x54, 0xfd, 0xed, 0xa2, 0x2d, 0x0e, 0xc7, 0x0c, 0x04, + 0xda, 0x9b, 0x7b, 0xdc, 0xe2, 0xf7, 0x2c, 0x91, 0xce, 0xc3, 0x04, 0x5b, 0x34, 0xea, 0xf6, 0x48, 0x4e, 0xec, 0x56, + 0x0f, 0x96, 0x3b, 0x0e, 0x07, 0x86, 0x9e, 0xed, 0x22, 0x2a, 0x0d, 0x12, 0xec, 0x7e, 0x2e, 0x51, 0x01, 0x91, 0x0e, + 0xba, 0xc3, 0xe4, 0xfb, 0x1e, 0x4b, 0x6b, 0xea, 0xcf, 0xdd, 0x68, 0xe0, 0x0a, 0x76, 0xb8, 0xc2, 0x4a, 0x5d, 0x6e, + 0xdc, 0x4d, 0x87, 0xb3, 0xce, 0xb1, 0x50, 0xb9, 0x1d, 0x1d, 0x7c, 0xbc, 0xde, 0x58, 0x7b, 0xe7, 0x88, 0x1c, 0xa0, + 0xb2, 0x71, 0x18, 0x70, 0xa9, 0x86, 0x9a, 0xa9, 0x0c, 0x81, 0xd6, 0x8d, 0x61, 0x96, 0xc3, 0x29, 0xfa, 0x3e, 0x75, + 0xec, 0x99, 0x62, 0x23, 0x95, 0x4b, 0xfb, 0xd0, 0x34, 0x35, 0x07, 0x9d, 0xbc, 0x3b, 0x05, 0x42, 0x7f, 0xc5, 0xe0, + 0xe1, 0x81, 0x6c, 0xff, 0xf1, 0xdc, 0xff, 0x7a, 0x73, 0x2e, 0xb6, 0x97, 0x39, 0x70, 0x08, 0x93, 0x7d, 0xd4, 0x12, + 0x0a, 0xa0, 0x48, 0xe6, 0xa6, 0x7a, 0x90, 0xab, 0x77, 0x03, 0xfa, 0x84, 0x8b, 0xb1, 0x49, 0xda, 0xa7, 0xc7, 0x1b, + 0x8c, 0x7c, 0x9e, 0x36, 0xbd, 0x76, 0x21, 0x55, 0xbe, 0xd8, 0x9b, 0xed, 0x17, 0x27, 0x9b, 0xe0, 0x24, 0x27, 0xca, + 0x8e, 0x6d, 0x16, 0xc3, 0x3b, 0xed, 0xd2, 0xbf, 0x6f, 0x5b, 0xb9, 0x81, 0x4b, 0x38, 0xb4, 0x43, 0x15, 0xcc, 0x7b, + 0x70, 0xe8, 0xf5, 0x83, 0x0d, 0x8e, 0xea, 0x41, 0x77, 0x60, 0xfa, 0xb4, 0xd6, 0x09, 0x06, 0x84, 0xef, 0x56, 0x30, + 0x0a, 0x01, 0xc7, 0x5b, 0xd7, 0x2e, 0x94, 0x7b, 0x3e, 0xe0, 0x07, 0x41, 0x85, 0x33, 0x43, 0x68, 0x7e, 0x10, 0x39, + 0xa5, 0x3d, 0xa5, 0xa4, 0xba, 0xba, 0x35, 0x0e, 0x37, 0xe0, 0xb3, 0x81, 0xe2, 0x68, 0xbb, 0xf1, 0x4e, 0x12, 0x87, + 0xf9, 0x28, 0x65, 0xe6, 0xd2, 0xfb, 0xce, 0x09, 0x30, 0xf1, 0x4e, 0xed, 0xf4, 0x09, 0x9e, 0xe8, 0x5b, 0x1f, 0x55, + 0xb5, 0x7d, 0xb1, 0xe7, 0x9b, 0xab, 0xee, 0x90, 0x43, 0x94, 0x10, 0x62, 0xf6, 0xa9, 0x73, 0xcc, 0x73, 0x3e, 0x4b, + 0x07, 0x13, 0xf6, 0xdc, 0x16, 0x40, 0xab, 0x46, 0x05, 0xba, 0x72, 0x40, 0x5e, 0xc2, 0x57, 0xb7, 0x4e, 0xe8, 0xd2, + 0x41, 0x7a, 0x2b, 0xbf, 0x5c, 0x35, 0x89, 0x40, 0xf7, 0xc2, 0x7b, 0x8f, 0xe6, 0x4e, 0x74, 0x9c, 0x89, 0x3b, 0xb8, + 0xe8, 0x27, 0xce, 0x69, 0x7c, 0x24, 0xee, 0x12, 0xf9, 0x2c, 0xa6, 0x01, 0x31, 0x4f, 0x84, 0xf8, 0xab, 0x9f, 0xb9, + 0x84, 0x8d, 0x0a, 0x66, 0xea, 0x6e, 0x91, 0xd3, 0xca, 0x16, 0x13, 0x28, 0xdc, 0x5f, 0x74, 0xc3, 0xad, 0x59, 0xbe, + 0x13, 0x0b, 0x30, 0x2d, 0x03, 0x5f, 0xda, 0x39, 0x20, 0x45, 0x44, 0x7a, 0x4f, 0xde, 0xce, 0xff, 0x9b, 0xda, 0x7b, + 0xc5, 0x7f, 0xb4, 0xb9, 0x44, 0x71, 0x3a, 0x6d, 0x0a, 0x4b, 0xe1, 0xdb, 0x3d, 0x02, 0x21, 0x32, 0x46, 0x04, 0x9a, + 0x31, 0x7f, 0xd0, 0x0e, 0x73, 0x0a, 0xbc, 0xc3, 0x01, 0x70, 0x14, 0xb6, 0xd4, 0x0f, 0x36, 0x78, 0x70, 0x8f, 0x77, + 0xbd, 0x94, 0x3a, 0x56, 0x0e, 0x08, 0xcb, 0x1d, 0x85, 0xe3, 0x20, 0x83, 0x40, 0xd5, 0x21, 0xf6, 0x0e, 0xca, 0x3a, + 0x1d, 0xdd, 0x3a, 0x0c, 0xa9, 0xd0, 0x3b, 0xdf, 0x2a, 0x32, 0x1f, 0xb3, 0x5a, 0xe3, 0xa0, 0xfa, 0x00, 0x4e, 0xc0, + 0x6a, 0x46, 0x1c, 0x3d, 0xcd, 0xcb, 0x3d, 0xcd, 0xbc, 0x80, 0x00, 0x67, 0xf8, 0x83, 0x1d, 0xce, 0xd9, 0x3b, 0xef, + 0x81, 0xd2, 0x0d, 0x80, 0xda, 0xc4, 0x69, 0x59, 0xb8, 0x15, 0xbf, 0x5a, 0x7d, 0x23, 0x79, 0x7b, 0x6e, 0x9f, 0x8e, + 0x78, 0x0f, 0x0f, 0x5e, 0x2a, 0x5a, 0xc8, 0x60, 0x65, 0x82, 0xad, 0x06, 0xc2, 0xca, 0x10, 0x0b, 0xfa, 0x68, 0x5e, + 0xab, 0xee, 0x6a, 0x84, 0xea, 0xff, 0xea, 0x29, 0x98, 0xb2, 0x05, 0xaf, 0x38, 0xa9, 0xe9, 0x86, 0x93, 0xb4, 0xd4, + 0x8a, 0xa3, 0xf9, 0xb1, 0x13, 0x06, 0x05, 0xb1, 0x1d, 0x22, 0xfe, 0xf4, 0x7f, 0x96, 0x28, 0x4b, 0xb8, 0xd5, 0x1c, + 0x51, 0xb6, 0xf4, 0x8e, 0x23, 0xe2, 0xdf, 0x8f, 0x78, 0x57, 0x07, 0x11, 0xaa, 0xc6, 0x7c, 0x52, 0x64, 0xfe, 0x2b, + 0xce, 0xf2, 0x46, 0xb8, 0xdb, 0xcc, 0xee, 0xeb, 0x9d, 0x2f, 0xe4, 0x22, 0x39, 0x3f, 0xcc, 0x2d, 0xdb, 0xce, 0xcb, + 0x4b, 0x3b, 0x55, 0xd2, 0xd6, 0xf3, 0xd3, 0xf2, 0x43, 0x8c, 0x23, 0x22, 0x2d, 0xcb, 0x30, 0xba, 0xf3, 0xec, 0x1c, + 0xbe, 0xff, 0x4e, 0xb9, 0xfa, 0xfe, 0xb3, 0x75, 0xc5, 0xb1, 0x35, 0x2e, 0xdf, 0xf1, 0x50, 0xea, 0xf8, 0xcc, 0x30, + 0xd4, 0xda, 0x40, 0x30, 0xb1, 0x95, 0x6d, 0x18, 0x03, 0x40, 0xef, 0x47, 0xb6, 0x18, 0xfa, 0x0b, 0xce, 0xa5, 0xd5, + 0xcd, 0x0b, 0xb9, 0x64, 0x7e, 0xe0, 0x1e, 0xe3, 0x03, 0xef, 0xfa, 0x83, 0xeb, 0x11, 0x3b, 0x91, 0x01, 0x0c, 0xa9, + 0x18, 0x5c, 0xe4, 0x3d, 0xe6, 0xf3, 0xae, 0x14, 0x21, 0xe4, 0x21, 0x4b, 0x01, 0xae, 0x5d, 0xfd, 0xb9, 0x2c, 0xcf, + 0xbc, 0x9f, 0xcf, 0xdb, 0x1c, 0x70, 0x58, 0xa8, 0xbe, 0x2c, 0x60, 0x1c, 0xfe, 0xa1, 0x18, 0x33, 0x81, 0x59, 0x1b, + 0x5e, 0x3f, 0xeb, 0x39, 0x47, 0x53, 0x33, 0x6a, 0x3b, 0xa5, 0x9e, 0xe5, 0xcb, 0xaa, 0xcd, 0x16, 0xcb, 0x90, 0x1b, + 0x1a, 0x1f, 0x27, 0x0d, 0x12, 0xe3, 0xaf, 0x09, 0xb4, 0x5c, 0x24, 0x6d, 0x44, 0x62, 0xd5, 0x8a, 0x56, 0x14, 0x2b, + 0xa3, 0x58, 0x88, 0x95, 0xfc, 0x56, 0xc9, 0xd3, 0x88, 0x39, 0xe5, 0xf1, 0xac, 0xdf, 0x95, 0xa3, 0x11, 0x50, 0xe1, + 0x60, 0x95, 0x4f, 0x7b, 0x6c, 0xed, 0x77, 0xf6, 0x3b, 0x28, 0xa5, 0xc4, 0x4e, 0x20, 0xd8, 0x27, 0x53, 0x1c, 0x00, + 0x2b, 0x9b, 0x23, 0xfb, 0x1b, 0x4e, 0xbf, 0x7a, 0xeb, 0x08, 0x68, 0x5d, 0x76, 0x4e, 0x75, 0xb4, 0x43, 0xef, 0x0b, + 0x32, 0x8d, 0x63, 0x41, 0x7e, 0x5a, 0x89, 0xcd, 0xe0, 0x31, 0x3a, 0x08, 0xd3, 0x2f, 0xac, 0x7d, 0xd5, 0xc8, 0x36, + 0x58, 0x62, 0xb6, 0xec, 0x58, 0xec, 0x5a, 0x27, 0x56, 0xce, 0xa8, 0x77, 0xef, 0xf9, 0x02, 0x9f, 0x54, 0x5b, 0xc9, + 0x0a, 0x38, 0x33, 0x81, 0x75, 0x14, 0x80, 0x6b, 0xec, 0x43, 0xea, 0x03, 0x8a, 0x83, 0xfa, 0x8a, 0xe3, 0xb3, 0x04, + 0x8b, 0x12, 0x42, 0x60, 0x9f, 0x74, 0xeb, 0x86, 0x4a, 0x38, 0x39, 0x4b, 0xda, 0x8f, 0xf0, 0x14, 0xc6, 0x0d, 0x2a, + 0x05, 0x9b, 0x8b, 0xf1, 0x45, 0xc5, 0x32, 0x85, 0xb3, 0x18, 0xd3, 0x61, 0xff, 0xb4, 0x4a, 0x58, 0x46, 0x1f, 0x1f, + 0x16, 0x16, 0x6e, 0xa6, 0x10, 0x4b, 0x4a, 0xfa, 0xfe, 0x80, 0xcd, 0xd7, 0x52, 0xff, 0xbf, 0xe6, 0x0a, 0x2a, 0xd8, + 0x8a, 0xb9, 0xe3, 0xf0, 0x77, 0xa8, 0xe0, 0xed, 0x2d, 0xf3, 0xa4, 0x6a, 0x6b, 0xeb, 0xbe, 0xa4, 0x16, 0x08, 0x2f, + 0x79, 0xfa, 0x89, 0xc3, 0x1d, 0xde, 0x8d, 0x33, 0x26, 0xc3, 0xab, 0x7b, 0x80, 0x24, 0x21, 0x20, 0xa1, 0x75, 0x5f, + 0x77, 0x0f, 0x06, 0x77, 0xb4, 0xc6, 0xf7, 0x20, 0xf1, 0x9e, 0x6f, 0xc6, 0xdb, 0x84, 0x5f, 0xa6, 0x7f, 0x69, 0x55, + 0xc7, 0x6a, 0xec, 0x83, 0x2a, 0xc6, 0xf5, 0x0f, 0xcf, 0xfd, 0xe9, 0x01, 0xb3, 0xcf, 0xfd, 0xcc, 0x26, 0x9a, 0x44, + 0x9f, 0xde, 0x5f, 0x4a, 0x5c, 0x78, 0xab, 0xf1, 0x96, 0xbf, 0xb4, 0xe2, 0xc2, 0xf9, 0x4a, 0xb7, 0xdb, 0x85, 0xa3, + 0xc3, 0x46, 0xe2, 0x69, 0xa3, 0x26, 0x80, 0x4c, 0xdf, 0x8a, 0xa9, 0xa4, 0x0b, 0x2b, 0xc8, 0xb4, 0x4f, 0xd2, 0x8d, + 0xc6, 0x53, 0x90, 0x4a, 0xb3, 0x58, 0x3d, 0x99, 0x19, 0xda, 0x68, 0x3d, 0x1c, 0x67, 0xd0, 0xff, 0x92, 0x18, 0xca, + 0x7a, 0xd9, 0xb6, 0x30, 0x5b, 0xa6, 0xba, 0xae, 0x3f, 0x6e, 0xa4, 0x95, 0xcc, 0xaa, 0x57, 0xd0, 0xf1, 0xf5, 0xfe, + 0x6d, 0x05, 0x4f, 0x24, 0x8a, 0x0f, 0x7b, 0xb7, 0x90, 0x68, 0x2d, 0x51, 0x2c, 0xe2, 0xfd, 0x32, 0x1d, 0xc7, 0x00, + 0xfc, 0xc1, 0xd0, 0x2d, 0x1d, 0xa7, 0xe9, 0xb1, 0xb2, 0x77, 0x68, 0x1f, 0x4a, 0x8a, 0x62, 0xb9, 0x48, 0xf8, 0x98, + 0x2d, 0xe0, 0xb8, 0x58, 0xa8, 0x86, 0xf6, 0x08, 0x16, 0x6d, 0x89, 0x2d, 0x7a, 0x4a, 0x8f, 0xa3, 0x1c, 0x23, 0xa6, + 0x51, 0xca, 0x97, 0xd1, 0xe3, 0x69, 0x4a, 0x00, 0x6d, 0x36, 0x64, 0x37, 0xef, 0x49, 0x12, 0xd6, 0xe4, 0x36, 0xb9, + 0x00, 0xb6, 0x7f, 0xe6, 0x54, 0xca, 0x5d, 0x1c, 0x44, 0x29, 0xed, 0xdc, 0xfc, 0x6e, 0x0e, 0xbc, 0x96, 0xeb, 0x22, + 0x31, 0xc6, 0xc8, 0x93, 0x2b, 0xa1, 0xa8, 0x65, 0xda, 0xf2, 0xae, 0x39, 0x12, 0xfc, 0xc2, 0x6b, 0xed, 0xad, 0x04, + 0x32, 0xd6, 0x65, 0xf8, 0x66, 0x74, 0x13, 0xb4, 0xf5, 0x9f, 0xec, 0x4d, 0xd7, 0x1b, 0xc4, 0xf2, 0xf3, 0xab, 0xba, + 0xea, 0xc8, 0xb3, 0xff, 0x50, 0xfb, 0x4e, 0x08, 0x2a, 0xe7, 0x26, 0x0c, 0xeb, 0x49, 0x7c, 0x2e, 0x3a, 0xde, 0x9d, + 0x48, 0x92, 0x16, 0xba, 0x3e, 0x93, 0x8e, 0x06, 0x0d, 0x93, 0x54, 0x50, 0x2e, 0x96, 0x81, 0xdf, 0x66, 0x29, 0xd1, + 0x8c, 0x88, 0x74, 0xaa, 0x56, 0x9f, 0x4c, 0x5f, 0xd4, 0x40, 0x5c, 0xaf, 0x02, 0x2b, 0x89, 0xfa, 0x4a, 0xff, 0x6d, + 0x0e, 0x35, 0x95, 0x1c, 0x3c, 0xf6, 0x7b, 0x03, 0xa3, 0x68, 0x52, 0x3d, 0xa9, 0xbb, 0x54, 0x38, 0xed, 0x04, 0x95, + 0x72, 0xe5, 0x29, 0x35, 0xe0, 0xa9, 0x5d, 0x1c, 0xf9, 0x99, 0x1f, 0x4c, 0x77, 0xc9, 0x9f, 0xb8, 0x17, 0xbf, 0xb0, + 0x0d, 0xa9, 0xfa, 0xcb, 0x1f, 0x6a, 0x03, 0xb2, 0x39, 0x09, 0xf5, 0xde, 0x8f, 0x43, 0x46, 0x35, 0xf7, 0x5b, 0xc7, + 0xe6, 0xf2, 0xa7, 0xdf, 0xde, 0xbd, 0xb9, 0xf0, 0x1b, 0xb4, 0x06, 0x65, 0xdd, 0xed, 0xaf, 0x6b, 0x78, 0x4a, 0xc5, + 0xbb, 0x2d, 0xc3, 0xcd, 0x92, 0xd1, 0x03, 0x90, 0x0f, 0xea, 0x9d, 0xff, 0xcc, 0xb5, 0x35, 0x4f, 0x75, 0x43, 0x73, + 0xb5, 0x08, 0x95, 0x33, 0x7f, 0x63, 0x18, 0xa9, 0x55, 0x4f, 0xf7, 0x64, 0x79, 0x63, 0x46, 0x03, 0xf7, 0x80, 0xa1, + 0x32, 0xc0, 0x8d, 0x96, 0x2e, 0x86, 0xd2, 0x5b, 0xd1, 0x97, 0xb6, 0xc5, 0xa6, 0x74, 0x5f, 0x6c, 0x4b, 0xeb, 0x62, + 0xb7, 0x71, 0x05, 0xdb, 0x92, 0xfe, 0x71, 0xfd, 0x1b, 0x7a, 0xa6, 0xd0, 0x63, 0xec, 0x2c, 0x3e, 0xe3, 0x65, 0xac, + 0xf5, 0x8d, 0x14, 0x59, 0xc1, 0x5b, 0xe3, 0x22, 0xd6, 0xc6, 0x0f, 0x25, 0x7b, 0x85, 0x71, 0x5f, 0x16, 0x58, 0x92, + 0x35, 0x1d, 0x0c, 0x8c, 0x54, 0x95, 0x56, 0xd2, 0x6d, 0x69, 0xf6, 0xdf, 0x2d, 0x5d, 0xc5, 0xc6, 0x42, 0xf3, 0x4b, + 0xaf, 0x74, 0x7a, 0x43, 0x62, 0x4d, 0xf6, 0xf3, 0x83, 0x0e, 0xc0, 0x8a, 0xd6, 0x45, 0x55, 0x91, 0x61, 0xbe, 0x56, + 0x91, 0x66, 0xd8, 0x13, 0x5c, 0x09, 0xd0, 0x40, 0xf5, 0x99, 0xa3, 0xf6, 0x21, 0x8e, 0x24, 0x56, 0xa3, 0x53, 0x0d, + 0xd9, 0x17, 0x45, 0x6c, 0x55, 0xbb, 0xa5, 0x46, 0xa9, 0x1a, 0x5d, 0xa2, 0x82, 0xca, 0x6f, 0x47, 0x8f, 0x88, 0x68, + 0x39, 0x6b, 0xf4, 0x21, 0x3e, 0x1f, 0x4d, 0xaf, 0x2b, 0x4e, 0x69, 0x80, 0x34, 0x48, 0x21, 0xb1, 0xa8, 0x74, 0xc1, + 0xcf, 0x04, 0xa4, 0xe5, 0x05, 0xfa, 0xd1, 0x55, 0x0c, 0x93, 0x33, 0x3c, 0x30, 0xf9, 0xad, 0xa3, 0x44, 0x7e, 0xb2, + 0xda, 0xe1, 0x37, 0xfc, 0xa5, 0xc5, 0x75, 0xa1, 0xf1, 0x96, 0x2b, 0xbf, 0x54, 0xcd, 0x55, 0x4c, 0xa1, 0x4b, 0x9f, + 0xc9, 0x6a, 0x86, 0x0c, 0xa6, 0xae, 0x62, 0x28, 0x01, 0x81, 0x6f, 0x40, 0x4a, 0x95, 0x41, 0x87, 0x10, 0x42, 0x6f, + 0xd0, 0x2a, 0x44, 0x74, 0x19, 0x36, 0x9f, 0x90, 0xaa, 0xb9, 0xce, 0x65, 0xf5, 0x68, 0xfe, 0x20, 0x26, 0xc1, 0x34, + 0xfc, 0x41, 0x23, 0x69, 0xb4, 0x07, 0x89, 0xc3, 0xa4, 0xd7, 0x21, 0xf4, 0x83, 0x37, 0xbd, 0x23, 0x0c, 0xdf, 0xfa, + 0xa4, 0xd3, 0xe3, 0x56, 0x92, 0xcb, 0xbf, 0x86, 0x95, 0x67, 0xa6, 0xd7, 0x26, 0xfc, 0x11, 0xfe, 0xa9, 0x0f, 0x66, + 0x75, 0x7b, 0x7b, 0x34, 0xc3, 0x6e, 0x68, 0x0c, 0x7f, 0x77, 0xc0, 0x5b, 0xf8, 0xc1, 0xf4, 0x35, 0x27, 0x76, 0x47, + 0xaf, 0x59, 0xb8, 0xce, 0xe7, 0xd1, 0xf8, 0x59, 0x9a, 0x17, 0xac, 0x3c, 0x79, 0x70, 0xdf, 0xfa, 0xde, 0x67, 0x12, + 0x19, 0x2f, 0x3f, 0x75, 0x79, 0xad, 0x2d, 0x27, 0x83, 0xf2, 0xc2, 0x52, 0xf7, 0xc3, 0x1e, 0xa7, 0x2f, 0xb5, 0xf6, + 0x97, 0x7a, 0xed, 0xb3, 0xcf, 0xa6, 0x26, 0x8f, 0x31, 0x3c, 0x1d, 0x4d, 0xdd, 0xd3, 0xc2, 0xfa, 0x16, 0x59, 0x21, + 0x36, 0xc7, 0xb7, 0xcb, 0xd1, 0xd3, 0x59, 0x6d, 0x2f, 0xae, 0xd0, 0xb4, 0x93, 0xd3, 0xa9, 0x13, 0x37, 0xaf, 0xa3, + 0x58, 0xd2, 0xb4, 0x8f, 0xef, 0xc7, 0x72, 0x87, 0xeb, 0x8a, 0x7a, 0x40, 0xd0, 0xa8, 0xa0, 0x17, 0x4c, 0xf5, 0xf8, + 0x74, 0x23, 0x40, 0x5d, 0x78, 0x3a, 0xb1, 0x4e, 0x53, 0xfd, 0x3d, 0xe0, 0x65, 0x60, 0x9a, 0x06, 0x5b, 0x3f, 0x54, + 0x92, 0x20, 0x97, 0x19, 0x6f, 0xfb, 0xf6, 0xfc, 0xf5, 0x3e, 0x5e, 0x58, 0x6a, 0x05, 0xf3, 0x5b, 0x7c, 0x0e, 0x52, + 0xb3, 0x80, 0x3b, 0x2a, 0x59, 0x84, 0x23, 0x88, 0x96, 0x77, 0xc8, 0x53, 0xc7, 0x01, 0xe9, 0xa0, 0x3a, 0x67, 0x24, + 0xe6, 0xf3, 0x5f, 0xed, 0x7b, 0x26, 0xf5, 0x7d, 0x0f, 0x5b, 0xaf, 0xde, 0x1d, 0x48, 0x39, 0xf4, 0x49, 0xf5, 0x19, + 0x68, 0x32, 0xf7, 0xdd, 0x56, 0x3a, 0x7d, 0xa3, 0xcf, 0xd6, 0xb5, 0xbb, 0x50, 0xf3, 0xd3, 0x54, 0xfa, 0xc4, 0x5e, + 0x39, 0x1b, 0xf5, 0x19, 0x94, 0x25, 0x73, 0x01, 0x04, 0x49, 0x8b, 0x40, 0x07, 0x3a, 0x71, 0xb6, 0x29, 0xd3, 0x40, + 0x74, 0x49, 0x6b, 0xce, 0xf8, 0x61, 0x9e, 0x9d, 0xe4, 0xd6, 0x7e, 0xcf, 0x49, 0x5c, 0x85, 0x73, 0xa8, 0xa0, 0xa0, + 0x79, 0x3c, 0xd1, 0x36, 0xf8, 0xaf, 0x17, 0xba, 0xc9, 0x89, 0x7c, 0x1e, 0xe6, 0x9c, 0xb6, 0x8c, 0x31, 0x42, 0x03, + 0x70, 0xd1, 0xf4, 0xea, 0x28, 0x60, 0xb9, 0x0b, 0x84, 0xdf, 0xf2, 0x79, 0xb7, 0xdd, 0xb6, 0xaa, 0x05, 0xa9, 0x76, + 0x62, 0x17, 0xd5, 0xcc, 0x32, 0x45, 0x06, 0xce, 0x00, 0x4f, 0xb6, 0x6f, 0x0b, 0xd9, 0xf8, 0xa0, 0xbd, 0xe9, 0xd2, + 0xe9, 0x51, 0x16, 0xf0, 0x83, 0x94, 0x93, 0x16, 0x9e, 0x1d, 0x43, 0xb1, 0x4d, 0x79, 0xb9, 0x2f, 0xf8, 0xd4, 0x35, + 0x86, 0xd4, 0x50, 0xda, 0x6c, 0x19, 0x29, 0xbc, 0x9b, 0x37, 0x06, 0x5c, 0xd2, 0xe2, 0xbd, 0x88, 0x31, 0xe0, 0xe1, + 0xfa, 0xa2, 0x45, 0x88, 0x27, 0x08, 0x73, 0xb8, 0x61, 0x86, 0x01, 0x74, 0x22, 0xe0, 0x60, 0x3a, 0xbd, 0xbd, 0x0e, + 0x7e, 0x4f, 0x56, 0x68, 0x4b, 0xaf, 0xe6, 0x0d, 0xb7, 0xab, 0x51, 0xba, 0xa1, 0x6d, 0x06, 0xb3, 0x7e, 0x3e, 0xf9, + 0x0d, 0xe5, 0xaa, 0xb3, 0x9a, 0xbf, 0x5c, 0xb0, 0x68, 0x75, 0x36, 0x73, 0x27, 0x9d, 0x1a, 0xdd, 0x53, 0xd5, 0x7a, + 0xea, 0x41, 0xb3, 0x37, 0xf4, 0x16, 0xd4, 0x14, 0x9a, 0x25, 0xc6, 0x1a, 0x3b, 0x1f, 0xfe, 0x47, 0xb6, 0xf0, 0x35, + 0x6b, 0x0f, 0xb4, 0xb6, 0x72, 0x7f, 0x6d, 0xc7, 0xd7, 0x08, 0x0e, 0xc3, 0x28, 0xc4, 0x09, 0xea, 0xd6, 0x5a, 0x52, + 0xe8, 0x56, 0xa7, 0x43, 0x54, 0x10, 0x93, 0xff, 0xa5, 0x37, 0xf3, 0x2e, 0x3e, 0x75, 0x0c, 0x9d, 0xab, 0x7f, 0x55, + 0x5c, 0x1d, 0x9b, 0xa6, 0xd9, 0xea, 0x5d, 0x3f, 0x17, 0x3e, 0xcc, 0xb4, 0xdf, 0x15, 0x2f, 0x3a, 0x42, 0x81, 0xc7, + 0x0f, 0x1e, 0xf6, 0xf5, 0x95, 0x15, 0xa4, 0x53, 0xcf, 0x27, 0xcc, 0x47, 0x4f, 0xd1, 0x31, 0x70, 0x43, 0x16, 0x13, + 0xef, 0xe3, 0x3a, 0x8b, 0xff, 0x59, 0xf6, 0xe1, 0x4c, 0xdb, 0x69, 0x54, 0x57, 0x8a, 0xc7, 0xb5, 0x08, 0xe8, 0xf3, + 0xe9, 0xe3, 0x12, 0x03, 0xd4, 0x5e, 0xac, 0x8a, 0x63, 0xb3, 0x41, 0x37, 0xbc, 0x2f, 0x84, 0xac, 0x57, 0x3a, 0xe3, + 0x3e, 0x2d, 0x12, 0x40, 0x5c, 0x7f, 0x44, 0x5d, 0x8b, 0xf9, 0xfa, 0xf2, 0xcd, 0xd1, 0xa6, 0xc7, 0x8c, 0x86, 0xc0, + 0x84, 0x59, 0xfb, 0x93, 0x51, 0x4a, 0xa7, 0x4f, 0xd1, 0x8a, 0xcf, 0x4d, 0xe1, 0x99, 0x6b, 0x75, 0x6d, 0x14, 0xe9, + 0x3f, 0x8a, 0xba, 0xf7, 0x31, 0x9c, 0x35, 0xaf, 0xbf, 0x60, 0x37, 0x07, 0xa3, 0x1f, 0x06, 0xcd, 0x41, 0x89, 0x45, + 0xbc, 0x7a, 0x12, 0x1f, 0x73, 0xbc, 0x26, 0x01, 0x3e, 0xe7, 0x39, 0x40, 0xff, 0x1c, 0x53, 0xcc, 0x25, 0x8c, 0xe3, + 0x63, 0x07, 0x54, 0x5b, 0x5b, 0x39, 0x24, 0xff, 0x66, 0xf6, 0x02, 0xb5, 0x59, 0xd7, 0x32, 0xa8, 0xbf, 0x83, 0xbc, + 0xda, 0xf4, 0xc2, 0xca, 0x41, 0xe7, 0x2b, 0x4b, 0xfa, 0xda, 0x04, 0xdd, 0xe2, 0xb2, 0xec, 0x80, 0xcf, 0xbc, 0xde, + 0x5d, 0x11, 0xbf, 0x14, 0xcc, 0x5b, 0xf8, 0x72, 0x1b, 0x9a, 0x70, 0x77, 0xe9, 0xa7, 0xc1, 0x09, 0xcd, 0x91, 0xdf, + 0x26, 0xa3, 0x0f, 0xdf, 0x7a, 0x76, 0xf5, 0xb2, 0x0e, 0xfc, 0x7f, 0xc3, 0x20, 0x10, 0x79, 0xa7, 0xd0, 0x2d, 0x69, + 0x9d, 0x7a, 0x14, 0x4b, 0x57, 0xca, 0x3e, 0xae, 0x5c, 0x7d, 0x74, 0x9b, 0xff, 0x1f, 0xae, 0xe0, 0x5b, 0xa3, 0xf8, + 0x49, 0x0c, 0xd0, 0x81, 0x22, 0x24, 0x3d, 0x22, 0xba, 0x78, 0xd6, 0xe2, 0xf1, 0x5b, 0x50, 0x33, 0xd8, 0xfa, 0x16, + 0xec, 0x04, 0x83, 0x90, 0x3d, 0x62, 0x9d, 0x0d, 0x1d, 0xb8, 0xfc, 0xad, 0x17, 0x65, 0x0e, 0x91, 0xde, 0x7c, 0x57, + 0x38, 0x75, 0x6d, 0xe5, 0x7d, 0xff, 0x97, 0xfa, 0xda, 0x64, 0x9e, 0xf3, 0xeb, 0x54, 0xf2, 0x85, 0xd3, 0x45, 0x57, + 0x21, 0xc6, 0xf1, 0xbb, 0x2b, 0x36, 0xde, 0x19, 0xf7, 0xc5, 0x45, 0xe4, 0xb4, 0xba, 0xf6, 0xd6, 0x4d, 0x0f, 0xba, + 0x71, 0x45, 0xf4, 0x18, 0xbf, 0xc4, 0x4c, 0xf7, 0xe6, 0x87, 0xc4, 0x3a, 0x7e, 0x37, 0xae, 0xf4, 0x5c, 0x4c, 0xe1, + 0x3e, 0x24, 0xf0, 0x3d, 0x7a, 0xb5, 0x42, 0x5c, 0x66, 0xdd, 0xf0, 0x82, 0x08, 0x50, 0x24, 0x00, 0x2b, 0x25, 0x09, + 0xa2, 0x25, 0x81, 0xe5, 0x70, 0xf2, 0xde, 0x56, 0x78, 0x6d, 0x7a, 0x77, 0x88, 0x16, 0x35, 0x2e, 0x54, 0x0c, 0x8f, + 0xbb, 0xa7, 0x93, 0xb9, 0x15, 0xa8, 0x57, 0xec, 0x41, 0x4c, 0x00, 0xa6, 0x05, 0x30, 0x56, 0x84, 0xcf, 0x6b, 0x44, + 0x1c, 0x00, 0x8a, 0x04, 0x0e, 0x30, 0xe2, 0x00, 0xfe, 0xbb, 0x9f, 0xf1, 0xa3, 0xf9, 0x85, 0x60, 0x59, 0xf4, 0x27, + 0xd3, 0x4f, 0xfb, 0x0c, 0x47, 0xe4, 0xf2, 0xe6, 0x21, 0x08, 0xb2, 0xda, 0x1c, 0xec, 0x8a, 0x1f, 0x60, 0x1b, 0xb7, + 0x27, 0xbc, 0xdc, 0x10, 0x5d, 0x3a, 0xab, 0xa4, 0xb4, 0x0b, 0xbe, 0xc0, 0xa5, 0x6f, 0xba, 0xbf, 0xa4, 0x87, 0xd5, + 0xc2, 0x17, 0xe3, 0x9e, 0xd5, 0x30, 0x3f, 0x78, 0xf1, 0xe8, 0xff, 0xac, 0x7a, 0xdd, 0x61, 0x63, 0x1c, 0xfe, 0x31, + 0xe0, 0x87, 0xa0, 0xf9, 0x49, 0xf6, 0xde, 0x47, 0xb7, 0xf6, 0xbd, 0x24, 0x39, 0x99, 0x1e, 0x56, 0x18, 0x4e, 0x3f, + 0x5e, 0x60, 0x55, 0x06, 0x3f, 0x97, 0x25, 0xd5, 0x5d, 0x85, 0x5f, 0x5c, 0x13, 0x61, 0x70, 0x0e, 0xef, 0xf8, 0x02, + 0x40, 0x5a, 0xcc, 0x70, 0x25, 0x5d, 0xeb, 0xf5, 0x77, 0x2f, 0xf8, 0xd6, 0x69, 0x92, 0x48, 0x20, 0x72, 0x5a, 0xc9, + 0xe1, 0x6c, 0x08, 0x4a, 0x4e, 0xca, 0xc3, 0x9c, 0x32, 0x38, 0x4b, 0x95, 0xd3, 0xa2, 0xc0, 0x9f, 0xda, 0xd9, 0xdd, + 0xba, 0xbc, 0x58, 0xd1, 0x1a, 0x4b, 0xf5, 0xbe, 0x0c, 0x35, 0x44, 0xb0, 0xd8, 0xf2, 0x69, 0x4b, 0x98, 0xfd, 0x0d, + 0x66, 0x53, 0x83, 0x08, 0xbf, 0xcf, 0x53, 0x42, 0x57, 0xde, 0x44, 0x04, 0x26, 0x54, 0x1f, 0x9a, 0x22, 0x46, 0x7a, + 0x44, 0xa7, 0x45, 0x42, 0x52, 0xab, 0x34, 0x42, 0x63, 0x0d, 0x89, 0x7e, 0xbf, 0x75, 0xcf, 0xab, 0xe5, 0x38, 0x1e, + 0xa3, 0xf2, 0x47, 0xd1, 0x6f, 0x30, 0x23, 0x17, 0xa4, 0xdd, 0xb0, 0x2b, 0x62, 0x98, 0xb2, 0x60, 0x18, 0xa8, 0xb2, + 0x41, 0x49, 0xe0, 0xb6, 0x62, 0xdb, 0xbf, 0xe3, 0xfb, 0x30, 0x22, 0xda, 0x4d, 0xc0, 0xcf, 0x3c, 0xa1, 0x76, 0x63, + 0x01, 0x1d, 0x7a, 0xc0, 0x6f, 0x58, 0xc3, 0x77, 0x4d, 0x14, 0xe9, 0x04, 0x4e, 0xd0, 0xb2, 0x48, 0xe2, 0xd3, 0xbd, + 0xf1, 0xff, 0x2f, 0x85, 0x54, 0x9f, 0xf7, 0xf7, 0xb7, 0x8d, 0x48, 0x0d, 0x3d, 0x15, 0xa8, 0xc8, 0xb8, 0x02, 0x5b, + 0xf6, 0x78, 0x29, 0x72, 0xc0, 0xc4, 0xe4, 0x5f, 0xb1, 0xc1, 0x4a, 0xe7, 0x8d, 0xe3, 0xd3, 0xbf, 0x60, 0x5a, 0x9c, + 0xed, 0x61, 0x16, 0xf3, 0x30, 0xfe, 0x4b, 0x47, 0x0f, 0x7a, 0xac, 0x87, 0x12, 0x2b, 0xe1, 0xc7, 0x65, 0x3e, 0xdc, + 0xf3, 0x8d, 0x59, 0xbe, 0xde, 0x1f, 0x2e, 0xec, 0x59, 0x89, 0xce, 0x8f, 0x7e, 0x89, 0xc5, 0x38, 0x32, 0xfe, 0x1b, + 0x6d, 0xc9, 0xe6, 0x36, 0xe0, 0x4e, 0x32, 0xa7, 0x77, 0x47, 0x47, 0x23, 0x0b, 0x72, 0x86, 0x25, 0xba, 0xbb, 0xe5, + 0x92, 0xdc, 0x65, 0xce, 0x2e, 0xfb, 0xfc, 0xeb, 0x7d, 0x76, 0xe1, 0x45, 0x7b, 0x4d, 0x9a, 0x4f, 0xd2, 0x06, 0x94, + 0x16, 0xb8, 0x3f, 0x9b, 0xdd, 0x22, 0x2a, 0x11, 0x32, 0x84, 0xf8, 0x82, 0x3b, 0x22, 0x05, 0xfb, 0x1d, 0xdb, 0x54, + 0x3c, 0xd0, 0x8d, 0xa8, 0xd7, 0x83, 0x97, 0x76, 0xdd, 0xf6, 0x8d, 0x01, 0x37, 0x4c, 0xd6, 0x2a, 0x46, 0xb5, 0xa0, + 0x59, 0x98, 0xde, 0x4e, 0x3e, 0x48, 0x55, 0x57, 0x12, 0x7a, 0x18, 0x1a, 0xf8, 0x14, 0xfb, 0x5a, 0xd7, 0x19, 0xbd, + 0x0c, 0x88, 0x7e, 0xc6, 0x0e, 0x3d, 0xf6, 0x03, 0xb3, 0xfc, 0x20, 0xe8, 0x62, 0xa9, 0x97, 0x40, 0x04, 0x34, 0x78, + 0x4d, 0x23, 0x56, 0x41, 0x9c, 0xb5, 0xd1, 0x61, 0xab, 0xa6, 0x07, 0x72, 0x8a, 0xbf, 0x58, 0x42, 0x28, 0x11, 0x5f, + 0x4d, 0xd3, 0xd2, 0x56, 0xe6, 0xe8, 0x2f, 0x0f, 0xc2, 0x5a, 0x90, 0x68, 0xea, 0x8c, 0xed, 0xad, 0xd2, 0x71, 0xf3, + 0x96, 0x97, 0x27, 0x24, 0xd0, 0xb6, 0x15, 0x61, 0x9e, 0x7f, 0xf2, 0x9f, 0xa6, 0xd6, 0x75, 0x0d, 0x5e, 0x99, 0x98, + 0xbf, 0x13, 0xb9, 0x95, 0xd3, 0xd1, 0x0f, 0x4d, 0xaa, 0x57, 0x0f, 0xb8, 0xc2, 0x7b, 0x33, 0xfe, 0xf3, 0x80, 0xd4, + 0xee, 0x38, 0x87, 0x33, 0x10, 0xa2, 0x79, 0x4e, 0x80, 0xd2, 0xa0, 0xe3, 0xe6, 0x20, 0x98, 0x95, 0x01, 0xc9, 0xce, + 0xea, 0x56, 0x7a, 0x8d, 0xcb, 0xd6, 0x89, 0x83, 0x74, 0xfb, 0x17, 0x62, 0xf2, 0x2c, 0xa5, 0x2b, 0x98, 0xe5, 0x65, + 0x42, 0x57, 0x2d, 0x06, 0x0a, 0x13, 0x39, 0x22, 0xfb, 0xbf, 0x62, 0x45, 0x1f, 0xac, 0xdf, 0x86, 0x8b, 0xb1, 0x23, + 0x24, 0xfb, 0x69, 0x16, 0xad, 0x91, 0xd2, 0xc8, 0x64, 0xc3, 0xe4, 0x52, 0x20, 0x57, 0x02, 0x09, 0x35, 0xea, 0x38, + 0x94, 0x83, 0x01, 0x9d, 0xda, 0x39, 0x28, 0x21, 0xec, 0x4b, 0x14, 0x50, 0x62, 0x44, 0x2a, 0x14, 0xfb, 0x39, 0x3a, + 0x4b, 0x19, 0x62, 0x66, 0x3a, 0x02, 0xee, 0x53, 0xa3, 0x84, 0x64, 0x32, 0x68, 0x00, 0xbd, 0xa5, 0x1d, 0xd4, 0x0f, + 0x70, 0x58, 0x64, 0xc4, 0xa5, 0x09, 0x80, 0xcf, 0x29, 0x6c, 0x6b, 0xff, 0x1e, 0x94, 0x2f, 0x5b, 0x17, 0x3d, 0xc8, + 0xd4, 0x45, 0x20, 0x74, 0x32, 0x8b, 0x05, 0x2a, 0xc3, 0xe5, 0xf0, 0xfb, 0xd4, 0x61, 0xaf, 0xa9, 0xd3, 0x4e, 0x91, + 0xc4, 0x5d, 0x9a, 0x69, 0xe8, 0xfb, 0x61, 0xdd, 0xdb, 0x34, 0xa9, 0xd8, 0x11, 0x78, 0x6b, 0x19, 0xcf, 0x42, 0xbb, + 0x51, 0x8c, 0x7d, 0x40, 0xde, 0xc8, 0xd0, 0xf9, 0x7f, 0x1b, 0x9b, 0x7e, 0xe0, 0x17, 0x9e, 0xa6, 0xcf, 0x9c, 0xf4, + 0xf3, 0xb0, 0x20, 0xbb, 0xc1, 0x4e, 0x45, 0x61, 0x45, 0x89, 0x2f, 0x50, 0x65, 0x53, 0x0d, 0xdd, 0x6b, 0x51, 0x28, + 0x92, 0x14, 0x72, 0x74, 0x61, 0x3c, 0xb9, 0x3c, 0x49, 0xb6, 0x5a, 0x46, 0xa5, 0x48, 0x12, 0xae, 0x4d, 0x48, 0xd6, + 0x09, 0x25, 0xda, 0xe7, 0xb1, 0xce, 0x48, 0xda, 0x8c, 0x0b, 0x76, 0xd6, 0x82, 0x6b, 0x53, 0xbb, 0xb9, 0x38, 0x65, + 0x1e, 0x6a, 0xfe, 0x44, 0x15, 0xa6, 0xcc, 0x9a, 0xa7, 0xb2, 0x36, 0xb9, 0x6a, 0xc8, 0x34, 0xf2, 0xa1, 0xbe, 0x0f, + 0xa9, 0x5e, 0x1c, 0x4e, 0x44, 0xc9, 0xf5, 0x89, 0x4b, 0x07, 0x00, 0xc4, 0x70, 0x9c, 0xf9, 0x65, 0xc9, 0x41, 0x14, + 0x70, 0xa2, 0x94, 0x29, 0xd9, 0x32, 0xb8, 0x1f, 0xc0, 0xbe, 0xb2, 0x1d, 0x05, 0x4a, 0xe6, 0xd8, 0x71, 0xed, 0x6f, + 0x4a, 0x48, 0x01, 0xfb, 0xa9, 0x92, 0x83, 0x71, 0x1d, 0x86, 0xea, 0x1c, 0xcc, 0x1d, 0x69, 0x17, 0xde, 0x57, 0x0c, + 0x5d, 0x9c, 0x8b, 0x22, 0x12, 0x2a, 0x09, 0x1f, 0x0f, 0x30, 0x9f, 0x17, 0x29, 0xec, 0x63, 0x42, 0xf4, 0x94, 0x43, + 0x54, 0x6a, 0xb5, 0x55, 0x0e, 0xf2, 0x82, 0x69, 0x63, 0x2a, 0xfb, 0x48, 0x1a, 0x40, 0xfc, 0x24, 0x6e, 0x50, 0xaa, + 0x6a, 0x9d, 0x76, 0x2b, 0x9a, 0xdb, 0x1a, 0x39, 0xb8, 0x6a, 0xbf, 0x31, 0xad, 0x2a, 0xb0, 0x13, 0x72, 0x2a, 0xa7, + 0x61, 0xeb, 0x4e, 0xc6, 0xbf, 0xdd, 0xaf, 0xa6, 0xbf, 0xfc, 0xb2, 0x14, 0x95, 0x60, 0x84, 0xcc, 0x64, 0x80, 0x6f, + 0x84, 0x10, 0xbc, 0x68, 0x6f, 0x3d, 0x54, 0xb6, 0x45, 0x1c, 0x47, 0x1d, 0x87, 0x15, 0x24, 0x10, 0xe6, 0x73, 0xb9, + 0x3b, 0x5b, 0x8d, 0x2e, 0xf6, 0xee, 0xa8, 0x7c, 0x95, 0x27, 0x89, 0x55, 0xc1, 0x4e, 0x49, 0xf1, 0x31, 0x00, 0x58, + 0x92, 0x27, 0x82, 0x15, 0xe4, 0x8e, 0x37, 0x0d, 0x7c, 0x98, 0xf0, 0x24, 0xf9, 0xbf, 0xbe, 0x09, 0xfc, 0x4c, 0x71, + 0xc9, 0xf2, 0x6d, 0x79, 0x30, 0x59, 0xce, 0x56, 0x2d, 0x05, 0x44, 0x23, 0x02, 0x13, 0x87, 0x7c, 0x9c, 0x5f, 0x27, + 0xd9, 0xbb, 0x0c, 0xf1, 0xa9, 0xf9, 0xa0, 0x27, 0x34, 0xcf, 0xfc, 0x26, 0x34, 0xf4, 0xb8, 0x52, 0x05, 0x5a, 0x02, + 0x42, 0x13, 0xf7, 0x8f, 0xd7, 0x86, 0x0e, 0xa6, 0x59, 0x7f, 0x41, 0xc0, 0x00, 0xab, 0xbc, 0xad, 0xa0, 0x0a, 0x73, + 0x3b, 0x12, 0xde, 0xd4, 0x0d, 0xe1, 0x2b, 0x67, 0xc6, 0xd1, 0xc9, 0x14, 0x3e, 0x19, 0x10, 0x40, 0x7c, 0x54, 0x6f, + 0x44, 0x43, 0x7c, 0x33, 0xcf, 0xaa, 0x3a, 0xb7, 0xb0, 0x55, 0xec, 0x97, 0xf0, 0x47, 0xaf, 0x61, 0x2f, 0xac, 0x8c, + 0x97, 0xc8, 0x15, 0x3f, 0xeb, 0xe8, 0xf8, 0x39, 0x68, 0x53, 0x43, 0xeb, 0x51, 0xa5, 0x0a, 0x15, 0xc7, 0x0c, 0x23, + 0x8a, 0x05, 0x9e, 0x63, 0x8c, 0x4f, 0xe8, 0x9e, 0xdb, 0xf2, 0xd7, 0xc8, 0xb0, 0xf9, 0x2f, 0x87, 0xf2, 0x75, 0xe6, + 0x98, 0xd0, 0x33, 0xe5, 0x4c, 0x85, 0x33, 0x1c, 0x61, 0xac, 0x37, 0xbe, 0xc1, 0xdc, 0x55, 0x33, 0xb6, 0xb5, 0x3a, + 0x93, 0xa2, 0xe9, 0x52, 0x54, 0x9f, 0x41, 0x43, 0xbc, 0xeb, 0xc6, 0xc0, 0xc2, 0xdd, 0x9f, 0x03, 0x42, 0x6e, 0x0e, + 0x85, 0xab, 0xda, 0x8c, 0x10, 0x6a, 0x09, 0xd4, 0x67, 0x85, 0xb0, 0x92, 0x56, 0x49, 0x4a, 0x4d, 0x31, 0xcf, 0x1f, + 0xc1, 0x7a, 0xaf, 0xf9, 0xff, 0x97, 0x19, 0xd1, 0xf7, 0xcb, 0xfe, 0x33, 0x7e, 0x41, 0xf4, 0x8c, 0x15, 0x4b, 0x26, + 0xfa, 0xf6, 0xba, 0x60, 0xc0, 0x09, 0xdf, 0x5e, 0xc3, 0xa9, 0xb5, 0xae, 0xdd, 0x4f, 0x0f, 0xe1, 0xfe, 0xbc, 0x51, + 0x2c, 0x9d, 0x22, 0x84, 0x58, 0xca, 0xcb, 0xcc, 0x54, 0xd2, 0x8a, 0x99, 0x17, 0x1d, 0x40, 0x9a, 0x77, 0x61, 0x76, + 0x9b, 0x72, 0x94, 0x25, 0x81, 0x67, 0x15, 0x30, 0xcd, 0xb0, 0x9d, 0x13, 0xa8, 0x5f, 0x1c, 0xff, 0x1d, 0xeb, 0xfe, + 0x0b, 0xe7, 0xa0, 0xee, 0xcf, 0x4f, 0x21, 0x91, 0x05, 0x4a, 0x94, 0x8c, 0x9a, 0x6e, 0x47, 0x75, 0x27, 0xeb, 0xdd, + 0x0b, 0x53, 0x22, 0x26, 0x5d, 0xf9, 0xdc, 0xcf, 0xed, 0x03, 0x68, 0x68, 0xab, 0x50, 0x55, 0x77, 0x65, 0xe3, 0x7c, + 0x45, 0x4b, 0x36, 0x20, 0xfd, 0x56, 0xfa, 0xe2, 0x06, 0x99, 0x97, 0x25, 0x51, 0x56, 0x91, 0xb3, 0xa4, 0xdb, 0xd3, + 0x39, 0x0a, 0x99, 0xe3, 0x7c, 0xe5, 0x85, 0xad, 0x95, 0xf6, 0xb5, 0x2a, 0xdb, 0x70, 0xa9, 0xa4, 0x68, 0x11, 0xcc, + 0x7a, 0x9f, 0xa3, 0xfe, 0x2e, 0x6f, 0x92, 0x89, 0x62, 0x54, 0x55, 0xbc, 0xae, 0x44, 0x2f, 0x7e, 0x7e, 0x0d, 0xc7, + 0x84, 0x7e, 0xf5, 0x07, 0xbd, 0xa5, 0xea, 0xde, 0x77, 0x98, 0xca, 0xec, 0xcd, 0x21, 0x88, 0xd2, 0x0d, 0xe9, 0xd5, + 0x5f, 0x89, 0x8f, 0xeb, 0xed, 0x89, 0x60, 0x39, 0x5d, 0x57, 0xf6, 0xeb, 0x7c, 0x5c, 0x0a, 0x73, 0x1e, 0xa9, 0x97, + 0xa6, 0xc1, 0xaf, 0x54, 0x51, 0x61, 0xce, 0xfa, 0xc7, 0x6c, 0x0a, 0xce, 0x4b, 0xd7, 0x32, 0x84, 0x1c, 0x91, 0xd0, + 0xc8, 0x91, 0x60, 0xce, 0xbf, 0x50, 0x8c, 0x5f, 0xb4, 0x49, 0xec, 0x8e, 0x5f, 0xc9, 0x6e, 0xa8, 0xe9, 0xa7, 0xcf, + 0xb9, 0x4b, 0x27, 0x54, 0x50, 0x7b, 0x82, 0x4b, 0xb0, 0xc0, 0xfb, 0x2b, 0x9b, 0x74, 0x31, 0xaa, 0xaa, 0x57, 0xe7, + 0xf3, 0x8f, 0x86, 0x38, 0x4c, 0x05, 0x14, 0x16, 0x6f, 0x32, 0x87, 0x76, 0x86, 0xd7, 0x74, 0x98, 0x67}; // Backwards compatibility alias #define INDEX_GZ INDEX_BR diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index a1cafe8707..37c80ddf96 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -3634,4058 +3634,4060 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x36, 0x16, 0x43, 0x14, 0xa2, 0x85, 0xb1, 0x9f, 0x44, 0x7a, 0x6a, 0xf7, 0x8b, 0xa8, 0x63, 0x84, 0xe7, 0x2e, 0x8e, 0x40, 0xbb, 0x60, 0xb2, 0xd8, 0xed, 0x3a, 0x06, 0xb0, 0x93, 0x12, 0x46, 0xf3, 0x4c, 0x91, 0xc8, 0x45, 0x3d, 0x95, 0x78, 0xf0, 0xa9, 0x53, 0x8d, 0x99, 0x83, 0xf0, 0x54, 0x31, 0xd4, 0xc0, 0xc7, 0x7a, 0xaf, 0x4d, 0xc8, 0x99, 0xff, - 0xaf, 0xbd, 0xa7, 0xdd, 0x6e, 0x13, 0x49, 0xf6, 0xff, 0x3c, 0x05, 0x26, 0xd9, 0x04, 0x12, 0xc0, 0x20, 0xf9, 0x43, - 0x91, 0x8c, 0x3c, 0x93, 0xc4, 0x99, 0x8f, 0xf5, 0x4c, 0xe6, 0x24, 0x9e, 0xec, 0xbd, 0xeb, 0xf5, 0xb1, 0x90, 0xd4, - 0x92, 0xd8, 0x20, 0xd0, 0x01, 0x64, 0xd9, 0xa3, 0xb0, 0xcf, 0xb2, 0x8f, 0x70, 0x9f, 0x61, 0x9f, 0xec, 0x9e, 0xaa, - 0xea, 0x86, 0x06, 0x81, 0x2c, 0x4f, 0x32, 0xb3, 0x7b, 0xcf, 0xb9, 0x67, 0x26, 0x89, 0x68, 0xba, 0x9b, 0xea, 0xaf, + 0xaf, 0xbd, 0xa7, 0xdd, 0x6e, 0x13, 0x49, 0xf6, 0xff, 0x3c, 0x05, 0x26, 0xd9, 0x04, 0x12, 0xc0, 0x20, 0x59, 0xb6, + 0x22, 0x19, 0x79, 0x26, 0x89, 0x33, 0x1f, 0xeb, 0x99, 0xcc, 0x49, 0x3c, 0xd9, 0x7b, 0xd7, 0xeb, 0x63, 0x21, 0xa9, + 0x25, 0xb1, 0x41, 0xa0, 0x03, 0xc8, 0x1f, 0xa3, 0xb0, 0xcf, 0xb2, 0x8f, 0x70, 0x9f, 0x61, 0x9f, 0xec, 0x9e, 0xaa, + 0xea, 0x86, 0x06, 0x81, 0x2c, 0x4f, 0x32, 0xb3, 0x7b, 0xcf, 0xb9, 0x67, 0x26, 0x89, 0x68, 0x9a, 0xee, 0xea, 0xaf, 0xaa, 0xea, 0xfa, 0x2c, 0x53, 0x4a, 0xbb, 0x82, 0x7f, 0x85, 0x15, 0x72, 0xa5, 0x94, 0xb6, 0x1c, 0x88, 0x39, 0xdd, - 0x3e, 0xae, 0x1a, 0x58, 0x15, 0x8a, 0x7b, 0x32, 0x98, 0x09, 0x56, 0x76, 0x9d, 0x98, 0x87, 0x59, 0x97, 0x36, 0xbc, - 0x11, 0xb8, 0x60, 0x7a, 0xd8, 0x50, 0x6b, 0xdc, 0xf5, 0x4a, 0xa1, 0x30, 0xe3, 0xf2, 0xa4, 0x9e, 0x78, 0xe5, 0x67, - 0xd8, 0xd2, 0x95, 0x2a, 0xe0, 0x1b, 0x53, 0xa9, 0x04, 0x52, 0xb0, 0xe0, 0x94, 0x3e, 0x6f, 0xa5, 0xd1, 0x79, 0xb4, - 0x12, 0x82, 0xe3, 0x13, 0xaf, 0xa6, 0x10, 0xcf, 0x49, 0x77, 0x74, 0x12, 0xd0, 0x0f, 0x27, 0x6b, 0xa0, 0x00, 0x59, - 0x31, 0xc1, 0x39, 0xdb, 0x3a, 0xa0, 0xcb, 0xd4, 0xf5, 0xe3, 0xb5, 0xd8, 0x72, 0xd9, 0xc0, 0xcf, 0xd3, 0xc2, 0x96, + 0x01, 0xae, 0x1a, 0x58, 0x15, 0x8a, 0x7b, 0x32, 0x98, 0x09, 0x56, 0x76, 0x9d, 0x98, 0x87, 0x79, 0x8f, 0x36, 0xbc, + 0x11, 0xb8, 0x60, 0x7a, 0xd8, 0x50, 0x6b, 0xd2, 0xf3, 0x4a, 0xa1, 0x30, 0xe3, 0xf2, 0xa4, 0x1e, 0x7b, 0xe5, 0x67, + 0xd8, 0xd2, 0x95, 0x2a, 0xe0, 0x1b, 0x53, 0xa9, 0x04, 0x52, 0xb0, 0xe0, 0x84, 0xba, 0xb7, 0xd2, 0xe8, 0x2c, 0xba, + 0x11, 0x82, 0xe3, 0x63, 0xaf, 0xa6, 0x10, 0xcf, 0x49, 0x6f, 0x7c, 0x1c, 0xd0, 0x0f, 0x27, 0x6b, 0xa0, 0x00, 0x59, + 0x31, 0xc1, 0x39, 0xdb, 0x3a, 0xa4, 0xcb, 0xd4, 0xd5, 0xe3, 0xb5, 0xd8, 0x72, 0xd9, 0xd0, 0xcf, 0xd3, 0xc2, 0x96, 0x58, 0x8e, 0x8c, 0x4f, 0xbd, 0x1c, 0x85, 0xb4, 0xa6, 0x1a, 0xdf, 0x1f, 0xae, 0x5f, 0xcb, 0xb7, 0x58, 0xf4, 0xc8, - 0x88, 0xa6, 0xd7, 0x57, 0x61, 0xb7, 0x6c, 0xa4, 0xd5, 0x01, 0x46, 0x82, 0x27, 0x31, 0x04, 0x0c, 0xcc, 0x8c, 0xa8, - 0xff, 0xdb, 0xb8, 0x8a, 0xfa, 0xd1, 0xfe, 0x2e, 0x47, 0xfe, 0x3f, 0xbf, 0x7d, 0x7f, 0x01, 0x7a, 0x2d, 0x0f, 0x15, - 0xd1, 0x6b, 0x95, 0xdb, 0xb0, 0x98, 0xa0, 0x29, 0x52, 0xbb, 0xaa, 0xb7, 0x00, 0xea, 0x8c, 0x37, 0x86, 0xfd, 0x5b, - 0x73, 0xb5, 0x5a, 0x99, 0x60, 0xd1, 0x6a, 0x2e, 0xe3, 0x80, 0xb8, 0xc3, 0xb1, 0x9a, 0x09, 0xa4, 0xce, 0x2a, 0x48, - 0x1d, 0xc2, 0xe1, 0xf2, 0x7c, 0x2a, 0xef, 0x67, 0xd1, 0xea, 0x9b, 0x20, 0x90, 0xc5, 0x36, 0x82, 0x89, 0xe3, 0x92, - 0x8c, 0x12, 0x32, 0xd0, 0x40, 0xfb, 0x64, 0xf9, 0xc9, 0x35, 0xb7, 0x17, 0x18, 0x5f, 0x0f, 0xef, 0xae, 0xb9, 0x4e, - 0x22, 0x8f, 0x47, 0xfc, 0x7e, 0x70, 0x32, 0xf6, 0x6f, 0x14, 0xe4, 0x34, 0x5d, 0x15, 0x9c, 0xb9, 0x02, 0x36, 0x5c, - 0xa6, 0x69, 0x14, 0x9a, 0x71, 0xb4, 0x52, 0xfb, 0x27, 0xf4, 0x20, 0x2a, 0x78, 0xf4, 0xa8, 0x2a, 0x5f, 0x8f, 0x02, - 0x7f, 0xf4, 0xd1, 0x55, 0x1f, 0xaf, 0x7d, 0xb7, 0x5f, 0xe1, 0x27, 0xed, 0x4c, 0xed, 0x03, 0xac, 0xca, 0x37, 0x41, - 0x70, 0xb2, 0x4f, 0x2d, 0xfa, 0x27, 0xfb, 0x63, 0xff, 0xa6, 0x2f, 0xa5, 0x86, 0xe1, 0x7a, 0x53, 0x97, 0x87, 0xe0, - 0xcc, 0x2d, 0xcd, 0x12, 0x8c, 0xe9, 0x30, 0x62, 0x5a, 0x71, 0xf9, 0x85, 0x58, 0x33, 0x04, 0xaf, 0x36, 0x42, 0x71, - 0x7a, 0x00, 0x57, 0xbd, 0x4f, 0x9f, 0xb4, 0xdc, 0x0e, 0x75, 0x26, 0x05, 0x69, 0x43, 0x35, 0x1f, 0x56, 0x31, 0x30, - 0xd2, 0x8c, 0xae, 0x89, 0x50, 0x72, 0x81, 0x6e, 0x8c, 0x32, 0x03, 0x33, 0xec, 0x78, 0x0b, 0xd0, 0x38, 0xf2, 0x9f, - 0xd2, 0x8d, 0x78, 0x04, 0x59, 0xb5, 0x25, 0x24, 0xae, 0x4b, 0x3a, 0x17, 0x3a, 0x85, 0x3c, 0x4e, 0x20, 0x28, 0x4b, - 0xf0, 0x3b, 0xa4, 0x07, 0xd1, 0x02, 0x1d, 0xb2, 0xba, 0xe5, 0xc1, 0x79, 0xbc, 0x4c, 0xe4, 0x51, 0x13, 0xf3, 0x72, - 0x5a, 0x5a, 0xa1, 0x6e, 0x75, 0xbd, 0x44, 0xd4, 0xc8, 0xbd, 0xa4, 0x69, 0xc9, 0x40, 0x87, 0xa7, 0xa5, 0x46, 0x85, - 0xe6, 0x82, 0x57, 0x9f, 0xa4, 0x38, 0x62, 0x86, 0x76, 0x99, 0x18, 0xd1, 0x55, 0x41, 0xa7, 0x12, 0x42, 0x94, 0xdd, - 0x28, 0x2b, 0x02, 0x38, 0xd3, 0xaa, 0xf7, 0x1f, 0xaf, 0x43, 0x24, 0x6c, 0x89, 0xdb, 0x2f, 0xef, 0x83, 0xd4, 0x1b, - 0x9a, 0xb4, 0x99, 0x55, 0xe5, 0xeb, 0xf1, 0x30, 0xc8, 0x17, 0x9b, 0x0e, 0xc1, 0xcc, 0x0b, 0xc7, 0x01, 0xbb, 0xf0, - 0x86, 0xdf, 0x61, 0x9d, 0xd7, 0xc3, 0xe0, 0x15, 0x54, 0xc8, 0xd4, 0xfe, 0xe3, 0x35, 0x91, 0xee, 0x3a, 0x84, 0x9d, - 0xd1, 0x16, 0xa8, 0x7e, 0x87, 0xa7, 0x5c, 0x62, 0x31, 0xb5, 0x46, 0x60, 0x89, 0xdc, 0x52, 0x1c, 0xdb, 0x32, 0x64, - 0x3c, 0xe5, 0x0f, 0xec, 0x4d, 0x85, 0x9f, 0x5a, 0x80, 0x2b, 0x12, 0x27, 0x58, 0xde, 0x99, 0x32, 0xb0, 0x44, 0x56, - 0xdf, 0x45, 0x2b, 0x01, 0x29, 0x9f, 0x00, 0x0a, 0x51, 0x79, 0xfa, 0x7e, 0x70, 0x22, 0xab, 0x85, 0x50, 0x76, 0x4e, - 0xfd, 0xc2, 0xaf, 0x4c, 0x55, 0x8a, 0x04, 0x50, 0x8b, 0x5b, 0xb5, 0x7f, 0xb2, 0x2f, 0xd7, 0xee, 0x0f, 0xba, 0x67, - 0xd2, 0xe0, 0xb0, 0x57, 0x71, 0x6f, 0xbe, 0x2c, 0x1e, 0xb2, 0x2b, 0x05, 0x6e, 0xc9, 0x19, 0x94, 0xc0, 0x1c, 0x95, - 0x9b, 0x6c, 0x90, 0x1f, 0x48, 0x99, 0x58, 0x10, 0x28, 0xda, 0x3d, 0x02, 0x3f, 0x46, 0x7a, 0x37, 0x5f, 0x42, 0xb2, - 0xcc, 0x14, 0xbd, 0x0d, 0xf8, 0xbf, 0xc5, 0x94, 0xa0, 0xa4, 0x9b, 0x85, 0x49, 0x14, 0xab, 0x30, 0xcc, 0x6a, 0xde, - 0x24, 0x45, 0xca, 0xd7, 0x86, 0x03, 0xae, 0x25, 0xab, 0x30, 0x61, 0xfb, 0xd5, 0xa6, 0xd2, 0xb8, 0x07, 0x7a, 0xf1, - 0x43, 0xe1, 0x83, 0xa9, 0x20, 0xad, 0x1c, 0xc0, 0xe6, 0x7c, 0x54, 0x97, 0x8f, 0x7d, 0xe3, 0x2f, 0x91, 0x31, 0xf4, - 0x8c, 0x6b, 0xcf, 0xf8, 0x31, 0xbc, 0xca, 0x6a, 0x17, 0x2f, 0xcf, 0x25, 0x67, 0xb0, 0x9e, 0x06, 0x11, 0x98, 0xca, - 0x97, 0x0a, 0xdf, 0xe2, 0x36, 0x23, 0x17, 0x5e, 0x3c, 0x65, 0x22, 0x85, 0x9b, 0x78, 0x2b, 0x64, 0x07, 0xba, 0x34, - 0x2d, 0x10, 0x9e, 0x6c, 0x8f, 0x9b, 0xd6, 0xf9, 0xd6, 0x28, 0x8d, 0x83, 0x3f, 0xb3, 0x3b, 0x60, 0xb3, 0x92, 0x34, - 0x5a, 0x80, 0xcc, 0xca, 0x9b, 0x72, 0x1d, 0x84, 0xa1, 0xb1, 0xdd, 0x3e, 0xf7, 0xe9, 0x13, 0x93, 0xb2, 0x8a, 0xa5, - 0xd1, 0x74, 0x1a, 0x30, 0x4d, 0xca, 0x3e, 0x96, 0x7f, 0xe6, 0x74, 0xcf, 0x16, 0x91, 0xab, 0xf5, 0xac, 0xe9, 0x60, - 0x89, 0x11, 0xb3, 0x9c, 0x1b, 0x04, 0xc4, 0x45, 0xc6, 0x55, 0xc8, 0x90, 0x6b, 0xe2, 0x5c, 0x14, 0x07, 0xd7, 0x1c, - 0x47, 0xcb, 0x61, 0xc0, 0x4c, 0x3c, 0x0d, 0xf0, 0xc9, 0xf5, 0x70, 0x39, 0x1c, 0x06, 0x94, 0x2e, 0x0c, 0xe2, 0xaf, - 0x45, 0x09, 0xca, 0x45, 0x33, 0xbd, 0x07, 0x83, 0xb2, 0xd2, 0x2a, 0xf8, 0x60, 0x33, 0x09, 0x37, 0x07, 0xfa, 0x40, - 0x0a, 0x32, 0xd0, 0xcd, 0x33, 0xed, 0xaa, 0x70, 0x63, 0x61, 0x89, 0xda, 0xab, 0x61, 0xe9, 0xdc, 0x4b, 0xf5, 0x3d, - 0xce, 0xb0, 0xe2, 0x85, 0x63, 0xe5, 0x15, 0xed, 0x5d, 0xd5, 0x50, 0xc9, 0xf4, 0x8b, 0x67, 0x97, 0x53, 0x0d, 0xf5, - 0xb5, 0xef, 0x4d, 0xc3, 0x28, 0x49, 0xfd, 0x91, 0x7a, 0xd5, 0x7b, 0xed, 0x6b, 0x97, 0xf3, 0x54, 0xd3, 0xaf, 0x8c, - 0x6f, 0xe5, 0x3c, 0x60, 0x02, 0x53, 0x62, 0x1a, 0xb0, 0x86, 0x3a, 0xf2, 0xe9, 0xd9, 0x56, 0x4f, 0x60, 0x64, 0xac, - 0xf3, 0xad, 0x0b, 0xb5, 0x2a, 0x19, 0xc5, 0x30, 0x55, 0x24, 0x64, 0x14, 0xfb, 0x56, 0xef, 0x91, 0x10, 0xe6, 0x9b, - 0xe5, 0x1a, 0x99, 0x86, 0xb4, 0x20, 0xbe, 0x18, 0x04, 0x5f, 0x78, 0x8e, 0xd2, 0xf3, 0x9e, 0xec, 0xf5, 0x50, 0x22, - 0xe3, 0x83, 0x6f, 0xca, 0x1c, 0xc8, 0xe3, 0x75, 0x9a, 0x81, 0xc9, 0x61, 0x18, 0xa5, 0x0a, 0x44, 0x76, 0x83, 0x0f, - 0x0e, 0xaa, 0x56, 0xd2, 0xbc, 0x57, 0x4d, 0xcf, 0x38, 0x16, 0x78, 0x89, 0xb4, 0x14, 0x25, 0x97, 0x10, 0x88, 0x02, - 0x82, 0x94, 0x96, 0xe2, 0x38, 0x71, 0xdf, 0x3c, 0x58, 0xbe, 0x12, 0xff, 0x26, 0xe1, 0xfd, 0x32, 0x3d, 0x7f, 0xbc, - 0x4e, 0x4e, 0x05, 0x51, 0xff, 0x3e, 0xc1, 0xb5, 0x04, 0x76, 0x85, 0x53, 0xf9, 0x4c, 0x55, 0x4e, 0x05, 0x25, 0xc2, - 0xba, 0x25, 0xf4, 0xaa, 0x09, 0x76, 0x37, 0x16, 0x31, 0xf3, 0xb9, 0x18, 0x45, 0x30, 0x60, 0x95, 0xa3, 0x07, 0xc1, - 0x9a, 0x72, 0xde, 0x2a, 0x05, 0x8b, 0x6b, 0x24, 0x18, 0x80, 0xb9, 0x38, 0x8f, 0x30, 0xc8, 0xae, 0x81, 0x91, 0x84, - 0x08, 0x66, 0x62, 0x8c, 0x46, 0x24, 0x27, 0x91, 0xf3, 0xc3, 0xc5, 0x32, 0xc5, 0xc8, 0xf4, 0x00, 0x00, 0xcb, 0x54, - 0x05, 0x2f, 0x8c, 0x80, 0xeb, 0x8b, 0x0b, 0x4f, 0xa6, 0x2a, 0xfe, 0x78, 0xb3, 0x8c, 0x4b, 0x67, 0x00, 0xc7, 0xe1, - 0x30, 0x50, 0xaf, 0x03, 0x8f, 0x31, 0x1f, 0xc6, 0xc8, 0x28, 0xd2, 0xba, 0x68, 0x23, 0xb4, 0x7f, 0xa8, 0x41, 0x20, - 0x23, 0xea, 0xa7, 0xa7, 0x05, 0xb5, 0x83, 0x85, 0x68, 0xd5, 0xa5, 0x61, 0x0e, 0x40, 0x46, 0x79, 0x0a, 0x73, 0xe7, - 0xc2, 0xa5, 0x5e, 0x98, 0xd6, 0xa9, 0x17, 0x2a, 0xd9, 0xd5, 0x0d, 0x70, 0x1a, 0x06, 0xd9, 0x75, 0xe1, 0xe8, 0x5a, - 0x8c, 0x17, 0xb6, 0x24, 0x95, 0x2b, 0x68, 0xe9, 0xe6, 0x72, 0x7b, 0xb6, 0x45, 0xec, 0xcf, 0xbd, 0xf8, 0x8e, 0xcc, - 0xdf, 0x0c, 0xd9, 0x46, 0x4e, 0x57, 0x15, 0xa2, 0x07, 0x34, 0x01, 0x44, 0x1a, 0x54, 0xe5, 0xeb, 0xbc, 0x8c, 0xf1, - 0xd1, 0xe6, 0x36, 0x40, 0xf0, 0xad, 0x6b, 0xf5, 0x39, 0xb3, 0x48, 0xfe, 0x48, 0x4d, 0x7a, 0x5a, 0xd2, 0x30, 0xbc, - 0xa4, 0x3c, 0xbc, 0xb0, 0xbc, 0xd1, 0x70, 0x30, 0x44, 0x29, 0x08, 0x6e, 0x1c, 0x19, 0x26, 0xc1, 0xac, 0x5f, 0x51, - 0x7a, 0xf7, 0x87, 0x2e, 0x07, 0x83, 0xe5, 0x08, 0x61, 0x39, 0x6a, 0x44, 0xb3, 0x9e, 0x58, 0x11, 0xe0, 0x45, 0x80, - 0x0b, 0x89, 0x91, 0x03, 0xa1, 0xfc, 0x98, 0x4a, 0xbe, 0x85, 0x62, 0x38, 0x1a, 0x04, 0x3b, 0x1d, 0x8d, 0xd8, 0x75, - 0x23, 0x6c, 0x15, 0x67, 0x27, 0xfb, 0x54, 0x9b, 0x88, 0x22, 0x55, 0x82, 0x69, 0x88, 0x61, 0x84, 0xc5, 0x2c, 0x40, - 0x82, 0x70, 0xd7, 0x29, 0x2e, 0x3a, 0xd6, 0x1c, 0xd5, 0xd2, 0xce, 0x69, 0x99, 0xe1, 0xc1, 0x56, 0x6a, 0xff, 0x04, - 0x53, 0x7e, 0x02, 0x59, 0x87, 0xa0, 0x58, 0x27, 0xfb, 0xf4, 0xa8, 0x54, 0x4e, 0x44, 0xd1, 0x89, 0x90, 0x41, 0x76, - 0x79, 0x07, 0x0f, 0x3a, 0x2a, 0x49, 0xca, 0x16, 0x50, 0xea, 0x65, 0xaa, 0x32, 0xe7, 0x0c, 0x16, 0x8f, 0xbe, 0x07, - 0xa1, 0x79, 0x6c, 0x70, 0x89, 0x50, 0x95, 0xb9, 0x77, 0x8b, 0x23, 0x17, 0x6f, 0xbc, 0x5b, 0xcd, 0xe1, 0xaf, 0x8a, - 0xb3, 0x96, 0x94, 0xcf, 0xda, 0x68, 0xe3, 0x86, 0x1c, 0xc0, 0x0d, 0x79, 0x54, 0xbf, 0xb8, 0x33, 0xb1, 0xb8, 0xe3, - 0x86, 0xc5, 0x1d, 0x6f, 0x59, 0xdc, 0x80, 0x2f, 0xa4, 0x92, 0x4f, 0x5d, 0x8c, 0xbe, 0xd4, 0xf9, 0xe4, 0x71, 0x7e, - 0xa4, 0xcb, 0xcf, 0x19, 0xce, 0x93, 0x99, 0x04, 0x60, 0x4b, 0xdc, 0x30, 0x57, 0x75, 0xf3, 0x22, 0x4d, 0xc4, 0xe6, - 0xc0, 0xf3, 0x53, 0x27, 0xc6, 0x0d, 0x29, 0xbc, 0xb5, 0xa0, 0x3a, 0x5e, 0xd8, 0xa5, 0xd8, 0xd0, 0xd0, 0x66, 0x0d, - 0x23, 0x9d, 0x6d, 0x19, 0xe9, 0xa8, 0x74, 0x74, 0xf9, 0xb0, 0xe9, 0x10, 0xca, 0x83, 0x82, 0x3d, 0x08, 0xfe, 0x15, - 0xb8, 0x65, 0xca, 0xfb, 0xb0, 0x19, 0xc7, 0x4a, 0x3b, 0x6a, 0xe1, 0x25, 0xc9, 0x2a, 0x8a, 0xc1, 0x40, 0x01, 0xba, - 0x79, 0xd8, 0x96, 0x9a, 0xfb, 0x21, 0x8f, 0x7d, 0xd6, 0xb8, 0x99, 0x8a, 0xf7, 0xf2, 0x96, 0x6a, 0x1d, 0x1e, 0x52, - 0x8d, 0x85, 0x97, 0xa6, 0x2c, 0xc6, 0x49, 0xf7, 0x20, 0x49, 0xc6, 0x7f, 0xc8, 0x36, 0xab, 0xc1, 0x21, 0x81, 0x84, - 0xd5, 0x11, 0x43, 0x2f, 0x80, 0x05, 0x23, 0x8d, 0x64, 0xa8, 0xaf, 0xa5, 0x38, 0xaa, 0x71, 0x3e, 0xf1, 0x3f, 0xe1, - 0x71, 0xd5, 0x62, 0xc9, 0xd3, 0xd7, 0x39, 0xd2, 0xad, 0x85, 0x37, 0x7e, 0x0f, 0x76, 0x30, 0x5a, 0xcb, 0x00, 0x9f, - 0x16, 0x39, 0x6a, 0x6a, 0x4c, 0x3c, 0xe1, 0xa8, 0x40, 0x92, 0x88, 0x25, 0xb9, 0xc5, 0x30, 0x04, 0x1b, 0xf0, 0xcc, - 0xc9, 0xd5, 0xba, 0x95, 0xed, 0x4f, 0x7d, 0x7d, 0x03, 0x6b, 0x02, 0x6a, 0x0b, 0xdc, 0x7e, 0x2e, 0x74, 0x0b, 0x0c, - 0xe7, 0x48, 0x07, 0x45, 0xe9, 0x25, 0xa4, 0x43, 0xb7, 0xc5, 0x65, 0x7a, 0x10, 0x03, 0xd5, 0x02, 0xb5, 0xe2, 0x93, - 0x29, 0xfe, 0x72, 0xae, 0xb2, 0x27, 0x43, 0xfc, 0xd5, 0xba, 0xca, 0x95, 0x58, 0x15, 0x29, 0x82, 0x34, 0x66, 0xb5, - 0x5f, 0xda, 0x4f, 0x64, 0xae, 0xfd, 0x80, 0x6d, 0xc3, 0x17, 0xf8, 0xd1, 0xe3, 0x75, 0x02, 0x01, 0x0a, 0xe4, 0x31, - 0x84, 0x56, 0xac, 0x67, 0xb5, 0xe5, 0xd3, 0x86, 0xf2, 0xa1, 0xfe, 0x07, 0x13, 0x7e, 0xdc, 0x25, 0x51, 0x41, 0x53, - 0xca, 0x32, 0x90, 0xeb, 0xa1, 0x1f, 0x7a, 0xf1, 0xdd, 0x35, 0xdd, 0x42, 0x34, 0xc1, 0xe2, 0xe7, 0xb2, 0x1d, 0xe2, - 0x45, 0xcb, 0xd6, 0x21, 0xa9, 0xa4, 0xa8, 0xba, 0xe3, 0x84, 0xde, 0xfd, 0x73, 0x2c, 0xf1, 0x77, 0xa5, 0x6b, 0x2c, - 0x5f, 0x90, 0xd2, 0x87, 0xae, 0x1f, 0xaf, 0x35, 0xb6, 0xd9, 0x4d, 0x65, 0xb4, 0x15, 0x06, 0x12, 0x96, 0x07, 0xaf, - 0xc4, 0xf3, 0xb1, 0xdf, 0x45, 0xf3, 0x8f, 0x61, 0x74, 0x6b, 0x3e, 0x5e, 0xa7, 0xa7, 0xea, 0xdc, 0x8b, 0x3f, 0xb2, - 0xb1, 0x39, 0xf2, 0xe3, 0x51, 0x00, 0xcc, 0xe3, 0x30, 0xf0, 0xc2, 0x8f, 0xfc, 0xd1, 0x8c, 0x96, 0x29, 0x1a, 0x74, - 0xdd, 0x7b, 0x83, 0x16, 0x73, 0x42, 0x82, 0x44, 0xe4, 0x6a, 0x6b, 0x66, 0x41, 0x79, 0x3f, 0x10, 0xd7, 0xfa, 0x82, - 0x51, 0x2c, 0x6a, 0x19, 0xe0, 0x8f, 0x00, 0x36, 0x66, 0x10, 0xe0, 0xc1, 0x50, 0x71, 0xbd, 0x54, 0x43, 0x1e, 0x2a, - 0x69, 0xd5, 0xf2, 0x0c, 0xc5, 0xd7, 0xd8, 0xc3, 0x6f, 0xff, 0x1c, 0x94, 0x3c, 0xe4, 0x73, 0x79, 0x2f, 0x9f, 0x37, - 0x42, 0x28, 0x35, 0xc9, 0xb1, 0xf0, 0x01, 0x1f, 0xe7, 0x0c, 0x66, 0xf3, 0xa7, 0xe5, 0xc6, 0x5e, 0x92, 0x2c, 0xe7, - 0x6c, 0x4c, 0x2a, 0xb1, 0xd3, 0x02, 0xa8, 0xf2, 0x3d, 0x44, 0x06, 0xec, 0x6f, 0xcb, 0xd6, 0xf1, 0xc1, 0x2b, 0x30, - 0xf0, 0x03, 0x86, 0x32, 0x9a, 0x4c, 0xd4, 0x42, 0x14, 0x70, 0x4f, 0x33, 0xe7, 0xe0, 0x6f, 0xcb, 0x37, 0x67, 0xf6, - 0x9b, 0xbc, 0x71, 0x08, 0x8c, 0xb1, 0xb0, 0x56, 0xe2, 0x7c, 0xb1, 0x04, 0xaf, 0x18, 0xd1, 0xc4, 0x0b, 0x9b, 0x87, - 0x73, 0x59, 0xda, 0xe2, 0x0b, 0xc6, 0xc6, 0xc0, 0x70, 0x1b, 0x1b, 0xa5, 0xd7, 0x01, 0xbb, 0x61, 0xb9, 0x25, 0xd4, - 0xe6, 0xc7, 0x6a, 0x5a, 0x60, 0xa8, 0x56, 0xae, 0x7b, 0xe4, 0x5c, 0x9d, 0x34, 0xa4, 0x01, 0x8e, 0x81, 0x8f, 0x5c, - 0x3e, 0x62, 0x95, 0x23, 0x35, 0x30, 0x54, 0x09, 0x80, 0x46, 0xc8, 0x4e, 0x1b, 0xca, 0xbb, 0x80, 0xa8, 0x1b, 0x60, - 0x33, 0x1c, 0xbd, 0x0b, 0xa9, 0x2d, 0xf8, 0x3c, 0x05, 0x70, 0xf2, 0xb4, 0x42, 0x6a, 0xd2, 0x34, 0x63, 0x75, 0xa2, - 0x36, 0x95, 0x84, 0x34, 0xc2, 0x39, 0x00, 0xbd, 0x64, 0x84, 0xb8, 0xaa, 0x76, 0x6d, 0x94, 0xf2, 0xc8, 0x87, 0x98, - 0xf8, 0x3d, 0x64, 0x49, 0xd2, 0x38, 0x61, 0xf9, 0xa2, 0x1b, 0x6a, 0x51, 0xbb, 0x3c, 0x1f, 0x45, 0xb9, 0x61, 0x1b, - 0xc0, 0x12, 0xe0, 0x00, 0xab, 0xdf, 0x42, 0xf2, 0x72, 0x3d, 0xe7, 0xe6, 0x9d, 0xf1, 0x74, 0xa8, 0x72, 0xd3, 0xbb, - 0xa6, 0xf7, 0x2b, 0x95, 0x03, 0x55, 0x22, 0xd3, 0xb5, 0xa0, 0x69, 0x25, 0xd4, 0xbb, 0x21, 0x55, 0xc2, 0x0e, 0x04, - 0x4c, 0x15, 0xfc, 0xca, 0x26, 0x13, 0x36, 0x4a, 0x13, 0x5d, 0xc8, 0x98, 0xf2, 0x60, 0xeb, 0xe0, 0x64, 0xbb, 0xe7, - 0xaa, 0x3f, 0x41, 0xc8, 0x19, 0x11, 0x93, 0x90, 0x03, 0x24, 0xee, 0x4c, 0xf5, 0xd3, 0x44, 0x3d, 0x96, 0xa7, 0x88, - 0x7f, 0x05, 0xa4, 0xd0, 0x35, 0xe5, 0x08, 0x1a, 0xa7, 0x3f, 0xc5, 0xbe, 0x88, 0x72, 0x23, 0xd0, 0xed, 0xa8, 0x68, - 0xdb, 0xf1, 0x5d, 0x3b, 0x6f, 0x0e, 0x1d, 0x3b, 0x53, 0x0d, 0x70, 0x75, 0xfe, 0x58, 0xd9, 0xc6, 0x44, 0xa0, 0x5c, - 0xf5, 0xfc, 0xed, 0xab, 0x3f, 0x9f, 0xbd, 0xde, 0x15, 0x23, 0x60, 0x97, 0x6d, 0xe8, 0x72, 0x19, 0x6e, 0xe9, 0xf4, - 0x97, 0x9f, 0x1e, 0xd6, 0x6d, 0xcb, 0x79, 0xe1, 0xa8, 0x06, 0x59, 0xa7, 0x4b, 0x78, 0x71, 0x14, 0xdd, 0xb0, 0xf8, - 0xb3, 0xa7, 0x41, 0xee, 0xbc, 0x1e, 0xdc, 0xb7, 0x3f, 0x9f, 0xfd, 0xb4, 0x33, 0xa8, 0x47, 0x8e, 0x0d, 0xb8, 0x3d, - 0x8d, 0x16, 0x0f, 0x18, 0x5d, 0x5b, 0x35, 0xd4, 0x51, 0x10, 0x25, 0xac, 0x01, 0x82, 0x57, 0xe7, 0x6f, 0xdf, 0xe3, - 0x74, 0x15, 0x2c, 0x08, 0x75, 0xf5, 0x79, 0x83, 0xff, 0xf9, 0xdd, 0xd9, 0xfb, 0xf7, 0xaa, 0x81, 0xc9, 0xba, 0x13, - 0xb9, 0x77, 0xbe, 0x89, 0xef, 0xa1, 0x38, 0xb5, 0x7b, 0x9d, 0xa8, 0x1a, 0x5d, 0xa4, 0xcb, 0xa3, 0xa1, 0xb2, 0x8d, - 0x6d, 0xce, 0xa9, 0x1d, 0xff, 0x32, 0xdd, 0x7e, 0x77, 0x1a, 0x57, 0x0d, 0x3e, 0xda, 0x4e, 0x52, 0x4b, 0x25, 0x73, - 0x3f, 0xbc, 0xae, 0x29, 0xf5, 0x6e, 0x6b, 0x4a, 0xe1, 0xfa, 0xb8, 0x81, 0x1f, 0x97, 0xd1, 0x5c, 0x62, 0x47, 0xd8, - 0xed, 0xfd, 0xd3, 0x25, 0xdd, 0xe1, 0x3e, 0x03, 0x68, 0x9e, 0x6c, 0xa5, 0x0a, 0x75, 0x4d, 0x31, 0xbf, 0x78, 0xe5, - 0x73, 0x3b, 0x0a, 0xc0, 0x26, 0x9f, 0xc9, 0x6a, 0xc8, 0x32, 0xab, 0xca, 0x3d, 0x6a, 0xdc, 0xca, 0xad, 0x80, 0x9a, - 0x91, 0xea, 0x86, 0xd3, 0x94, 0x85, 0x37, 0x06, 0x43, 0x77, 0x73, 0x18, 0xa5, 0x69, 0x34, 0xef, 0x3a, 0xf6, 0xe2, - 0x56, 0x55, 0x7a, 0x42, 0xd8, 0xc1, 0xed, 0xf0, 0xbb, 0xff, 0xfa, 0x67, 0x05, 0xcd, 0x53, 0xf9, 0x75, 0xca, 0xe6, - 0x0b, 0x16, 0x7b, 0xe9, 0x32, 0x66, 0x99, 0xf2, 0xaf, 0xff, 0x79, 0x55, 0xb9, 0xd8, 0xf7, 0xe4, 0x36, 0xc4, 0xd2, - 0xcb, 0x4d, 0xae, 0x83, 0x68, 0xb5, 0x57, 0x78, 0xdc, 0xdd, 0x53, 0x79, 0xe6, 0x4f, 0x67, 0x79, 0xed, 0xd3, 0x74, - 0xcb, 0xd8, 0x04, 0xf4, 0xa4, 0x0f, 0x50, 0xce, 0xa3, 0x55, 0xf7, 0x5f, 0xff, 0xcc, 0x05, 0x36, 0xf7, 0xee, 0xba, - 0x7a, 0x40, 0xcb, 0x2b, 0x5a, 0x5f, 0x67, 0x63, 0x89, 0xe1, 0xfd, 0xc6, 0x02, 0x6f, 0x14, 0xd2, 0xae, 0xdc, 0xd4, - 0xcd, 0x6d, 0x19, 0xd3, 0x77, 0xfe, 0x74, 0xf6, 0xb9, 0x83, 0x82, 0x09, 0xbd, 0x77, 0x54, 0x50, 0xe9, 0x0b, 0x0c, - 0x6b, 0xd0, 0xdd, 0x7d, 0xc1, 0x3e, 0x73, 0x5c, 0xf7, 0x0d, 0xe9, 0x4b, 0x8c, 0x86, 0x4b, 0x6e, 0xdf, 0x0f, 0x06, - 0x79, 0xb2, 0x5a, 0xb9, 0x3d, 0xf8, 0x0c, 0x9e, 0x6e, 0x94, 0x70, 0xf6, 0xa2, 0x6b, 0xeb, 0x14, 0xcc, 0x67, 0x87, - 0x09, 0x41, 0xeb, 0xf7, 0x9a, 0xe9, 0x68, 0xc6, 0xd7, 0xe4, 0xc4, 0xb6, 0xf1, 0xed, 0x0d, 0x64, 0x0d, 0xa5, 0x98, - 0xe8, 0x34, 0xd7, 0x1a, 0x1a, 0xf5, 0xe0, 0xac, 0x62, 0x6f, 0x41, 0x4a, 0x02, 0x05, 0x35, 0x26, 0x20, 0x74, 0xa9, - 0xdc, 0xa2, 0x6f, 0xbc, 0xe0, 0x66, 0xb7, 0x0b, 0x55, 0x33, 0x05, 0x43, 0xd2, 0xfc, 0xef, 0x23, 0xde, 0x48, 0x97, - 0x1f, 0x4c, 0xbb, 0x57, 0x5e, 0xca, 0xe2, 0xeb, 0x19, 0x78, 0xfb, 0x0a, 0xe9, 0x01, 0xc4, 0xd1, 0xdd, 0x86, 0x94, - 0x4b, 0x6c, 0x69, 0x0d, 0x1a, 0x2d, 0x30, 0xdc, 0x6f, 0xc3, 0xdd, 0x5f, 0x08, 0x73, 0x77, 0xcf, 0xc0, 0x1f, 0xf3, - 0x77, 0xc3, 0xde, 0xdb, 0x28, 0xd3, 0xff, 0x63, 0xef, 0xff, 0x44, 0xec, 0xbd, 0xf5, 0x3b, 0xbf, 0x65, 0x61, 0xff, - 0x0f, 0x60, 0xf9, 0x2e, 0x73, 0xcf, 0x38, 0xa6, 0xd7, 0x34, 0xcf, 0xd5, 0xe2, 0xd2, 0xe1, 0x45, 0xbc, 0xba, 0xa1, - 0x60, 0xe5, 0xc1, 0xd6, 0xb8, 0xe5, 0xa0, 0x87, 0xc8, 0x7e, 0xcb, 0x51, 0xfe, 0xfd, 0x11, 0x7d, 0x42, 0x19, 0xaa, - 0x24, 0x4c, 0xdf, 0x3d, 0x33, 0x92, 0xd2, 0x48, 0xbc, 0x95, 0x77, 0xb7, 0x0b, 0xde, 0x11, 0xc0, 0x7e, 0xb3, 0xf2, - 0xee, 0xea, 0x80, 0x6d, 0x44, 0xaf, 0xd5, 0x8f, 0x9d, 0x82, 0x97, 0x4f, 0x17, 0x5d, 0x7c, 0x8c, 0x41, 0xc2, 0xd2, - 0x53, 0x28, 0x74, 0x1f, 0xaf, 0xf7, 0xaa, 0x15, 0xb3, 0x01, 0xf8, 0x3f, 0x4b, 0x80, 0x47, 0x25, 0xc0, 0xfd, 0xe4, - 0x3a, 0x0a, 0x1f, 0x02, 0xf9, 0xcf, 0x20, 0xfc, 0xf9, 0xcd, 0xa0, 0xe3, 0xe7, 0x36, 0x60, 0xc7, 0xd2, 0x2a, 0xf0, - 0x58, 0x58, 0x85, 0xbe, 0x57, 0x2f, 0xab, 0xaf, 0x10, 0x5a, 0xa4, 0xb1, 0x8c, 0x08, 0xad, 0x02, 0x7a, 0x15, 0x05, - 0x74, 0x5c, 0x15, 0x92, 0xeb, 0x87, 0x93, 0xd8, 0x8b, 0xd9, 0xb8, 0xf9, 0x0a, 0x50, 0xb2, 0x4e, 0xbe, 0xb3, 0x92, - 0xe5, 0x62, 0x11, 0xc5, 0x69, 0x72, 0x8d, 0x71, 0x5a, 0xe6, 0x3e, 0x5c, 0x28, 0x20, 0xa3, 0x58, 0x1e, 0xb5, 0xf7, - 0xac, 0x4e, 0xbe, 0x6d, 0x30, 0xb7, 0x9c, 0x6c, 0x83, 0x7b, 0xdf, 0x18, 0xdc, 0x7f, 0x67, 0x26, 0xe9, 0x2f, 0x66, - 0x56, 0x1a, 0xfb, 0x73, 0x4d, 0x37, 0x1c, 0x5b, 0xd7, 0x85, 0x7c, 0x65, 0xe6, 0xf6, 0xf7, 0x28, 0xda, 0xf0, 0x4c, - 0x87, 0xa8, 0x85, 0xe8, 0xd1, 0x02, 0xb6, 0x72, 0x2f, 0x97, 0x93, 0x09, 0x8b, 0x35, 0x11, 0x96, 0x11, 0xe2, 0xc2, - 0x92, 0x31, 0x20, 0xf8, 0x39, 0x7e, 0xf0, 0xd9, 0x0a, 0xf2, 0x3f, 0x15, 0x61, 0xd5, 0xc1, 0xd7, 0x93, 0xcc, 0xc9, - 0x21, 0xb7, 0x5c, 0xda, 0x6e, 0x69, 0xe3, 0x67, 0x07, 0xc6, 0x0c, 0x82, 0x31, 0x15, 0xee, 0xf1, 0x18, 0xe7, 0xcf, - 0x0f, 0xd3, 0x0e, 0x7e, 0x01, 0x3a, 0x80, 0xc3, 0x1b, 0xb8, 0xb9, 0x5f, 0x94, 0x32, 0xca, 0x3b, 0x9c, 0xb9, 0xfd, - 0xe0, 0xb9, 0x4b, 0x7a, 0x1e, 0xb4, 0xdb, 0x7b, 0x35, 0xf3, 0xe2, 0x57, 0xd1, 0x98, 0x21, 0xa0, 0xc3, 0x34, 0x02, - 0x6f, 0x4d, 0x29, 0x0c, 0x0f, 0x46, 0xe1, 0x31, 0x4b, 0x91, 0x79, 0xf6, 0xa1, 0xe8, 0x5a, 0x2e, 0x72, 0x9f, 0x3f, - 0xde, 0x37, 0xe0, 0xa4, 0xd5, 0xaf, 0xb4, 0x58, 0x34, 0xbe, 0xd4, 0xb5, 0xaf, 0xe4, 0xdd, 0xfa, 0xca, 0x8b, 0x63, - 0x9f, 0xc5, 0x8a, 0xf6, 0xdd, 0xaf, 0xba, 0xbc, 0x69, 0x4b, 0x0a, 0x1d, 0xae, 0x65, 0x56, 0x30, 0x1a, 0xdd, 0xc4, - 0x67, 0xc1, 0xd8, 0x55, 0x47, 0xd4, 0x30, 0x57, 0xde, 0xb4, 0x3b, 0xb6, 0x6d, 0x73, 0x85, 0xa9, 0x43, 0x3f, 0x41, - 0x61, 0x0a, 0x3f, 0xe1, 0xa1, 0x24, 0x5e, 0xec, 0x10, 0x17, 0xb1, 0x41, 0xce, 0x6a, 0x21, 0x7c, 0x47, 0xf1, 0x7c, - 0x1e, 0x02, 0x1b, 0x8f, 0xfb, 0x23, 0x40, 0x73, 0x04, 0x58, 0x05, 0x4c, 0x15, 0x80, 0x0e, 0x1f, 0x02, 0xd0, 0x85, - 0x3f, 0xf7, 0xc3, 0x69, 0xd2, 0x08, 0x11, 0xaa, 0x4d, 0x4b, 0xf0, 0xa4, 0xd4, 0x42, 0x55, 0x70, 0x0d, 0x67, 0x51, - 0x00, 0x79, 0x88, 0x54, 0x66, 0x4d, 0x2d, 0xe5, 0x85, 0x6d, 0xdb, 0x86, 0x79, 0x00, 0x19, 0xff, 0x0e, 0x8f, 0x6c, - 0xc3, 0x84, 0xbf, 0x2c, 0xcb, 0xaa, 0x91, 0xc7, 0xf6, 0xe6, 0x7e, 0x68, 0xd2, 0x63, 0xcb, 0xde, 0x0d, 0xde, 0x7b, - 0xad, 0x7a, 0x13, 0xae, 0x1b, 0x1b, 0xe6, 0xae, 0xa3, 0xda, 0xd0, 0x4d, 0xca, 0xb6, 0x6e, 0x16, 0x05, 0x5c, 0xe2, - 0xf1, 0x30, 0x2a, 0xc4, 0x68, 0x58, 0x7e, 0x8b, 0x6c, 0x69, 0x5c, 0xcd, 0x63, 0xa1, 0x7e, 0xcf, 0xc1, 0xea, 0x2a, - 0xaf, 0xa2, 0x65, 0x30, 0x46, 0x73, 0x28, 0xb0, 0x5d, 0x56, 0x0a, 0xab, 0xd0, 0x4a, 0xb2, 0x29, 0xc8, 0x25, 0x8e, - 0x89, 0xd6, 0xde, 0x23, 0x71, 0x8a, 0x62, 0xed, 0x29, 0x4e, 0xf1, 0x65, 0xdd, 0x16, 0xbc, 0x7a, 0x0a, 0xf1, 0x84, - 0x76, 0x68, 0xc0, 0xf7, 0x05, 0xd4, 0x0f, 0x76, 0xa9, 0x2f, 0xd6, 0xed, 0xea, 0x29, 0x05, 0x9d, 0xf5, 0x3e, 0x7d, - 0xda, 0x1b, 0x7d, 0xfa, 0xb4, 0xb7, 0x91, 0xa9, 0xa3, 0x79, 0x84, 0xb4, 0x31, 0x18, 0x0f, 0x31, 0x02, 0x71, 0x83, - 0x08, 0xe8, 0xef, 0xa1, 0xbc, 0xeb, 0xf1, 0x68, 0x55, 0xf4, 0x34, 0x32, 0xf8, 0x07, 0xe9, 0x31, 0xc8, 0x2a, 0x93, - 0x32, 0x73, 0x3d, 0x12, 0xf3, 0x7c, 0xfa, 0xc4, 0x8f, 0x9b, 0x31, 0x76, 0x47, 0x79, 0x91, 0xa3, 0x1a, 0x4b, 0x37, - 0xc8, 0x1f, 0x55, 0x04, 0x79, 0xc9, 0x31, 0x66, 0x01, 0xf1, 0xca, 0x8b, 0x43, 0x19, 0xe0, 0x9f, 0x22, 0x85, 0x7f, - 0x56, 0xe1, 0x11, 0x51, 0xc7, 0xd5, 0xd5, 0x98, 0xb8, 0x4c, 0x5b, 0x12, 0x0e, 0x14, 0x96, 0x6e, 0x52, 0x07, 0x17, - 0x02, 0xdb, 0x63, 0x5a, 0x54, 0x31, 0x40, 0xf4, 0xaf, 0xc6, 0x93, 0x3b, 0x16, 0xc3, 0x7a, 0xe7, 0xad, 0xba, 0x4b, - 0xf1, 0x70, 0x46, 0x26, 0xf1, 0xdd, 0x49, 0xee, 0xb7, 0xbc, 0x20, 0xaf, 0xc3, 0xa9, 0xfb, 0x6d, 0xac, 0x2d, 0x8c, - 0xd4, 0x50, 0x05, 0x19, 0x51, 0x75, 0x63, 0x5e, 0x17, 0x60, 0xb5, 0x37, 0xe7, 0xe1, 0x66, 0x34, 0xb1, 0x15, 0xae, - 0x27, 0xe8, 0xab, 0x10, 0x8e, 0xee, 0x30, 0x80, 0x72, 0xf1, 0x9e, 0x40, 0xb9, 0xe6, 0xd9, 0xf7, 0xc6, 0xf2, 0x2b, - 0x58, 0x70, 0xd5, 0x98, 0xe8, 0x06, 0xf9, 0x00, 0x4c, 0xbf, 0xa4, 0xb9, 0x3f, 0xc5, 0x54, 0x9e, 0x4b, 0xe1, 0x5e, - 0x85, 0x03, 0xc0, 0x75, 0xc5, 0x01, 0xa0, 0x66, 0x3e, 0x95, 0x98, 0x25, 0x8b, 0x28, 0x84, 0xbb, 0xe2, 0x75, 0xe1, - 0xe1, 0x75, 0xbd, 0xe9, 0xe1, 0x55, 0xd3, 0x14, 0xdf, 0x50, 0x3b, 0x50, 0x49, 0x5f, 0xfc, 0x57, 0xc5, 0x42, 0x5f, - 0x90, 0x7a, 0xcc, 0x52, 0x7e, 0xd6, 0xe4, 0xd9, 0xfd, 0xfd, 0xfd, 0x9e, 0xdd, 0xe7, 0x3b, 0x79, 0x76, 0x7f, 0xff, - 0xc5, 0x3d, 0xbb, 0xcf, 0x64, 0xcf, 0x6e, 0x20, 0xc1, 0x67, 0x6c, 0x27, 0x47, 0x5a, 0xe1, 0xd2, 0x12, 0xad, 0x12, - 0xd7, 0xe1, 0x9a, 0xb5, 0x64, 0x34, 0x63, 0x60, 0xaa, 0xc0, 0x59, 0xdd, 0x20, 0x9a, 0x82, 0xbf, 0x6b, 0xb3, 0x47, - 0xeb, 0x97, 0xf2, 0x67, 0x0d, 0xa2, 0xa9, 0x2a, 0xe5, 0x69, 0x0b, 0x45, 0x9e, 0x36, 0x88, 0x4d, 0xf7, 0xb7, 0x5b, - 0xe7, 0xe5, 0xa5, 0xd3, 0x6b, 0x3b, 0x10, 0xe7, 0x14, 0xb4, 0xcf, 0x58, 0x60, 0xf7, 0xda, 0x6d, 0x28, 0x58, 0x49, - 0x05, 0x2d, 0x28, 0xf0, 0xa5, 0x82, 0x43, 0x28, 0x18, 0x49, 0x05, 0x47, 0x50, 0x30, 0x96, 0x0a, 0x8e, 0xa1, 0xe0, - 0x46, 0xcd, 0x2e, 0xc3, 0xdc, 0x6f, 0xfd, 0x58, 0xbf, 0x2a, 0xa5, 0xe8, 0xcc, 0x4d, 0x25, 0x44, 0x95, 0x63, 0x43, - 0xe4, 0x8b, 0x30, 0x0f, 0x74, 0xce, 0xa3, 0x0d, 0xbe, 0x1a, 0x00, 0xe6, 0x05, 0xcb, 0x11, 0x03, 0xec, 0x6e, 0xa8, - 0x66, 0x5b, 0xbc, 0x56, 0xbb, 0xb9, 0x9f, 0xb7, 0x6d, 0xb4, 0x84, 0xdf, 0x74, 0x17, 0xa3, 0x78, 0x88, 0xca, 0x87, - 0xcf, 0x67, 0x79, 0xf0, 0xe8, 0xa5, 0x5b, 0x04, 0xc3, 0x69, 0x43, 0x0a, 0x1d, 0xce, 0xab, 0x31, 0x0d, 0xec, 0x65, - 0x20, 0xd6, 0x89, 0x38, 0x45, 0xe2, 0x03, 0x0a, 0x3a, 0xc3, 0xf7, 0xbc, 0x82, 0x87, 0xe3, 0xa1, 0xd6, 0x09, 0xfa, - 0x79, 0x1e, 0xc1, 0x9a, 0x74, 0xa9, 0x4b, 0x23, 0xf5, 0xa6, 0xdd, 0x99, 0x41, 0x86, 0x54, 0xdd, 0x29, 0xa4, 0x24, - 0x39, 0x1d, 0x77, 0x17, 0xc6, 0x6a, 0xc6, 0xc2, 0xee, 0x84, 0xbb, 0x1d, 0xc2, 0xfa, 0x93, 0x27, 0xc9, 0x5c, 0x17, - 0x2e, 0x50, 0xb8, 0x27, 0x8a, 0xb7, 0x04, 0xa5, 0x99, 0x6f, 0xa5, 0xc2, 0x7b, 0x47, 0x93, 0x8d, 0xac, 0xbe, 0x84, - 0xaf, 0xc5, 0x6b, 0x36, 0x5c, 0x4e, 0x95, 0xf3, 0x68, 0x7a, 0xaf, 0x5f, 0x85, 0xfc, 0x0a, 0xa0, 0x54, 0xc9, 0x9a, - 0xd4, 0x14, 0xdb, 0x9b, 0x7f, 0x8b, 0x1e, 0xb3, 0x72, 0xfd, 0x14, 0x60, 0x53, 0x52, 0x62, 0x1b, 0xe0, 0x3b, 0x30, - 0xdb, 0x92, 0xe7, 0xc2, 0x39, 0xcc, 0x9f, 0xf4, 0x7c, 0xe1, 0x49, 0xf0, 0xf4, 0x7f, 0x64, 0x49, 0xe2, 0x4d, 0x99, - 0x8c, 0x5a, 0x4a, 0x9d, 0x03, 0x16, 0xcc, 0xd5, 0xc9, 0x38, 0x81, 0xc0, 0xd8, 0xfb, 0x1b, 0xfe, 0x28, 0xe0, 0x32, - 0x0b, 0x7e, 0x5a, 0xb0, 0x68, 0x85, 0xf3, 0x86, 0x6f, 0xc1, 0xf2, 0x94, 0xfd, 0x28, 0x00, 0x89, 0xdc, 0xb0, 0xa0, - 0x5a, 0x98, 0x7a, 0xd3, 0x6a, 0x11, 0xad, 0x75, 0x56, 0x42, 0x7b, 0x7a, 0xe9, 0x51, 0xe0, 0xc2, 0xcf, 0xb0, 0xcb, - 0x0f, 0xa2, 0xe9, 0xef, 0x6a, 0x94, 0xbf, 0xc5, 0x99, 0xe2, 0xc7, 0xd0, 0x08, 0xd3, 0x81, 0x85, 0x73, 0xac, 0x58, - 0x30, 0x85, 0xdd, 0x30, 0x9d, 0x99, 0x18, 0x58, 0x4e, 0x6b, 0x85, 0xba, 0x61, 0xe1, 0xda, 0xae, 0xab, 0xe1, 0x34, - 0xbb, 0xf1, 0x74, 0xe8, 0x69, 0x4e, 0xeb, 0xd8, 0x10, 0x7f, 0x2c, 0xfb, 0x50, 0xcf, 0xb0, 0x07, 0x65, 0xec, 0xdf, - 0xac, 0x27, 0x51, 0x98, 0x9a, 0x13, 0x6f, 0xee, 0x07, 0x77, 0xdd, 0x79, 0x14, 0x46, 0xc9, 0xc2, 0x1b, 0xb1, 0x9e, - 0xc4, 0x8f, 0x62, 0xa0, 0x66, 0x1e, 0x2b, 0xd0, 0xb1, 0x5a, 0x31, 0x9b, 0x53, 0xeb, 0x3c, 0x0e, 0xf3, 0x24, 0x60, - 0xb7, 0x19, 0xff, 0x7c, 0xa9, 0x32, 0x55, 0xc5, 0x2d, 0x47, 0x2d, 0x80, 0x65, 0xe6, 0x41, 0x9e, 0x21, 0xb5, 0x41, - 0x8f, 0x4b, 0x1d, 0xbb, 0x56, 0xeb, 0x30, 0x66, 0x73, 0xc5, 0x3a, 0x6c, 0xec, 0x3c, 0x8e, 0x56, 0x7d, 0x80, 0x16, - 0x1b, 0x9b, 0x09, 0x0b, 0x26, 0xf8, 0xc6, 0xc4, 0xb8, 0x52, 0xa2, 0x1f, 0x13, 0xed, 0x0a, 0xa0, 0x37, 0x36, 0xef, - 0xc1, 0xeb, 0x6e, 0x4b, 0xb1, 0x25, 0x7e, 0xfa, 0xd8, 0x5e, 0x48, 0x7d, 0xc9, 0xf3, 0xa7, 0xaf, 0xb1, 0xba, 0xa3, - 0xd8, 0x3d, 0xd0, 0x1f, 0x4f, 0x82, 0x68, 0xd5, 0x9d, 0xf9, 0xe3, 0x31, 0x0b, 0x7b, 0x08, 0x73, 0x5e, 0xc8, 0x82, - 0xc0, 0x5f, 0x24, 0x7e, 0xd2, 0x9b, 0x7b, 0xb7, 0xbc, 0xd7, 0x83, 0xa6, 0x5e, 0xdb, 0xbc, 0xd7, 0xf6, 0xce, 0xbd, - 0x4a, 0xdd, 0x40, 0x0c, 0x2b, 0xea, 0x87, 0x83, 0x76, 0xa8, 0xd8, 0x95, 0x71, 0xee, 0xdc, 0xeb, 0x22, 0x66, 0xeb, - 0xb9, 0x17, 0x4f, 0xfd, 0xb0, 0x6b, 0x67, 0xd6, 0xcd, 0x9a, 0x36, 0xc6, 0xa3, 0x4e, 0xa7, 0x93, 0x59, 0x63, 0xf1, - 0x64, 0x8f, 0xc7, 0x99, 0x35, 0x12, 0x4f, 0x93, 0x89, 0x6d, 0x4f, 0x26, 0x99, 0xe5, 0x8b, 0x82, 0x76, 0x6b, 0x34, - 0x6e, 0xb7, 0x32, 0x6b, 0x25, 0xd5, 0xc8, 0x2c, 0xc6, 0x9f, 0x62, 0x36, 0xee, 0xe1, 0x46, 0xe2, 0xfe, 0xcf, 0xc7, - 0xb6, 0x9d, 0x21, 0x06, 0xb8, 0x2c, 0xe1, 0x26, 0x34, 0x5d, 0xb9, 0x5a, 0xef, 0x5c, 0x53, 0x29, 0x3e, 0x37, 0x1a, - 0xd5, 0xd6, 0x1b, 0x7b, 0xf1, 0xc7, 0x2b, 0x45, 0x1a, 0x85, 0xe7, 0x51, 0xb5, 0xb5, 0x98, 0x06, 0xf3, 0xb6, 0x0b, - 0x09, 0x3b, 0x7a, 0xc3, 0x28, 0x86, 0x33, 0x1b, 0x7b, 0x63, 0x7f, 0x99, 0x74, 0x9d, 0xd6, 0xe2, 0x56, 0x14, 0xf1, - 0xbd, 0x5e, 0x14, 0xe0, 0xd9, 0xeb, 0x26, 0x51, 0xe0, 0x8f, 0x45, 0x51, 0xd3, 0x59, 0x72, 0x5a, 0x7a, 0x0f, 0xf9, - 0x57, 0x1f, 0x83, 0x2e, 0x7b, 0x41, 0xa0, 0x58, 0xed, 0x44, 0x61, 0x5e, 0x82, 0xe6, 0x72, 0x8a, 0x9d, 0xd0, 0xbc, - 0x60, 0x68, 0x5a, 0xe7, 0x60, 0x71, 0x9b, 0xef, 0x79, 0xe7, 0x68, 0x71, 0x9b, 0x7d, 0x3d, 0x67, 0x63, 0xdf, 0x53, - 0xb4, 0x62, 0x37, 0x39, 0x36, 0x98, 0xd4, 0xe9, 0xeb, 0x86, 0x6d, 0x2a, 0x8e, 0x05, 0x24, 0x36, 0xda, 0xf3, 0xe7, - 0x20, 0x87, 0xf1, 0xc2, 0x34, 0xcb, 0x06, 0x57, 0x59, 0xd6, 0x3b, 0xf7, 0xb5, 0xcb, 0xff, 0xd6, 0x88, 0x16, 0x92, - 0x09, 0x6a, 0xa6, 0x5f, 0x19, 0x67, 0x4c, 0x76, 0x97, 0x01, 0x32, 0x86, 0xae, 0x32, 0x72, 0x65, 0xa2, 0xb7, 0x9b, - 0x95, 0x69, 0x92, 0xf3, 0xea, 0xe4, 0x7d, 0x53, 0xae, 0x82, 0x14, 0x08, 0x2a, 0x9c, 0x31, 0xf7, 0x5c, 0xf2, 0xbd, - 0x01, 0xa6, 0x07, 0x2b, 0x53, 0x54, 0xa1, 0xd7, 0x4d, 0xbc, 0xe7, 0xc5, 0xfd, 0xbc, 0xe7, 0x5f, 0xd3, 0x5d, 0x78, - 0xcf, 0x8b, 0x2f, 0xce, 0x7b, 0xbe, 0xde, 0x8c, 0x2a, 0x74, 0x11, 0xb9, 0x6a, 0x6e, 0x30, 0x09, 0xa4, 0x29, 0xa6, - 0x78, 0xfd, 0xaf, 0xd3, 0xdf, 0x1a, 0xde, 0x45, 0xf4, 0x86, 0x44, 0x81, 0xf3, 0xa9, 0x20, 0x66, 0x7d, 0x1b, 0xba, - 0x7f, 0x8e, 0xe5, 0xe7, 0xc9, 0xc4, 0x7d, 0x1d, 0x49, 0x05, 0xf9, 0x13, 0xf7, 0x25, 0x29, 0xc5, 0x56, 0xa6, 0x37, - 0xb9, 0xb7, 0x0f, 0x64, 0x9f, 0x86, 0xd0, 0xac, 0xe4, 0xda, 0x3d, 0xce, 0x7d, 0xee, 0x7a, 0x65, 0x10, 0xb4, 0xdc, - 0xc9, 0x55, 0x04, 0xe0, 0xda, 0xb0, 0x8c, 0x9a, 0x32, 0x21, 0x03, 0x78, 0x79, 0xf7, 0xfd, 0x58, 0xbb, 0x88, 0xf4, - 0xcc, 0x4f, 0xde, 0x56, 0xc3, 0x5f, 0x09, 0x3d, 0x97, 0x3c, 0x9c, 0x8c, 0xfb, 0xcd, 0x49, 0x51, 0x6e, 0xf1, 0x35, - 0x35, 0x3f, 0x2d, 0x8d, 0xb4, 0x2b, 0x37, 0xec, 0x51, 0xcc, 0xef, 0x0d, 0x62, 0xcc, 0xc3, 0xc4, 0xac, 0x39, 0x97, - 0xb7, 0xc6, 0x67, 0x88, 0x1a, 0x3a, 0xa6, 0xe6, 0xfe, 0x38, 0xcb, 0xf4, 0x9e, 0x98, 0x08, 0x89, 0xd0, 0xb2, 0xfb, - 0x98, 0xb8, 0xa4, 0x10, 0x02, 0x71, 0x89, 0x0f, 0x59, 0x33, 0x5f, 0x80, 0x7f, 0x00, 0xb7, 0x7d, 0xe6, 0x73, 0xa6, - 0x2a, 0x34, 0x7d, 0xe4, 0x37, 0x22, 0x0d, 0x08, 0x0c, 0xda, 0x65, 0x6f, 0xab, 0xd2, 0x82, 0x6c, 0x3a, 0xb6, 0xd2, - 0xe4, 0xa0, 0x83, 0x03, 0xc4, 0xf8, 0x15, 0x62, 0x21, 0x42, 0x3b, 0xbc, 0x0e, 0x3e, 0x64, 0x6a, 0xce, 0xfb, 0xe1, - 0xf6, 0xeb, 0x9f, 0xec, 0x43, 0x83, 0x7e, 0x45, 0xe9, 0x76, 0x8f, 0x5f, 0x26, 0xb0, 0x12, 0xc9, 0xca, 0xb0, 0x92, - 0x95, 0xf2, 0x6c, 0x2d, 0xe2, 0x63, 0xa7, 0xde, 0xc2, 0x04, 0x2d, 0x0f, 0xe2, 0x5e, 0x8e, 0xf1, 0xa4, 0x50, 0xdc, - 0xbd, 0x65, 0x02, 0xb8, 0x11, 0xe5, 0x28, 0x88, 0x7f, 0x7a, 0xa3, 0x65, 0x9c, 0x44, 0x71, 0x77, 0x11, 0xf9, 0x61, - 0xca, 0xe2, 0x8c, 0x04, 0x2b, 0x38, 0x3f, 0x62, 0x7a, 0xae, 0xd6, 0xd1, 0xc2, 0x1b, 0xf9, 0xe9, 0x5d, 0xd7, 0xe6, - 0x2c, 0x85, 0xdd, 0xe3, 0xdc, 0x81, 0x5d, 0x5b, 0xbf, 0xcb, 0x67, 0xf3, 0x39, 0x32, 0x7e, 0xf1, 0x26, 0x3b, 0x23, - 0x6f, 0xf3, 0x9e, 0xf4, 0x96, 0x22, 0x84, 0x03, 0xfb, 0xe1, 0xc5, 0xe6, 0x14, 0xb0, 0x3c, 0x2c, 0xb5, 0x3d, 0x66, - 0x53, 0x03, 0xb1, 0x36, 0x98, 0x19, 0x8a, 0x3f, 0xd6, 0xa1, 0xae, 0xd8, 0xf5, 0xc5, 0xc0, 0xf1, 0xe8, 0xbb, 0x40, - 0xd6, 0xf5, 0x26, 0x29, 0x8b, 0x8d, 0x5d, 0x6a, 0x0e, 0xd9, 0x24, 0x8a, 0x19, 0x65, 0x93, 0x73, 0x3a, 0x8b, 0xdb, - 0xdd, 0xbb, 0xdf, 0x3e, 0xfc, 0xfa, 0x7e, 0xc2, 0x28, 0xd5, 0x44, 0x67, 0xfa, 0x3d, 0xbd, 0x6d, 0xd2, 0x33, 0x60, - 0x0d, 0x69, 0xe6, 0x47, 0x24, 0x05, 0x81, 0x48, 0x60, 0xb5, 0x49, 0x3b, 0x16, 0x11, 0xa7, 0x79, 0x31, 0x0b, 0xbc, - 0xd4, 0xbf, 0x11, 0x3c, 0x63, 0xfb, 0x68, 0x71, 0x2b, 0xd6, 0x18, 0x09, 0xde, 0x03, 0x16, 0xa9, 0x02, 0x8a, 0x58, - 0xa4, 0x6a, 0x31, 0x2e, 0x52, 0x6f, 0x63, 0x34, 0x22, 0x8e, 0x75, 0x85, 0xd2, 0x1f, 0x2e, 0x6e, 0x65, 0x12, 0x5d, - 0x34, 0xcb, 0x29, 0x75, 0x35, 0x01, 0xc9, 0xdc, 0x1f, 0x8f, 0x03, 0x96, 0x95, 0x16, 0xba, 0xbc, 0x96, 0xd2, 0xe4, - 0xe4, 0xf3, 0xe0, 0x0d, 0x93, 0x28, 0x58, 0xa6, 0xac, 0x7e, 0xba, 0x84, 0x44, 0xb7, 0x98, 0x1c, 0xfc, 0x5d, 0x86, - 0xf5, 0x10, 0xd8, 0x6d, 0xd8, 0x26, 0x76, 0x0f, 0xf2, 0x0d, 0x9a, 0xed, 0x32, 0xe8, 0xf0, 0x2a, 0x07, 0xda, 0xa8, - 0x19, 0x88, 0x01, 0x64, 0x89, 0xb0, 0xb7, 0x62, 0x39, 0xbc, 0x2c, 0xcf, 0xb9, 0x96, 0x17, 0x65, 0xe5, 0xc1, 0xfc, - 0x3e, 0x67, 0xec, 0x45, 0xfd, 0x19, 0x7b, 0x21, 0xce, 0xd8, 0xf6, 0x9d, 0xf9, 0x68, 0xe2, 0xc0, 0x7f, 0xbd, 0x62, - 0x40, 0x5d, 0x5b, 0x69, 0x2f, 0x6e, 0x15, 0x67, 0x71, 0xab, 0x98, 0xad, 0xc5, 0xad, 0x82, 0x5d, 0xa3, 0x7b, 0x8b, - 0x61, 0xb5, 0x74, 0xc3, 0x56, 0xa0, 0x10, 0xfe, 0xd8, 0xa5, 0x57, 0xce, 0x01, 0xbc, 0x83, 0x56, 0x87, 0x9b, 0xef, - 0x5a, 0xdb, 0x8f, 0x3a, 0x9d, 0x25, 0x81, 0xb4, 0x75, 0x2b, 0xf5, 0x86, 0x43, 0x10, 0x65, 0x46, 0xa3, 0x65, 0xf2, - 0x0f, 0x0e, 0x3f, 0x9f, 0xc4, 0xad, 0x88, 0xa0, 0xd2, 0x8f, 0x68, 0x0a, 0x8a, 0xc2, 0x1b, 0x26, 0x7a, 0x58, 0xe7, - 0xeb, 0xd4, 0xa5, 0xe4, 0x88, 0x2d, 0xeb, 0xa0, 0x66, 0x93, 0xd7, 0x4f, 0xf4, 0xef, 0xb6, 0x4a, 0xcd, 0x28, 0xe6, - 0x33, 0xa6, 0x65, 0xeb, 0x74, 0x3c, 0x7c, 0x36, 0xf8, 0x6a, 0xda, 0x9d, 0x7a, 0x70, 0x2f, 0xc5, 0x97, 0xae, 0x04, - 0x51, 0xe1, 0x74, 0x8b, 0x87, 0xe2, 0xd8, 0xde, 0x6b, 0xd3, 0x1e, 0xd9, 0xe8, 0x75, 0x0b, 0x41, 0x28, 0xea, 0xee, - 0x88, 0xe5, 0x1f, 0xbd, 0x38, 0x80, 0xff, 0x88, 0xab, 0xff, 0x6b, 0x5a, 0xc7, 0xa8, 0xbf, 0x4e, 0x4b, 0x8c, 0x3a, - 0xb1, 0x4a, 0xc8, 0x88, 0xef, 0x5e, 0x7f, 0x32, 0x79, 0x58, 0x83, 0x9d, 0x6b, 0x93, 0x67, 0x58, 0xb5, 0xf6, 0xcb, - 0x28, 0x0a, 0x98, 0x17, 0x6e, 0x56, 0x17, 0xd3, 0x43, 0x6e, 0xfe, 0xa9, 0x0b, 0x8d, 0xc4, 0x3d, 0x82, 0x9c, 0x12, - 0x54, 0x6c, 0x43, 0x57, 0x89, 0xf3, 0xa6, 0xab, 0xc4, 0xbb, 0xfb, 0xaf, 0x12, 0x3f, 0xec, 0x74, 0x95, 0x78, 0xf7, - 0xc5, 0xaf, 0x12, 0xe7, 0x9b, 0x57, 0x89, 0xf3, 0x48, 0xb8, 0x03, 0x1b, 0x6f, 0x96, 0xfc, 0xe7, 0x07, 0xb2, 0xf7, - 0x7d, 0x17, 0xb9, 0x87, 0x36, 0x25, 0x3c, 0xbc, 0xf8, 0xcd, 0x17, 0x0b, 0xdc, 0x88, 0xef, 0xd0, 0x3b, 0xae, 0xb8, - 0x5a, 0x70, 0xcc, 0x8e, 0xdf, 0x91, 0x8a, 0x83, 0x28, 0x9c, 0xfe, 0x0c, 0xf6, 0xde, 0x20, 0x0e, 0x8c, 0xa5, 0x17, - 0x7e, 0xf2, 0x73, 0xb4, 0x58, 0x2e, 0x50, 0x51, 0xf5, 0xc1, 0x4f, 0xfc, 0x61, 0xc0, 0xf2, 0x08, 0x93, 0xa4, 0x75, - 0xe5, 0xb2, 0x75, 0x50, 0xbc, 0x8a, 0x9f, 0xde, 0xad, 0xf8, 0x89, 0x2e, 0xb6, 0xfc, 0x37, 0xb9, 0x09, 0xaa, 0xf5, - 0x17, 0x11, 0x61, 0x21, 0x26, 0x01, 0xfd, 0xf0, 0xcb, 0xc8, 0xb9, 0x88, 0xe5, 0x55, 0x1a, 0xa5, 0x70, 0xdf, 0x68, - 0xec, 0x87, 0x55, 0xfb, 0x79, 0xb3, 0xd4, 0x8d, 0x3c, 0x01, 0xc7, 0xa6, 0x38, 0x7f, 0x1e, 0x2d, 0x13, 0x36, 0x8e, - 0x56, 0xa1, 0x6a, 0x84, 0x5c, 0xaf, 0x1a, 0xa1, 0x4c, 0x3d, 0x6f, 0x53, 0x56, 0x38, 0xaa, 0xd6, 0x02, 0xe6, 0xd0, - 0x24, 0x0d, 0xb6, 0x89, 0x43, 0x54, 0x45, 0xc8, 0xa6, 0xde, 0x9e, 0xa6, 0x45, 0xee, 0xc3, 0x5a, 0x0a, 0xcf, 0x93, - 0xc8, 0xe2, 0x52, 0xe1, 0x44, 0x0b, 0x85, 0x70, 0x51, 0x44, 0xc1, 0xae, 0x59, 0x38, 0xfe, 0x86, 0x22, 0x44, 0x16, - 0x6f, 0x41, 0x57, 0x95, 0x2d, 0xf9, 0x7a, 0xf0, 0x98, 0xd0, 0xf4, 0xf8, 0x4a, 0x9a, 0xc6, 0xb7, 0x37, 0x2c, 0x0e, - 0xbc, 0x3b, 0x4d, 0xcf, 0xa2, 0xf0, 0x47, 0x98, 0x80, 0xd7, 0xd1, 0x2a, 0x94, 0x2b, 0x60, 0xaa, 0xf6, 0x9a, 0xbd, - 0x54, 0x1b, 0xbd, 0x1c, 0x62, 0x76, 0x48, 0x10, 0xf8, 0xd6, 0xc2, 0x9b, 0xb2, 0xff, 0x32, 0xe8, 0xdf, 0xff, 0xd6, - 0x33, 0xe3, 0x5d, 0x94, 0x7f, 0xe8, 0x97, 0xc5, 0x0e, 0x9f, 0x79, 0xf2, 0x64, 0xaf, 0x79, 0xd8, 0xda, 0x28, 0x60, - 0x5e, 0x2c, 0xa0, 0xa8, 0x69, 0xad, 0x37, 0x9e, 0x02, 0x80, 0xe2, 0x22, 0x5a, 0x8e, 0x66, 0xe8, 0xb7, 0xfb, 0xe5, - 0xc6, 0x9b, 0x42, 0x9f, 0x2c, 0xb9, 0xb4, 0xaf, 0xf2, 0xa1, 0x57, 0x8a, 0x8a, 0x59, 0xc0, 0xef, 0x9f, 0x41, 0xfa, - 0xad, 0x7f, 0xe3, 0x34, 0x6c, 0xee, 0x9a, 0x3c, 0xe4, 0xd7, 0x83, 0x36, 0x6f, 0xcf, 0x87, 0xa8, 0x3c, 0x14, 0xd8, - 0x5a, 0x28, 0xe9, 0xea, 0x91, 0x4c, 0x56, 0x9d, 0x34, 0x39, 0x89, 0x4c, 0x53, 0x7e, 0x1c, 0xf1, 0x15, 0x66, 0x95, - 0xac, 0x46, 0x0c, 0xc6, 0xb1, 0x55, 0x05, 0xc9, 0x70, 0x6f, 0x0a, 0x86, 0xe8, 0xab, 0xfa, 0x6e, 0xee, 0x87, 0x06, - 0xe6, 0x80, 0xdd, 0x7c, 0xe3, 0xdd, 0x42, 0x16, 0x44, 0x40, 0x6e, 0xd5, 0x57, 0x50, 0x68, 0xc8, 0xd1, 0x82, 0xbc, - 0xf1, 0x58, 0x53, 0x6b, 0x67, 0x42, 0x68, 0x03, 0x07, 0x5f, 0x29, 0x8a, 0xa2, 0xe4, 0xd7, 0x08, 0x25, 0xbf, 0x47, - 0x60, 0x39, 0x5e, 0x07, 0x40, 0x5b, 0x92, 0x2d, 0x6e, 0xa9, 0x04, 0x6e, 0x06, 0x68, 0x3f, 0x2d, 0x0a, 0x78, 0xa2, - 0x1f, 0x30, 0x6e, 0xa1, 0x02, 0x71, 0xa1, 0x07, 0xd5, 0xb7, 0x17, 0x43, 0x3e, 0xc0, 0xae, 0x82, 0x17, 0x76, 0x7c, - 0xcb, 0x25, 0xc1, 0x8a, 0x4d, 0x8f, 0x83, 0x1e, 0xab, 0xcf, 0x08, 0x13, 0x4a, 0x58, 0x10, 0xb4, 0x0e, 0x95, 0x04, - 0x8f, 0x06, 0xab, 0xc1, 0x8d, 0x78, 0x2f, 0xba, 0x4d, 0xe7, 0x2c, 0x5c, 0xaa, 0x06, 0x58, 0x9d, 0x60, 0x86, 0x1e, - 0xa8, 0xf3, 0x9a, 0x98, 0x2d, 0xc0, 0x36, 0xf5, 0x2d, 0x67, 0x44, 0x0b, 0x85, 0xa9, 0x8a, 0x67, 0x8c, 0x78, 0x00, - 0x9c, 0x84, 0xe3, 0xb6, 0x2a, 0x85, 0xe0, 0x4b, 0x1a, 0x95, 0xb1, 0x39, 0x0f, 0x79, 0x85, 0x9c, 0x02, 0xd9, 0x88, - 0x71, 0x71, 0x91, 0x98, 0x76, 0xcd, 0xab, 0x2e, 0x5a, 0xae, 0x91, 0xf1, 0x2a, 0x82, 0xa2, 0x58, 0xdf, 0xec, 0x86, - 0xc3, 0x09, 0x69, 0x09, 0x1a, 0xfb, 0x19, 0x6d, 0xf4, 0xd3, 0x30, 0xe8, 0x8f, 0xec, 0x8e, 0x08, 0x09, 0x4d, 0xd5, - 0x47, 0x76, 0x07, 0xc6, 0xe1, 0x67, 0x20, 0x4d, 0x51, 0xb7, 0xa0, 0x6b, 0x03, 0x12, 0xfd, 0x8e, 0x20, 0x55, 0xc5, - 0x96, 0x03, 0x64, 0x67, 0x5b, 0xb0, 0x38, 0x85, 0x23, 0x35, 0x92, 0x9e, 0x38, 0xc4, 0x3c, 0x62, 0x81, 0x56, 0x3b, - 0xc7, 0x66, 0xcd, 0xd1, 0xd0, 0x9f, 0x39, 0xb6, 0xbd, 0xbf, 0x51, 0x1f, 0x04, 0xd9, 0x75, 0xb5, 0x75, 0x23, 0x75, - 0x1d, 0xdb, 0xf4, 0x9f, 0x59, 0xad, 0xde, 0x06, 0x8d, 0x96, 0x32, 0x49, 0x0d, 0x50, 0xfc, 0xd5, 0x7f, 0xbc, 0xd6, - 0x36, 0x0e, 0xa4, 0x5e, 0x8d, 0x00, 0x80, 0xb0, 0x65, 0x5c, 0xfe, 0x35, 0xd8, 0x24, 0xfd, 0x94, 0xc7, 0x8a, 0xb2, - 0x9a, 0x0f, 0x20, 0x17, 0xa2, 0x06, 0xc7, 0xe8, 0x4f, 0xca, 0x73, 0x45, 0xa3, 0xe3, 0xa3, 0xeb, 0x83, 0x9e, 0xc0, - 0x28, 0x22, 0x44, 0x8e, 0xdc, 0x41, 0xe5, 0x8b, 0x49, 0x15, 0xc3, 0xf1, 0xac, 0x6b, 0xac, 0xd0, 0xe8, 0x6d, 0xe5, - 0x16, 0xb0, 0xff, 0x06, 0xf2, 0x69, 0x0d, 0x21, 0xc6, 0x23, 0xd4, 0x80, 0xcc, 0xa9, 0xf7, 0x76, 0x08, 0xe1, 0x79, - 0xe5, 0xee, 0xca, 0x44, 0x72, 0xf7, 0xce, 0x90, 0xe8, 0xa0, 0x0e, 0x2d, 0xef, 0xaf, 0x9e, 0xdc, 0x3d, 0xb0, 0x4b, - 0x16, 0x8e, 0xcb, 0x1d, 0x56, 0xe8, 0xd7, 0xee, 0xdd, 0x95, 0x30, 0x0a, 0xa4, 0x14, 0x8e, 0x6a, 0x30, 0x4a, 0x16, - 0x85, 0xb8, 0xf9, 0xe9, 0xb8, 0xf9, 0x3b, 0x71, 0x31, 0xd8, 0x80, 0xf2, 0x81, 0xe4, 0xcd, 0x24, 0xa1, 0x38, 0xe4, - 0xad, 0xc4, 0x08, 0x5a, 0x9a, 0x60, 0x44, 0x1b, 0x77, 0x62, 0x2a, 0xdc, 0x15, 0x8b, 0x36, 0x3e, 0xcf, 0x44, 0xb5, - 0xab, 0xd4, 0xda, 0xbf, 0x5f, 0x6a, 0x9d, 0xde, 0x27, 0xb5, 0xa6, 0xe8, 0x30, 0xdc, 0x1e, 0x54, 0x44, 0xc9, 0x11, - 0xcc, 0xb9, 0x1c, 0x67, 0xa8, 0x24, 0xea, 0xc6, 0x60, 0x32, 0x35, 0x56, 0xa4, 0xd4, 0x1b, 0x39, 0x20, 0xa2, 0xf8, - 0x5b, 0xba, 0xa0, 0x08, 0x85, 0xba, 0x2c, 0x1b, 0x3f, 0x2f, 0x64, 0xe3, 0x74, 0xab, 0x29, 0xe2, 0x82, 0x08, 0xee, - 0x5f, 0x8a, 0xb9, 0x93, 0xdf, 0x0e, 0x8a, 0xd8, 0x3b, 0x05, 0xa4, 0x52, 0x34, 0x99, 0xe2, 0xa2, 0x21, 0xc5, 0x28, - 0x12, 0xb7, 0x8c, 0x72, 0xa8, 0xa2, 0x72, 0xd5, 0x22, 0x98, 0x4c, 0x51, 0x0e, 0x52, 0x77, 0x04, 0x39, 0x2f, 0x96, - 0xb7, 0x4d, 0x39, 0x9a, 0x88, 0xfc, 0x5a, 0xda, 0x24, 0x79, 0xd8, 0x0f, 0x9a, 0x60, 0x21, 0xa6, 0xaf, 0xe8, 0xb5, - 0x73, 0x1b, 0x08, 0x04, 0xb2, 0x26, 0x4a, 0xd1, 0xfd, 0xd2, 0x79, 0xca, 0x96, 0x5c, 0xa8, 0xae, 0x1d, 0xa4, 0xee, - 0xa4, 0x09, 0x96, 0xe5, 0x11, 0x38, 0xd7, 0x57, 0x92, 0x04, 0xa1, 0x6b, 0x2b, 0x76, 0xaf, 0x86, 0x01, 0x40, 0xfa, - 0x5f, 0x7d, 0xe6, 0xac, 0x00, 0x48, 0x22, 0x15, 0x5b, 0xd6, 0xf9, 0xe3, 0x21, 0x36, 0xc9, 0x92, 0x1d, 0xab, 0x6e, - 0x7e, 0x93, 0xe4, 0x3d, 0x6b, 0x1e, 0x13, 0xa4, 0x2c, 0xce, 0xe7, 0x35, 0xba, 0x02, 0x0e, 0xbe, 0xcb, 0xe2, 0x65, - 0x88, 0x49, 0x70, 0xcd, 0x34, 0xf6, 0x46, 0x1f, 0xd7, 0xd2, 0xf7, 0xb8, 0x48, 0x14, 0xc4, 0xc5, 0x65, 0xa5, 0x42, - 0xcf, 0xc3, 0x9c, 0x51, 0xac, 0x6b, 0xb5, 0x12, 0x49, 0x50, 0xd3, 0x7d, 0x64, 0xb7, 0xbd, 0x17, 0x93, 0x83, 0x8a, - 0xfc, 0xb4, 0x75, 0x58, 0x96, 0xae, 0xe7, 0x70, 0xcc, 0xa3, 0x5f, 0x79, 0xf4, 0xa4, 0x3f, 0xfe, 0xd3, 0x09, 0xff, - 0x66, 0x65, 0x8d, 0x3e, 0x07, 0x04, 0x68, 0x5f, 0x52, 0x4c, 0xcb, 0x6a, 0x9a, 0x8d, 0x92, 0x26, 0xb0, 0x26, 0x7e, - 0x10, 0x98, 0x01, 0xb8, 0x31, 0xac, 0x3f, 0x6b, 0x78, 0xd8, 0xcf, 0x12, 0xb2, 0x15, 0x7e, 0x46, 0x3f, 0xe5, 0x9d, - 0x92, 0xce, 0x96, 0xf3, 0xe1, 0x5a, 0x16, 0x94, 0x4b, 0xf2, 0xf3, 0x4d, 0x99, 0xb9, 0xfc, 0xd9, 0xc9, 0x64, 0x52, - 0x96, 0x1a, 0xdb, 0xca, 0x01, 0x4a, 0x7e, 0x1f, 0xd9, 0xb6, 0x5d, 0x9d, 0xdf, 0xa6, 0x83, 0x42, 0x07, 0xc3, 0x44, - 0x21, 0x7c, 0xe7, 0xfe, 0x3d, 0xf5, 0x07, 0x41, 0x4b, 0x5d, 0x35, 0x9d, 0x47, 0xda, 0x6a, 0xff, 0x11, 0xa0, 0x20, - 0x6a, 0xb8, 0xef, 0xf8, 0x6f, 0xee, 0x95, 0x2d, 0x3d, 0x55, 0x0f, 0xf0, 0xc3, 0x1a, 0xdf, 0xb3, 0xd7, 0x77, 0x68, - 0xda, 0xb4, 0xbd, 0x33, 0xab, 0x20, 0xbb, 0x25, 0x9b, 0xa5, 0x1e, 0x59, 0x2a, 0xf9, 0x29, 0x9b, 0x27, 0xdd, 0x11, - 0x43, 0x05, 0xa9, 0x25, 0x51, 0x5b, 0xb4, 0xea, 0x31, 0xa7, 0x60, 0xc7, 0xe5, 0x08, 0x3c, 0x6c, 0x2b, 0xa8, 0xac, - 0xda, 0xd0, 0xac, 0x89, 0x8f, 0x20, 0x15, 0x5b, 0x6f, 0x2a, 0x9c, 0x70, 0x9b, 0x1e, 0xda, 0x7f, 0x2a, 0xd5, 0x53, - 0x80, 0x3b, 0x5d, 0x0b, 0x6b, 0x13, 0x52, 0x9e, 0xe0, 0xdf, 0xb9, 0x72, 0xee, 0xc5, 0xe2, 0xb6, 0x6c, 0xdc, 0xd5, - 0x01, 0x75, 0x53, 0x41, 0xca, 0x08, 0xea, 0x3a, 0xd4, 0x97, 0x9b, 0x00, 0x4d, 0x64, 0xeb, 0x16, 0xb0, 0xa0, 0x11, - 0x53, 0x50, 0xd1, 0x11, 0xe6, 0xa0, 0xe2, 0x75, 0x16, 0x76, 0x5e, 0x21, 0xdf, 0xc7, 0x5f, 0x90, 0xa5, 0x1c, 0xd2, - 0x9d, 0xfc, 0xc9, 0x78, 0xde, 0x41, 0xe5, 0x5e, 0x69, 0xab, 0xa2, 0xa9, 0x0c, 0xee, 0x01, 0x71, 0x23, 0x55, 0x96, - 0x71, 0x60, 0x52, 0xe2, 0x7a, 0x4d, 0x5f, 0x6f, 0x8e, 0xbb, 0xb9, 0x7b, 0xe7, 0x10, 0xf4, 0x1a, 0x9b, 0x53, 0xb5, - 0x93, 0x6a, 0xaf, 0xaa, 0xc3, 0x16, 0x70, 0xc2, 0x0a, 0x80, 0xcf, 0xac, 0x82, 0x46, 0x43, 0x4a, 0x05, 0xf7, 0xd1, - 0xa0, 0xf3, 0xb7, 0x32, 0xb2, 0x16, 0xe3, 0xc4, 0xee, 0xea, 0xab, 0x50, 0xdf, 0x42, 0x33, 0x08, 0x73, 0xc7, 0xb1, - 0x13, 0x3e, 0x9b, 0xb0, 0x63, 0x64, 0x74, 0xe5, 0xe0, 0x0e, 0xc2, 0x53, 0x6a, 0x52, 0xf2, 0x13, 0x3a, 0xa5, 0xa8, - 0x4b, 0xf8, 0xa1, 0x56, 0x78, 0x7f, 0x51, 0x92, 0xc6, 0xf3, 0xa0, 0x13, 0x2d, 0x7d, 0xa7, 0xda, 0x73, 0x3f, 0xdc, - 0xbd, 0xae, 0x77, 0xbb, 0x73, 0x5d, 0x60, 0x0e, 0x77, 0xae, 0x0c, 0xdc, 0x25, 0x56, 0xbe, 0x48, 0xdd, 0x1f, 0x24, - 0xe5, 0x81, 0x1c, 0x30, 0x51, 0xc5, 0x56, 0x74, 0xa3, 0xff, 0x69, 0xe9, 0x0e, 0x4e, 0x4e, 0x6f, 0xe7, 0x81, 0x72, - 0xc3, 0xe2, 0x04, 0x12, 0x4a, 0xa8, 0x8e, 0x65, 0xab, 0x0a, 0x1a, 0xf4, 0xfb, 0xe1, 0xd4, 0x55, 0x7f, 0xb9, 0x78, - 0x63, 0x76, 0xd4, 0x53, 0x30, 0xc7, 0xb8, 0x99, 0x22, 0x8b, 0x7b, 0xee, 0xdd, 0xb1, 0xf8, 0xba, 0xc5, 0x3d, 0x7e, - 0x88, 0xb9, 0xc5, 0x32, 0xa5, 0xa5, 0xee, 0x90, 0x12, 0x5e, 0xb9, 0xf1, 0xd9, 0xea, 0x65, 0x74, 0xeb, 0xaa, 0x80, - 0x58, 0x9d, 0x56, 0x47, 0x71, 0x5a, 0x07, 0xd6, 0x51, 0x47, 0xed, 0x7f, 0xa5, 0x28, 0x27, 0x63, 0x36, 0x49, 0xfa, - 0x28, 0x8e, 0x39, 0x41, 0x7e, 0x90, 0x7e, 0x2b, 0x8a, 0x35, 0x0a, 0x12, 0xd3, 0x51, 0xd6, 0xfc, 0x51, 0x51, 0x00, - 0x19, 0x75, 0x95, 0x47, 0x93, 0xd6, 0xe4, 0x60, 0xf2, 0xa2, 0xc7, 0x8b, 0xb3, 0xaf, 0x4a, 0xd5, 0x0d, 0xfa, 0xb7, - 0x25, 0x35, 0x4b, 0xd2, 0x38, 0xfa, 0xc8, 0x38, 0x2f, 0xa9, 0xe4, 0x82, 0xa2, 0x6a, 0xd3, 0xd6, 0xe6, 0x97, 0x9c, - 0xce, 0x70, 0x34, 0x69, 0x15, 0xd5, 0x11, 0xc6, 0xfd, 0x1c, 0xc8, 0x93, 0x7d, 0x01, 0xfa, 0x89, 0x3c, 0x4d, 0x8e, - 0x59, 0x37, 0x51, 0x8e, 0xca, 0xc7, 0x38, 0x15, 0xe3, 0x3b, 0x81, 0x8c, 0x6b, 0x85, 0xf7, 0x62, 0x82, 0xcd, 0x5c, - 0xf5, 0x47, 0xa7, 0xd5, 0x31, 0x1c, 0xe7, 0xc8, 0x3a, 0xea, 0x8c, 0x6c, 0xe3, 0xc0, 0x3a, 0x30, 0xdb, 0xd6, 0x91, - 0xd1, 0x31, 0x3b, 0x46, 0xe7, 0xbb, 0xce, 0xc8, 0x3c, 0xb0, 0x0e, 0x0c, 0xdb, 0xec, 0x40, 0xa1, 0xd9, 0x31, 0x3b, - 0x37, 0xe6, 0x41, 0x67, 0x64, 0x63, 0x69, 0xcb, 0x3a, 0x3c, 0x34, 0x1d, 0xdb, 0x3a, 0x3c, 0x34, 0x0e, 0xad, 0xa3, - 0x23, 0xd3, 0x69, 0x5b, 0x47, 0x47, 0xe7, 0x87, 0x1d, 0xab, 0x0d, 0xef, 0xda, 0xed, 0x51, 0xdb, 0x72, 0x1c, 0x13, - 0xfe, 0x32, 0x3a, 0x56, 0x8b, 0x7e, 0x38, 0x8e, 0xd5, 0x76, 0x0c, 0x3b, 0x38, 0x6c, 0x59, 0x47, 0x2f, 0x0c, 0xfc, - 0x1b, 0xab, 0x19, 0xf8, 0x17, 0x74, 0x63, 0xbc, 0xb0, 0x5a, 0x47, 0xf4, 0x0b, 0x3b, 0xbc, 0x39, 0xe8, 0xfc, 0x55, - 0xdd, 0x6f, 0x1c, 0x83, 0x43, 0x63, 0xe8, 0x1c, 0x5a, 0xed, 0xb6, 0x71, 0xe0, 0x58, 0x9d, 0xf6, 0xcc, 0x3c, 0x68, - 0x59, 0x47, 0xc7, 0x23, 0xd3, 0xb1, 0x8e, 0x8f, 0x0d, 0xdb, 0x6c, 0x5b, 0x2d, 0xc3, 0xb1, 0x0e, 0xda, 0xf8, 0xa3, - 0x6d, 0xb5, 0x6e, 0x8e, 0x5f, 0x58, 0x47, 0x87, 0xb3, 0x23, 0xeb, 0xe0, 0xc3, 0x41, 0xc7, 0x6a, 0xb5, 0x67, 0xed, - 0x23, 0xab, 0x75, 0x7c, 0x73, 0x64, 0x1d, 0xcc, 0xcc, 0xd6, 0xd1, 0xd6, 0x96, 0x4e, 0xcb, 0x82, 0x39, 0xc2, 0xd7, - 0xf0, 0xc2, 0xe0, 0x2f, 0xe0, 0xcf, 0x0c, 0xdb, 0xfe, 0x81, 0xdd, 0x24, 0x9b, 0x4d, 0x5f, 0x58, 0x9d, 0xe3, 0x11, - 0x55, 0x87, 0x02, 0x53, 0xd4, 0x80, 0x26, 0x37, 0x26, 0x7d, 0x16, 0xbb, 0x33, 0x45, 0x47, 0xe2, 0x0f, 0xff, 0xd8, - 0x8d, 0x09, 0x1f, 0xa6, 0xef, 0xfe, 0x5b, 0xfb, 0xc9, 0x97, 0xfc, 0x64, 0x7f, 0x4a, 0x5b, 0x7f, 0xda, 0xff, 0xea, - 0x04, 0x0e, 0x77, 0x7f, 0x60, 0xfc, 0xda, 0xa4, 0x94, 0xfc, 0xfb, 0xfd, 0x4a, 0xc9, 0x97, 0xcb, 0x5d, 0x94, 0x92, - 0x7f, 0xff, 0xe2, 0x4a, 0xc9, 0x5f, 0xab, 0xbe, 0x35, 0x6f, 0xaa, 0x59, 0xa8, 0x7f, 0x58, 0x57, 0x45, 0x0e, 0x89, - 0xa7, 0x5d, 0xfe, 0xb4, 0xbc, 0x82, 0xf8, 0xf1, 0x6f, 0x22, 0xf7, 0xe5, 0xb2, 0x64, 0xf0, 0x19, 0x01, 0x8e, 0x7d, - 0x13, 0x11, 0x8e, 0xfd, 0xb0, 0x74, 0xc1, 0xca, 0x8c, 0xb3, 0x39, 0xfe, 0xd8, 0x9c, 0x79, 0xc1, 0x24, 0x67, 0x91, - 0xa0, 0xa4, 0x87, 0xc5, 0xe0, 0x37, 0x0f, 0xe4, 0x19, 0x6e, 0x32, 0xcb, 0x79, 0x98, 0x80, 0x45, 0x30, 0x58, 0x72, - 0x4c, 0xe2, 0xac, 0xd2, 0xd8, 0x12, 0x11, 0xf7, 0xaf, 0xb9, 0x47, 0x71, 0xe3, 0x7b, 0x34, 0x00, 0xae, 0xef, 0xdd, - 0xd9, 0xec, 0x57, 0x01, 0xcb, 0x3a, 0x61, 0x20, 0x0d, 0xdc, 0x7e, 0xdd, 0xfb, 0xb2, 0x19, 0x6e, 0xc5, 0xf0, 0xba, - 0x19, 0x52, 0x80, 0xa4, 0xda, 0xde, 0x29, 0x9b, 0xf1, 0xde, 0x37, 0xcc, 0x9a, 0xcf, 0x97, 0x9a, 0x6f, 0xb1, 0x21, - 0xce, 0x3b, 0xae, 0x4e, 0xd5, 0xba, 0xc4, 0xa7, 0xd5, 0x4f, 0x48, 0x71, 0x41, 0x2d, 0x0c, 0x8d, 0x0b, 0x4e, 0xd5, - 0x56, 0x90, 0xdf, 0xb1, 0xa5, 0x77, 0xa5, 0x3e, 0x65, 0xe3, 0xe4, 0x67, 0x6b, 0xbc, 0x57, 0xf8, 0xbf, 0x02, 0x27, - 0xca, 0x39, 0x9e, 0x61, 0x24, 0xcf, 0xf3, 0x5a, 0xea, 0x97, 0xa4, 0x11, 0xd9, 0xcc, 0x59, 0x6f, 0xf2, 0xa2, 0x8d, - 0x6e, 0x09, 0x0e, 0x9b, 0x0b, 0x2e, 0x08, 0x3f, 0x4f, 0x4e, 0x00, 0x19, 0x39, 0x6a, 0xa0, 0x9f, 0xc3, 0xb6, 0xce, - 0x44, 0xbd, 0x47, 0xb0, 0x89, 0xb9, 0x27, 0xa0, 0x22, 0x87, 0x34, 0x5d, 0x4f, 0x82, 0xc8, 0x4b, 0xbb, 0xc8, 0xa6, - 0x49, 0x2c, 0x6f, 0x0b, 0x3d, 0x16, 0x7a, 0x5b, 0x8c, 0xe9, 0xe4, 0x8e, 0x79, 0x27, 0xe8, 0xf9, 0xb0, 0xcd, 0xfe, - 0x2e, 0x77, 0x38, 0x5b, 0x97, 0xcc, 0x51, 0x9c, 0xc3, 0x63, 0xc3, 0x39, 0x32, 0xac, 0xe3, 0x43, 0x3d, 0x13, 0x07, - 0x4e, 0xee, 0xb2, 0x34, 0x21, 0xe0, 0x00, 0x91, 0x83, 0xe9, 0x87, 0x7e, 0xea, 0x7b, 0x41, 0x06, 0xfc, 0x70, 0xf9, - 0x92, 0xf2, 0xf7, 0x65, 0x92, 0xc2, 0x18, 0x05, 0xd3, 0x8b, 0xce, 0x1f, 0xe6, 0x90, 0xa5, 0x2b, 0xc6, 0xc2, 0x06, - 0xc3, 0x98, 0xaa, 0x2f, 0xc9, 0xef, 0x67, 0x59, 0x9f, 0x91, 0xd5, 0xda, 0x30, 0x0d, 0xf9, 0xfe, 0x10, 0x8e, 0x0f, - 0xd9, 0xc0, 0xf8, 0xae, 0x09, 0xe1, 0xfe, 0x72, 0x3f, 0xc2, 0x4d, 0xd9, 0x2e, 0x08, 0xf7, 0x97, 0x2f, 0x8e, 0x70, - 0xbf, 0x93, 0x11, 0x6e, 0xc9, 0x7f, 0xb0, 0xd0, 0x30, 0xbd, 0xc7, 0x67, 0x0d, 0x5c, 0x64, 0x9f, 0xab, 0xfb, 0xc4, - 0xc0, 0xab, 0x7a, 0x91, 0xbd, 0xf6, 0x2f, 0x4b, 0xd9, 0x82, 0x1a, 0x05, 0xa0, 0x98, 0xd7, 0xd1, 0x47, 0xd7, 0x65, - 0x1f, 0x5c, 0xdd, 0x44, 0x18, 0x06, 0xe8, 0xf3, 0xfb, 0x30, 0x0d, 0xac, 0x77, 0xfc, 0x1e, 0x09, 0x0a, 0xdd, 0x37, - 0x51, 0x3c, 0xf7, 0x30, 0xc5, 0x88, 0xaa, 0x83, 0x3b, 0x1d, 0x3c, 0xd8, 0x10, 0x08, 0x64, 0x14, 0x85, 0xe3, 0x5c, - 0x2b, 0xc9, 0xdc, 0x4b, 0xe2, 0xb8, 0xd5, 0x3b, 0xe6, 0xc5, 0xaa, 0x41, 0xaf, 0x61, 0x71, 0x9f, 0xb5, 0xed, 0x67, - 0xad, 0x83, 0x67, 0x47, 0x36, 0xfc, 0xef, 0xb0, 0x76, 0x66, 0xf0, 0x8a, 0xf3, 0x28, 0x4c, 0x67, 0x45, 0xcd, 0xa6, - 0x6a, 0x2b, 0xc6, 0x3e, 0x16, 0xb5, 0x8e, 0xeb, 0x2b, 0x8d, 0xbd, 0xbb, 0xa2, 0x4e, 0x6d, 0x8d, 0x59, 0xb4, 0x94, - 0xc0, 0xaa, 0x81, 0xc6, 0x0f, 0x97, 0x20, 0x67, 0x97, 0x6a, 0xc8, 0xaf, 0xf9, 0x70, 0x8b, 0x71, 0xb1, 0x76, 0x76, - 0x25, 0x72, 0x28, 0xa8, 0x3d, 0x91, 0x56, 0xef, 0xde, 0x19, 0xe4, 0x2a, 0x4a, 0x1b, 0x73, 0x4e, 0x61, 0x66, 0x43, - 0xc8, 0x38, 0xc5, 0xc4, 0x02, 0x79, 0xb4, 0x40, 0x69, 0xbc, 0x0c, 0x47, 0x1a, 0xfe, 0xf4, 0x86, 0x89, 0xe6, 0xef, - 0xc7, 0x16, 0xff, 0xb0, 0x8e, 0xab, 0xe6, 0xf5, 0xed, 0x22, 0xe9, 0x7c, 0x22, 0x56, 0xc5, 0x7b, 0x96, 0x1a, 0x31, - 0xea, 0xb1, 0x69, 0x69, 0x4d, 0xd7, 0x7b, 0x96, 0x37, 0x7c, 0x96, 0x1a, 0xe1, 0x73, 0xd0, 0x7d, 0xba, 0xf6, 0x93, - 0x27, 0x54, 0x6b, 0xcf, 0x15, 0xc3, 0x3a, 0x1d, 0x15, 0x99, 0x29, 0x14, 0x6f, 0x1a, 0x51, 0x72, 0x8a, 0xee, 0xc8, - 0x88, 0x9e, 0x3f, 0xef, 0xbb, 0x8e, 0x3e, 0x8c, 0x99, 0xf7, 0x31, 0x13, 0xe1, 0xbe, 0x43, 0xcc, 0x4f, 0x7b, 0xbe, - 0x9b, 0xa1, 0x91, 0x5e, 0xeb, 0x4a, 0xbb, 0x80, 0x3b, 0x93, 0x2d, 0xdc, 0x11, 0x38, 0xf6, 0x82, 0xdc, 0xf5, 0x64, - 0x50, 0xe0, 0x09, 0x83, 0x1f, 0x51, 0xe7, 0x7a, 0xe6, 0x25, 0x3f, 0x24, 0x51, 0xf8, 0xcb, 0x02, 0x82, 0x1f, 0x17, - 0x16, 0x45, 0xe2, 0x32, 0xd6, 0xb6, 0x6c, 0xcb, 0x56, 0xf3, 0xfe, 0x26, 0xfe, 0xd4, 0x5d, 0x47, 0xa9, 0xd7, 0xdd, - 0x73, 0x8c, 0x20, 0x9a, 0x82, 0x7b, 0x5d, 0xea, 0xa7, 0x01, 0xeb, 0xaa, 0x2a, 0xf8, 0xd9, 0xcd, 0xe9, 0xba, 0x9e, - 0x71, 0xa7, 0x07, 0x2f, 0x86, 0x6c, 0xe6, 0xf1, 0x9d, 0xf0, 0xd0, 0xc5, 0x18, 0xea, 0x3f, 0x02, 0x8d, 0xd4, 0x54, - 0x0d, 0x44, 0x06, 0x2c, 0x4e, 0x4c, 0xd9, 0x89, 0xa8, 0xab, 0x40, 0x1b, 0x5d, 0xe5, 0x63, 0x9b, 0xc4, 0xde, 0x1c, - 0xd2, 0xed, 0xae, 0x33, 0x83, 0x23, 0x60, 0x95, 0x63, 0x60, 0xc5, 0x79, 0x71, 0x64, 0x28, 0x2d, 0xc7, 0x50, 0x6c, - 0xc0, 0xc2, 0x6a, 0x66, 0xac, 0xb3, 0xab, 0xde, 0x7d, 0x76, 0x10, 0x84, 0x76, 0x1e, 0xd1, 0x38, 0xc8, 0x02, 0x82, - 0x6b, 0x98, 0x52, 0xca, 0x9d, 0xa3, 0x49, 0x89, 0x35, 0x7d, 0xd2, 0x85, 0x5e, 0xb0, 0xdb, 0x54, 0x07, 0x85, 0x92, - 0xa8, 0xe2, 0xeb, 0x6b, 0xf4, 0x23, 0xf6, 0x43, 0xc5, 0xff, 0xf4, 0x49, 0xf3, 0xc1, 0xc7, 0xc9, 0x95, 0xe6, 0x07, - 0x9e, 0xf5, 0xd2, 0x84, 0xf9, 0x85, 0xf6, 0x1e, 0x27, 0x0b, 0x1c, 0x10, 0xe1, 0xdf, 0xa2, 0x58, 0xfc, 0xe0, 0xd6, - 0x13, 0x56, 0xe0, 0x85, 0x53, 0xc0, 0x74, 0x5e, 0x38, 0xdd, 0xb0, 0xd2, 0x22, 0x57, 0xe8, 0x4a, 0x69, 0xd1, 0x55, - 0x61, 0x41, 0x95, 0xbc, 0xbc, 0xbb, 0xf0, 0xa6, 0x3f, 0x79, 0x73, 0xa6, 0xa9, 0x40, 0xfc, 0xd0, 0x73, 0xb7, 0x50, - 0xf0, 0x3e, 0x77, 0x9f, 0x9e, 0xcc, 0x59, 0xea, 0x91, 0x76, 0x08, 0xee, 0xc4, 0xc0, 0x25, 0x28, 0x9c, 0xfe, 0xf0, - 0x38, 0x18, 0x2e, 0xa5, 0xd8, 0x22, 0xf2, 0x61, 0x28, 0x9c, 0x7c, 0x99, 0x68, 0x08, 0xea, 0x3a, 0x06, 0xf9, 0x21, - 0x8c, 0x3c, 0x4c, 0xb3, 0xe3, 0x86, 0x91, 0xda, 0x7f, 0x9a, 0xbb, 0x6c, 0x36, 0x2d, 0x42, 0xe0, 0x87, 0x1f, 0x2f, - 0x63, 0x16, 0xfc, 0xc3, 0x7d, 0x0a, 0xf4, 0xfc, 0xe9, 0x95, 0xaa, 0xf7, 0x52, 0x6b, 0x16, 0xb3, 0x89, 0xfb, 0x14, - 0xee, 0xa9, 0x5d, 0xb4, 0x9a, 0x05, 0x66, 0xfe, 0xf9, 0xed, 0x3c, 0x30, 0xf0, 0xd6, 0x4f, 0xb0, 0xa8, 0xed, 0x56, - 0x11, 0xee, 0xbc, 0xbd, 0xd3, 0x5d, 0xbf, 0xcf, 0x2f, 0xf1, 0x70, 0x31, 0x5c, 0x97, 0xae, 0xde, 0x4e, 0x0f, 0xaf, - 0xd5, 0xc3, 0xc0, 0x1b, 0x7d, 0xec, 0xd1, 0x9b, 0xd2, 0x83, 0x09, 0x44, 0x7c, 0xe4, 0x2d, 0xba, 0x48, 0x75, 0xe5, - 0x42, 0x70, 0xaa, 0xa6, 0xd2, 0x9c, 0xe1, 0xab, 0xdd, 0xcb, 0xb8, 0x95, 0xd7, 0xf8, 0x65, 0xfc, 0xd4, 0x6a, 0xe6, - 0xa7, 0x4c, 0x7c, 0x0a, 0x1f, 0xb2, 0x4c, 0xdc, 0xdf, 0xe9, 0xe6, 0x8a, 0xf7, 0x6d, 0xab, 0xad, 0x38, 0x9d, 0xef, - 0x0e, 0x6f, 0x1c, 0x7b, 0xd6, 0x72, 0xac, 0xce, 0x07, 0xa7, 0x33, 0x6b, 0x5b, 0xc7, 0x81, 0xd9, 0xb6, 0x8e, 0xe1, - 0xcf, 0x87, 0x63, 0xab, 0x33, 0x33, 0x5b, 0xd6, 0xc1, 0x07, 0xa7, 0x15, 0x98, 0x1d, 0xeb, 0x18, 0xfe, 0x9c, 0x53, - 0x2b, 0xb8, 0x17, 0xd1, 0x35, 0xe8, 0x69, 0x09, 0x39, 0x48, 0xbf, 0x73, 0x55, 0xad, 0x51, 0xa2, 0x7a, 0x35, 0xea, - 0xde, 0x05, 0x06, 0x97, 0x10, 0x69, 0x6d, 0x30, 0xf4, 0x90, 0x16, 0xba, 0x8c, 0xd2, 0xcd, 0x0a, 0xc3, 0x37, 0xe1, - 0xa1, 0x5e, 0xe4, 0x3f, 0x95, 0x4e, 0x10, 0xaf, 0xdb, 0x4b, 0x68, 0xbb, 0x4b, 0x61, 0xe5, 0xb4, 0xca, 0xb1, 0x6b, - 0xc8, 0x7c, 0xac, 0x1b, 0xa0, 0x3a, 0x06, 0xc4, 0x54, 0x84, 0x83, 0xd2, 0x6a, 0xd1, 0x96, 0xc0, 0x66, 0x09, 0x4b, - 0xa9, 0x48, 0x13, 0x2d, 0x81, 0xd8, 0xe8, 0xba, 0x48, 0x58, 0x8c, 0x1d, 0xf3, 0x1a, 0x8c, 0x67, 0x56, 0xae, 0x7d, - 0xd5, 0xab, 0xe2, 0x4b, 0xf0, 0x8a, 0xb7, 0xc2, 0x68, 0x85, 0x56, 0x1f, 0xf7, 0xcd, 0x1d, 0xc6, 0x19, 0x60, 0xc2, - 0xe6, 0xac, 0x0c, 0x2c, 0x0f, 0x9d, 0x5d, 0xfd, 0xe0, 0x06, 0x82, 0x7e, 0xd0, 0x07, 0xa5, 0x44, 0xdd, 0x9f, 0xd5, - 0x0f, 0x8f, 0x62, 0x21, 0x27, 0xcb, 0x1c, 0xfb, 0x71, 0x0e, 0x9e, 0x44, 0x51, 0x9c, 0xfa, 0x4c, 0xa5, 0xba, 0x41, - 0xb1, 0x9c, 0x58, 0x7c, 0xe3, 0x05, 0x92, 0xdd, 0x9d, 0xd4, 0x72, 0x2f, 0x27, 0x54, 0x4f, 0x9e, 0x14, 0xc0, 0x99, - 0x15, 0xb8, 0x4f, 0x9c, 0x43, 0xe0, 0x12, 0x0e, 0x59, 0x7b, 0xab, 0x09, 0x28, 0x5d, 0xcc, 0xb6, 0xb9, 0x82, 0x17, - 0x89, 0x99, 0x84, 0x99, 0x97, 0x30, 0x30, 0x69, 0xb4, 0x43, 0xdd, 0x30, 0x2f, 0x81, 0xcc, 0x76, 0x95, 0x9b, 0x99, - 0xaa, 0xf7, 0x42, 0x61, 0x2d, 0x11, 0x6e, 0xc9, 0x49, 0xc7, 0xaf, 0x8e, 0x2a, 0x4c, 0xcd, 0x96, 0x71, 0xdc, 0xe3, - 0xcf, 0xfe, 0xef, 0x1e, 0x04, 0xfa, 0x96, 0x82, 0x79, 0x47, 0x05, 0x8b, 0x94, 0x7c, 0x0d, 0x73, 0x7a, 0x4f, 0x84, - 0x9e, 0x25, 0xa7, 0x2a, 0x14, 0xa9, 0x5d, 0x15, 0xfd, 0xd8, 0xd4, 0xdc, 0xb6, 0x35, 0xa7, 0x62, 0x45, 0x81, 0xe1, - 0x63, 0xfe, 0x4f, 0xe1, 0xe7, 0xaa, 0x3f, 0x79, 0xd2, 0x48, 0x1c, 0xc9, 0x96, 0x28, 0x61, 0xa9, 0xb8, 0x4f, 0x68, - 0xaa, 0x8c, 0x77, 0x55, 0x19, 0xf5, 0xe5, 0xfd, 0x22, 0x36, 0x13, 0x26, 0xb9, 0xb4, 0xf7, 0xf0, 0xe7, 0x90, 0x79, - 0xa9, 0xc5, 0x75, 0xbb, 0x9a, 0xc4, 0x74, 0x18, 0x80, 0x36, 0x32, 0x42, 0x21, 0xf9, 0x30, 0x07, 0x8f, 0xd7, 0x7f, - 0x59, 0xf2, 0x20, 0x14, 0xd0, 0xc7, 0xa7, 0x4f, 0x76, 0x11, 0x37, 0xf4, 0x6d, 0xea, 0x51, 0xdc, 0x36, 0x99, 0x17, - 0x88, 0x52, 0x8f, 0xec, 0x4f, 0x7c, 0x0c, 0xb5, 0x53, 0x1f, 0x41, 0x4c, 0x8a, 0x54, 0xd1, 0x7f, 0x7b, 0xf1, 0x8d, - 0xc2, 0x0f, 0x00, 0x59, 0x37, 0xe0, 0xc5, 0x8b, 0xe2, 0xe3, 0xb8, 0x14, 0x1f, 0x47, 0xe1, 0x99, 0x97, 0x21, 0x47, - 0x6c, 0xb6, 0x4f, 0x53, 0x88, 0x02, 0x73, 0xb2, 0xf9, 0x98, 0x2f, 0x83, 0xd4, 0x5f, 0x78, 0x71, 0xba, 0x8f, 0xc1, - 0x71, 0x30, 0xd8, 0x4e, 0x53, 0xfc, 0x0a, 0x32, 0x1b, 0x11, 0xd9, 0x4c, 0xd2, 0x50, 0xd8, 0x8d, 0x4c, 0xfc, 0x20, - 0x37, 0x1b, 0x11, 0x1f, 0xf0, 0x46, 0x23, 0xb6, 0x48, 0xdd, 0x52, 0x10, 0x9e, 0x68, 0x94, 0xb2, 0xd4, 0x4c, 0xd2, - 0x98, 0x79, 0x73, 0x35, 0x0f, 0xca, 0xb5, 0xd9, 0x5f, 0xb2, 0x1c, 0x42, 0x54, 0x21, 0x11, 0x1e, 0x8c, 0x06, 0x08, - 0x06, 0x1c, 0x00, 0x22, 0x04, 0xc5, 0xa1, 0x29, 0x3c, 0x8f, 0xa6, 0x95, 0x2d, 0x55, 0xb0, 0x54, 0xa7, 0x98, 0xd4, - 0x8c, 0x6e, 0x5e, 0x20, 0xdd, 0x1e, 0x45, 0xc1, 0x35, 0x8f, 0xb9, 0x91, 0x67, 0xc7, 0x51, 0xfb, 0x27, 0xfc, 0x3a, - 0xae, 0x60, 0xb8, 0x19, 0xf5, 0xd0, 0x86, 0xb4, 0x6d, 0x4d, 0xd1, 0x38, 0xf6, 0x79, 0x65, 0xa0, 0x99, 0xd4, 0x33, - 0x66, 0xde, 0x24, 0x58, 0x2e, 0x80, 0x64, 0x95, 0x0c, 0x7c, 0x66, 0x4e, 0x3f, 0x77, 0xff, 0x44, 0xa8, 0x90, 0xaa, - 0x7d, 0xfa, 0xf4, 0x7e, 0xf0, 0xaf, 0x7f, 0x42, 0x7a, 0xd0, 0x99, 0x23, 0x62, 0x60, 0x5c, 0xca, 0xb5, 0x38, 0x5b, - 0x6c, 0x0c, 0xd0, 0xb8, 0x8b, 0x8d, 0x45, 0x74, 0x42, 0xb1, 0xb7, 0xb2, 0xc1, 0x95, 0x88, 0xab, 0x07, 0x89, 0x85, - 0x75, 0x11, 0xa9, 0x63, 0x00, 0xcb, 0x3b, 0x10, 0x31, 0x5c, 0x94, 0xbf, 0xdd, 0xbe, 0x3c, 0x56, 0x8a, 0x70, 0x8f, - 0x75, 0x16, 0x48, 0xb4, 0x87, 0xfa, 0x27, 0x9e, 0x82, 0xdc, 0x14, 0xf2, 0x45, 0x49, 0x77, 0x1f, 0x86, 0x39, 0x8b, - 0xe6, 0xcc, 0xf2, 0xa3, 0xfd, 0x15, 0x1b, 0x9a, 0xde, 0xc2, 0x27, 0x3b, 0x22, 0x94, 0x13, 0x2a, 0xc4, 0x92, 0xe6, - 0xe6, 0x39, 0xc4, 0xf8, 0x67, 0xc5, 0x54, 0x46, 0x95, 0xc0, 0x6d, 0xad, 0x42, 0x6f, 0x79, 0xc0, 0x83, 0xa2, 0x89, - 0x9a, 0xfd, 0x93, 0x7d, 0xaf, 0x5f, 0xce, 0x94, 0x63, 0x89, 0x8c, 0xaf, 0x65, 0x2a, 0x70, 0x4a, 0x09, 0x6f, 0x44, - 0x6e, 0x9b, 0xe2, 0xc1, 0x8c, 0x26, 0x13, 0x39, 0xbb, 0x8d, 0x55, 0x06, 0x2f, 0x9f, 0xb4, 0x62, 0x4b, 0x47, 0x0b, - 0xfa, 0xd2, 0xe6, 0x27, 0xf2, 0x9f, 0x6a, 0x17, 0xd3, 0x5a, 0xc1, 0x98, 0xe1, 0xbc, 0x6f, 0x64, 0xc9, 0xc9, 0x67, - 0xec, 0x11, 0x55, 0xe2, 0x88, 0xa4, 0x9a, 0x93, 0xb1, 0x81, 0xa5, 0xda, 0x73, 0x5d, 0xc2, 0x73, 0x55, 0x74, 0x07, - 0x93, 0x58, 0x93, 0xfd, 0x16, 0x06, 0x9b, 0x42, 0x43, 0x93, 0xdc, 0x7b, 0xb1, 0x51, 0x75, 0x38, 0x9b, 0x30, 0xee, - 0x7b, 0x62, 0xfb, 0x95, 0x36, 0x28, 0x6c, 0x3c, 0xbe, 0xee, 0x80, 0xe0, 0x45, 0x3f, 0x15, 0x3c, 0xaf, 0x7c, 0x4d, - 0x28, 0xdd, 0x0c, 0xbc, 0xbb, 0x48, 0x32, 0xbb, 0xe2, 0x11, 0x58, 0xce, 0xb1, 0xf4, 0x42, 0x78, 0x3e, 0x6f, 0x1c, - 0x34, 0xa4, 0x61, 0x90, 0x25, 0x74, 0xf3, 0xb0, 0x15, 0x04, 0x38, 0x60, 0xf7, 0x9d, 0x35, 0xb9, 0x6e, 0x79, 0x30, - 0x88, 0x3c, 0xb3, 0xe2, 0x1c, 0x96, 0x5e, 0x22, 0x5a, 0xc8, 0x4e, 0xf6, 0x61, 0x7c, 0x94, 0xf7, 0x50, 0x30, 0x79, - 0xc2, 0xbe, 0x10, 0x6f, 0xbd, 0x7e, 0xd3, 0xad, 0xb7, 0xca, 0xa3, 0x94, 0x59, 0x2f, 0x5f, 0x87, 0xd8, 0x36, 0x5e, - 0x42, 0xb6, 0x67, 0xdf, 0x8f, 0x39, 0x61, 0x90, 0xbe, 0x92, 0x07, 0xc3, 0x2c, 0xd5, 0xd3, 0xb7, 0x06, 0x89, 0xa1, - 0x8c, 0xbb, 0x1f, 0x96, 0x98, 0x6e, 0x37, 0xeb, 0xa5, 0x4c, 0xc4, 0x6c, 0x38, 0x4f, 0x1b, 0xc2, 0x3a, 0x34, 0x55, - 0x21, 0x3e, 0x7c, 0x4b, 0x85, 0x62, 0x9b, 0x6f, 0xab, 0x55, 0x70, 0x56, 0x45, 0x35, 0x4f, 0x53, 0x1f, 0xe1, 0x81, - 0xd8, 0xa8, 0x8d, 0xa5, 0x18, 0x6c, 0x22, 0x75, 0xa1, 0xaa, 0x50, 0x2d, 0x78, 0x8b, 0x05, 0x55, 0xd6, 0x7b, 0x27, - 0xfb, 0x74, 0x9d, 0xee, 0xd3, 0x06, 0xec, 0x9f, 0x80, 0x65, 0x3a, 0xed, 0x09, 0x6f, 0xb1, 0xe0, 0x2b, 0x4e, 0xbf, - 0xe8, 0xcd, 0xfe, 0x2c, 0x9d, 0x07, 0xfd, 0xff, 0x05, 0x64, 0x23, 0xa6, 0xdb, 0x06, 0x7b, 0x03, 0x00}; + 0x88, 0xa6, 0xd7, 0x57, 0x61, 0xb7, 0x6c, 0xac, 0xd5, 0x01, 0x46, 0x82, 0x27, 0x31, 0x04, 0x0c, 0xcc, 0x8c, 0xa8, + 0xff, 0xdb, 0xb8, 0x8a, 0xfa, 0xd1, 0xfe, 0x2e, 0x47, 0xfe, 0x3f, 0xbf, 0x7d, 0x7f, 0x0e, 0x7a, 0x2d, 0x0f, 0x15, + 0xd1, 0x6b, 0x95, 0xdb, 0xb0, 0x98, 0xa0, 0x29, 0x52, 0x7b, 0xaa, 0xb7, 0x04, 0xea, 0x8c, 0x37, 0x86, 0xfd, 0x5b, + 0xf3, 0xe6, 0xe6, 0xc6, 0x04, 0x8b, 0x56, 0x73, 0x15, 0x07, 0xc4, 0x1d, 0x4e, 0xd4, 0x4c, 0x20, 0x75, 0x56, 0x41, + 0xea, 0x10, 0x0e, 0x97, 0xe7, 0x53, 0x79, 0x3f, 0x8f, 0x6e, 0xbe, 0x09, 0x02, 0x59, 0x6c, 0x23, 0x98, 0x38, 0x2e, + 0xc9, 0x28, 0x21, 0x03, 0x0d, 0xb4, 0x4f, 0x96, 0x9f, 0x5c, 0x71, 0x7b, 0x81, 0xc9, 0xd5, 0xe8, 0xee, 0x8a, 0xeb, + 0x24, 0xf2, 0x78, 0xc4, 0xef, 0x87, 0xc7, 0x13, 0xff, 0x5a, 0x41, 0x4e, 0xd3, 0x55, 0xc1, 0x99, 0x2b, 0x60, 0xa3, + 0x55, 0x9a, 0x46, 0xa1, 0x19, 0x47, 0x37, 0xea, 0xe0, 0x98, 0x1e, 0x44, 0x05, 0x8f, 0x1e, 0x55, 0xe5, 0xeb, 0x71, + 0xe0, 0x8f, 0x3f, 0xba, 0xea, 0xe3, 0xb5, 0xef, 0x0e, 0x2a, 0xfc, 0xa4, 0x9d, 0xa9, 0x03, 0x80, 0x55, 0xf9, 0x26, + 0x08, 0x8e, 0xf7, 0xe9, 0x8b, 0xc1, 0xf1, 0xfe, 0xc4, 0xbf, 0x1e, 0x48, 0xa9, 0x61, 0xb8, 0xde, 0xd4, 0xe5, 0x21, + 0x38, 0x73, 0x4b, 0xb3, 0x04, 0x63, 0x3a, 0x8c, 0x99, 0x56, 0x5c, 0x7e, 0x21, 0xd6, 0x0c, 0xc1, 0xab, 0x8d, 0x51, + 0x9c, 0x1e, 0xc0, 0x55, 0xef, 0xd3, 0x27, 0x2d, 0xb7, 0x43, 0x9d, 0x4b, 0x41, 0xda, 0x50, 0xcd, 0x87, 0x55, 0x0c, + 0x8c, 0x34, 0xa3, 0x6b, 0x22, 0x94, 0x5c, 0xa0, 0x1b, 0xe3, 0xcc, 0xc0, 0x0c, 0x3b, 0xde, 0x12, 0x34, 0x8e, 0xfc, + 0xa7, 0x74, 0x23, 0x1e, 0x43, 0x56, 0x6d, 0x09, 0x89, 0xeb, 0x92, 0xce, 0x85, 0x4e, 0x21, 0x8f, 0x13, 0x08, 0xca, + 0x12, 0xec, 0x87, 0xf4, 0x20, 0x5a, 0xa0, 0x43, 0x56, 0xb7, 0x3c, 0x38, 0x8f, 0x97, 0x89, 0x3c, 0x6a, 0x62, 0x5e, + 0x4e, 0x4a, 0x2b, 0xd4, 0xab, 0xae, 0x97, 0x88, 0x1a, 0xb9, 0x97, 0x34, 0x2d, 0x19, 0xe8, 0xf0, 0xb4, 0xd4, 0xa8, + 0xd0, 0x5c, 0xf0, 0xea, 0x93, 0x14, 0x47, 0xcc, 0xd0, 0x2e, 0x12, 0x23, 0xba, 0x2c, 0xe8, 0x54, 0x42, 0x88, 0xb2, + 0x17, 0x65, 0x45, 0x00, 0x67, 0x5a, 0xf5, 0xc1, 0xe3, 0x75, 0x88, 0x84, 0x2d, 0x71, 0x07, 0xe5, 0x7d, 0x90, 0x7a, + 0x23, 0x93, 0x36, 0xb3, 0xaa, 0x7c, 0x3d, 0x19, 0x05, 0xf9, 0x62, 0xd3, 0x21, 0x98, 0x7b, 0xe1, 0x24, 0x60, 0xe7, + 0xde, 0xe8, 0x3b, 0xac, 0xf3, 0x7a, 0x14, 0xbc, 0x82, 0x0a, 0x99, 0x3a, 0x78, 0xbc, 0x26, 0xd2, 0x5d, 0x87, 0xb0, + 0x33, 0xda, 0x02, 0xd5, 0x7e, 0x78, 0xca, 0x25, 0x16, 0xd3, 0xd7, 0x08, 0x2c, 0x91, 0x5b, 0x8a, 0x63, 0x5b, 0x86, + 0x8c, 0xa7, 0xfc, 0x81, 0xbd, 0xa9, 0xf0, 0x53, 0x0b, 0x70, 0x45, 0xe2, 0x04, 0xcb, 0x3b, 0x53, 0x06, 0x96, 0xc8, + 0xea, 0xbb, 0xe8, 0x46, 0x40, 0xca, 0x27, 0x80, 0x42, 0x54, 0x9e, 0xbc, 0x1f, 0x1e, 0xcb, 0x6a, 0x21, 0x94, 0x9d, + 0x53, 0xbb, 0xf0, 0x2b, 0x53, 0x95, 0x22, 0x01, 0xd4, 0xf2, 0x56, 0x1d, 0x1c, 0xef, 0xcb, 0xb5, 0x07, 0xc3, 0xde, + 0xa9, 0x34, 0x38, 0x6c, 0x55, 0xdc, 0x9b, 0x2f, 0x8a, 0x87, 0xec, 0x52, 0x81, 0x5b, 0x72, 0x06, 0x25, 0x30, 0x47, + 0xe5, 0x4f, 0x36, 0xc8, 0x0f, 0xa4, 0x4c, 0x2c, 0x08, 0x14, 0xed, 0x1e, 0x81, 0x1f, 0x23, 0xbd, 0x97, 0x2f, 0x21, + 0x59, 0x66, 0x8a, 0xd6, 0x86, 0xfc, 0xdf, 0x62, 0x4a, 0x50, 0xd2, 0xcd, 0xc2, 0x24, 0x8a, 0x55, 0x18, 0x66, 0x35, + 0x6f, 0x92, 0x22, 0xe5, 0x6b, 0xc3, 0x01, 0xd7, 0x92, 0x55, 0x98, 0xb0, 0xfd, 0xea, 0xa7, 0xd2, 0xb8, 0x87, 0x7a, + 0xf1, 0x43, 0xe1, 0x83, 0xa9, 0x20, 0xad, 0x1c, 0xc0, 0xe6, 0x7c, 0x54, 0x17, 0x8f, 0x7d, 0xe3, 0x2f, 0x91, 0x31, + 0xf2, 0x8c, 0x2b, 0xcf, 0xf8, 0x31, 0xbc, 0xcc, 0x6a, 0x17, 0x2f, 0xcf, 0x25, 0x67, 0xb0, 0xbe, 0x06, 0x11, 0x98, + 0xca, 0x97, 0x0a, 0xdf, 0xe2, 0x36, 0x23, 0xe7, 0x5e, 0x3c, 0x63, 0x22, 0x85, 0x9b, 0x78, 0x2b, 0x64, 0x07, 0xba, + 0x34, 0x2d, 0x10, 0x9e, 0x6c, 0x8f, 0x9b, 0xd6, 0xf9, 0xd6, 0x38, 0x8d, 0x83, 0x3f, 0xb3, 0x3b, 0x60, 0xb3, 0x92, + 0x34, 0x5a, 0x82, 0xcc, 0xca, 0x9b, 0x71, 0x1d, 0x84, 0xa1, 0xb1, 0xdd, 0xba, 0xfb, 0xf4, 0x89, 0x49, 0x59, 0xc5, + 0xd2, 0x68, 0x36, 0x0b, 0x98, 0x26, 0x65, 0x1f, 0xcb, 0xbb, 0x39, 0xd9, 0xb3, 0x45, 0xe4, 0x6a, 0x3d, 0x6b, 0x3a, + 0x58, 0x62, 0xc4, 0x2c, 0xe7, 0x06, 0x01, 0x71, 0x91, 0x71, 0x15, 0x32, 0xe4, 0x9a, 0x38, 0x17, 0xc5, 0xc1, 0x35, + 0x27, 0xd1, 0x6a, 0x14, 0x30, 0x13, 0x4f, 0x03, 0x74, 0xb9, 0x1e, 0xad, 0x46, 0xa3, 0x80, 0xd2, 0x85, 0x41, 0xfc, + 0xb5, 0x28, 0x41, 0xb9, 0x68, 0xa6, 0xf7, 0x61, 0x50, 0x56, 0x5a, 0x05, 0x1f, 0x6c, 0x26, 0xe1, 0xe6, 0x40, 0x1d, + 0xa4, 0x20, 0x03, 0xdd, 0x3c, 0xd3, 0xae, 0x0a, 0x37, 0x16, 0x96, 0xa8, 0xfd, 0x1a, 0x96, 0xce, 0xbd, 0x50, 0xdf, + 0xe3, 0x0c, 0x2b, 0x5e, 0x38, 0x51, 0x5e, 0xd1, 0xde, 0x55, 0x0d, 0x95, 0x4c, 0xbf, 0x78, 0x76, 0x39, 0xd5, 0x50, + 0x5f, 0xfb, 0xde, 0x2c, 0x8c, 0x92, 0xd4, 0x1f, 0xab, 0x97, 0xfd, 0xd7, 0xbe, 0x76, 0xb1, 0x48, 0x35, 0xfd, 0xd2, + 0xf8, 0x56, 0xce, 0x03, 0x26, 0x30, 0x25, 0xa6, 0x01, 0x6b, 0xa8, 0x23, 0x9f, 0x9e, 0x6d, 0xf5, 0x04, 0x46, 0xc6, + 0x3a, 0xdf, 0xba, 0x50, 0xab, 0x92, 0x51, 0x0c, 0x53, 0x45, 0x42, 0x46, 0xb1, 0x6f, 0xf5, 0x3e, 0x09, 0x61, 0xbe, + 0x59, 0xad, 0x91, 0x69, 0x48, 0x0b, 0xe2, 0x8b, 0x41, 0xf0, 0x85, 0xe7, 0x28, 0x3d, 0xef, 0xc9, 0x5e, 0x0f, 0x25, + 0x32, 0x3e, 0xfc, 0xa6, 0xcc, 0x81, 0x3c, 0x5e, 0xa7, 0x19, 0x98, 0x1c, 0x86, 0x51, 0xaa, 0x40, 0x64, 0x37, 0xe8, + 0x70, 0x58, 0xb5, 0x92, 0xe6, 0xad, 0x6a, 0x7a, 0xc6, 0xb1, 0xc0, 0x4b, 0xa4, 0xa5, 0x28, 0xb9, 0x84, 0x40, 0x14, + 0x10, 0xa4, 0xb4, 0x14, 0xc7, 0x89, 0xfb, 0xe6, 0xc1, 0xf2, 0x95, 0xf8, 0x37, 0x09, 0xef, 0x97, 0xe9, 0xf9, 0xe3, + 0x75, 0x72, 0x22, 0x88, 0xfa, 0xf7, 0x09, 0xae, 0x25, 0xb0, 0x2b, 0x9c, 0xca, 0x67, 0xaa, 0x72, 0x22, 0x28, 0x11, + 0xd6, 0x2d, 0xa1, 0x57, 0x4d, 0xb0, 0xbb, 0xb1, 0x88, 0x99, 0xcf, 0xc5, 0x28, 0x82, 0x01, 0xab, 0x1c, 0x3d, 0x08, + 0xd6, 0x94, 0xf3, 0x56, 0x29, 0x58, 0x5c, 0x23, 0xc1, 0x00, 0xcc, 0xc5, 0x79, 0x84, 0x61, 0x76, 0x05, 0x8c, 0x24, + 0x44, 0x30, 0x13, 0x63, 0x34, 0x22, 0x39, 0x89, 0x9c, 0x1f, 0x2e, 0x57, 0x29, 0x46, 0xa6, 0x07, 0x00, 0x58, 0xa6, + 0x2a, 0x78, 0x61, 0x04, 0x5c, 0x5f, 0x5c, 0x78, 0x32, 0x55, 0xf1, 0x27, 0x9b, 0x65, 0x5c, 0x3a, 0x03, 0x38, 0x0e, + 0x87, 0x81, 0x7a, 0x1d, 0x78, 0x8c, 0xf9, 0x30, 0xc6, 0x46, 0x91, 0xd6, 0x45, 0x1b, 0xa3, 0xfd, 0x43, 0x0d, 0x02, + 0x19, 0x53, 0x3b, 0x7d, 0x2d, 0xa8, 0x1d, 0x2c, 0x44, 0xab, 0x2e, 0x0d, 0x73, 0x08, 0x32, 0xca, 0x13, 0x98, 0x3b, + 0x17, 0x2e, 0xf5, 0xc2, 0xb4, 0x4e, 0x3d, 0x57, 0xc9, 0xae, 0x6e, 0x88, 0xd3, 0x30, 0xcc, 0xae, 0x0a, 0x47, 0xd7, + 0x62, 0xbc, 0xb0, 0x25, 0xa9, 0x5c, 0x41, 0x4b, 0x37, 0x97, 0xdb, 0xb3, 0x2d, 0x63, 0x7f, 0xe1, 0xc5, 0x77, 0x64, + 0xfe, 0x66, 0xc8, 0x36, 0x72, 0xba, 0xaa, 0x10, 0x3d, 0xa0, 0x09, 0x20, 0xd2, 0xa0, 0x2a, 0x5f, 0xe7, 0x65, 0x8c, + 0x8f, 0x36, 0xb7, 0x01, 0x82, 0xbe, 0xae, 0xd4, 0xe7, 0xcc, 0x22, 0xf9, 0x23, 0x7d, 0xd2, 0xd7, 0x92, 0x86, 0xe1, + 0x25, 0xe5, 0xe1, 0x85, 0xe5, 0x8d, 0x86, 0x83, 0x21, 0x4a, 0x41, 0x70, 0xe3, 0xc8, 0x30, 0x09, 0x66, 0xfd, 0x8a, + 0xd2, 0xbb, 0x3f, 0x74, 0x39, 0x18, 0x2c, 0x47, 0x08, 0xcb, 0x51, 0x23, 0x9a, 0xf5, 0xc4, 0x8a, 0x00, 0x2f, 0x02, + 0x5c, 0x48, 0x8c, 0x1c, 0x08, 0xe5, 0xc7, 0x54, 0xf2, 0x2d, 0x14, 0xc3, 0xd1, 0x20, 0xd8, 0xe9, 0x68, 0xc4, 0xae, + 0x1b, 0xe1, 0x57, 0x71, 0x76, 0xbc, 0x4f, 0xb5, 0x89, 0x28, 0x52, 0x25, 0x98, 0x86, 0x18, 0x46, 0x58, 0xcc, 0x02, + 0x24, 0x08, 0x77, 0x9d, 0xe2, 0xa2, 0x63, 0x2d, 0x50, 0x2d, 0xed, 0x9c, 0x94, 0x19, 0x1e, 0xfc, 0x4a, 0x1d, 0x1c, + 0x63, 0xca, 0x4f, 0x20, 0xeb, 0x10, 0x14, 0xeb, 0x78, 0x9f, 0x1e, 0x95, 0xca, 0x89, 0x28, 0x1a, 0x11, 0x32, 0xc8, + 0x1e, 0x6f, 0xe0, 0x41, 0x47, 0x25, 0x49, 0xd9, 0x12, 0x4a, 0xbd, 0x4c, 0x55, 0x16, 0x9c, 0xc1, 0xe2, 0xd1, 0xf7, + 0x20, 0x34, 0x8f, 0x0d, 0x2e, 0x11, 0xaa, 0xb2, 0xf0, 0x6e, 0x71, 0xe4, 0xe2, 0x8d, 0x77, 0xab, 0x39, 0xfc, 0x55, + 0x71, 0xd6, 0x92, 0xf2, 0x59, 0x1b, 0x6f, 0xdc, 0x90, 0x03, 0xb8, 0x21, 0x8f, 0xeb, 0x17, 0x77, 0x2e, 0x16, 0x77, + 0xd2, 0xb0, 0xb8, 0x93, 0x2d, 0x8b, 0x1b, 0xf0, 0x85, 0x54, 0xf2, 0xa9, 0x8b, 0xd1, 0x97, 0x3a, 0x9f, 0x3c, 0xce, + 0x8f, 0xf4, 0xf8, 0x39, 0xc3, 0x79, 0x32, 0x93, 0x00, 0x6c, 0x89, 0x1b, 0xe6, 0xaa, 0x6e, 0x5e, 0xa4, 0x89, 0xd8, + 0x1c, 0x78, 0x7e, 0xea, 0xc4, 0xb8, 0x21, 0x85, 0xb7, 0x16, 0x54, 0xc7, 0x0b, 0xbb, 0x14, 0x3f, 0x34, 0xb4, 0x79, + 0xc3, 0x48, 0xe7, 0x5b, 0x46, 0x3a, 0x2e, 0x1d, 0x5d, 0x3e, 0x6c, 0x3a, 0x84, 0xf2, 0xa0, 0x60, 0x0f, 0x82, 0x7f, + 0x05, 0x6e, 0x99, 0xf2, 0x3e, 0x6c, 0xc6, 0xb1, 0xd2, 0x8e, 0x5a, 0x7a, 0x49, 0x72, 0x13, 0xc5, 0x60, 0xa0, 0x00, + 0xcd, 0x3c, 0x6c, 0x4b, 0x2d, 0xfc, 0x90, 0xc7, 0x3e, 0x6b, 0xdc, 0x4c, 0xc5, 0x7b, 0x79, 0x4b, 0xb5, 0x3a, 0x1d, + 0xaa, 0xb1, 0xf4, 0xd2, 0x94, 0xc5, 0x38, 0xe9, 0x1e, 0x24, 0xc9, 0xf8, 0x0f, 0xd9, 0x66, 0x35, 0x38, 0x24, 0x90, + 0xb0, 0x3a, 0x62, 0xe8, 0x25, 0xb0, 0x60, 0xa4, 0x91, 0x0c, 0xf5, 0xb5, 0x14, 0x47, 0x35, 0xce, 0x27, 0xfe, 0x27, + 0x3c, 0xae, 0x5a, 0x2c, 0x79, 0xfa, 0x3a, 0x87, 0xba, 0xb5, 0xf4, 0x26, 0xef, 0xc1, 0x0e, 0x46, 0x6b, 0x19, 0xe0, + 0xd3, 0x22, 0x47, 0x4d, 0x8d, 0x89, 0x27, 0x1c, 0x17, 0x48, 0x12, 0xb1, 0x24, 0xb7, 0x18, 0x86, 0x60, 0x03, 0x9e, + 0x39, 0xbd, 0x5c, 0xb7, 0xb2, 0xfd, 0x99, 0xaf, 0x6f, 0x60, 0x4d, 0x40, 0x6d, 0x81, 0x3b, 0xc8, 0x85, 0x6e, 0x81, + 0xe1, 0x1c, 0xea, 0xa0, 0x28, 0xbd, 0x80, 0x74, 0xe8, 0xb6, 0xb8, 0x4c, 0x0f, 0x63, 0xa0, 0x5a, 0xa0, 0x56, 0x7c, + 0x32, 0xc3, 0x5f, 0xce, 0x65, 0xf6, 0x64, 0x84, 0xbf, 0x5a, 0x97, 0xb9, 0x12, 0xab, 0x22, 0x45, 0x90, 0xc6, 0xac, + 0x0e, 0x4a, 0xfb, 0x89, 0xcc, 0xb5, 0x1f, 0xb0, 0x6d, 0xf8, 0x02, 0x3f, 0x7a, 0xbc, 0x4e, 0x20, 0x40, 0x81, 0x3c, + 0x86, 0xd0, 0x8a, 0xf5, 0xac, 0xb6, 0x7c, 0xd6, 0x50, 0x3e, 0xd2, 0xff, 0x60, 0xc2, 0x8f, 0xbb, 0x24, 0x2a, 0x68, + 0x4a, 0x59, 0x06, 0x72, 0x35, 0xf2, 0x43, 0x2f, 0xbe, 0xbb, 0xa2, 0x5b, 0x88, 0x26, 0x58, 0xfc, 0x5c, 0xb6, 0x43, + 0xbc, 0x68, 0xd9, 0x3a, 0x24, 0x95, 0x14, 0x55, 0x77, 0x9c, 0xd0, 0xbb, 0x7f, 0x8e, 0x25, 0xfe, 0xae, 0x74, 0x8d, + 0xe5, 0x0b, 0x52, 0xea, 0xe8, 0xea, 0xf1, 0x5a, 0x63, 0x9b, 0xcd, 0x54, 0x46, 0x5b, 0x61, 0x20, 0x61, 0x79, 0xf0, + 0x4a, 0xbc, 0x98, 0xf8, 0x3d, 0x34, 0xff, 0x18, 0x45, 0xb7, 0xe6, 0xe3, 0x75, 0x7a, 0xa2, 0x2e, 0xbc, 0xf8, 0x23, + 0x9b, 0x98, 0x63, 0x3f, 0x1e, 0x07, 0xc0, 0x3c, 0x8e, 0x02, 0x2f, 0xfc, 0xc8, 0x1f, 0xcd, 0x68, 0x95, 0xa2, 0x41, + 0xd7, 0xbd, 0x37, 0x68, 0x31, 0x27, 0x24, 0x48, 0x44, 0xae, 0xb6, 0x66, 0x16, 0x94, 0xf7, 0x43, 0x71, 0xad, 0x2f, + 0x18, 0xc5, 0xa2, 0x96, 0x01, 0xfe, 0x08, 0x60, 0x63, 0x06, 0x01, 0x1e, 0x0c, 0x15, 0xd7, 0x4b, 0x35, 0xe4, 0xa1, + 0x92, 0x56, 0x2d, 0xcf, 0x50, 0x7c, 0x85, 0x2d, 0xfc, 0xf6, 0xee, 0xa0, 0xe4, 0x21, 0xdd, 0xe5, 0xad, 0x7c, 0xde, + 0x08, 0xa1, 0xd4, 0x24, 0xc7, 0xc2, 0x07, 0x74, 0xce, 0x19, 0xcc, 0xe6, 0xae, 0xe5, 0x8f, 0xbd, 0x24, 0x59, 0x2d, + 0xd8, 0x84, 0x54, 0x62, 0x27, 0x05, 0x50, 0xe5, 0x7b, 0x88, 0x0c, 0xd8, 0xdf, 0x56, 0xad, 0xa3, 0x83, 0x57, 0x60, + 0xe0, 0x07, 0x0c, 0x65, 0x34, 0x9d, 0xaa, 0x85, 0x28, 0xe0, 0x9e, 0xcf, 0x9c, 0x83, 0xbf, 0xad, 0xde, 0x9c, 0xda, + 0x6f, 0xf2, 0x8f, 0x43, 0x60, 0x8c, 0x85, 0xb5, 0x12, 0xe7, 0x8b, 0x25, 0x78, 0xc5, 0x88, 0xa6, 0x5e, 0xd8, 0x3c, + 0x9c, 0x8b, 0xd2, 0x16, 0x5f, 0x32, 0x36, 0x01, 0x86, 0xdb, 0xd8, 0x28, 0xbd, 0x0a, 0xd8, 0x35, 0xcb, 0x2d, 0xa1, + 0x36, 0x3b, 0xab, 0xf9, 0x02, 0x43, 0xb5, 0x72, 0xdd, 0x23, 0xe7, 0xea, 0xa4, 0x21, 0x0d, 0x71, 0x0c, 0x7c, 0xe4, + 0xf2, 0x11, 0xab, 0x1c, 0xa9, 0xa1, 0xa1, 0x4a, 0x00, 0x34, 0x42, 0x76, 0xd2, 0x50, 0xde, 0x03, 0x44, 0xdd, 0x00, + 0x9b, 0xe1, 0xe8, 0x3d, 0x48, 0x6d, 0xc1, 0xe7, 0x29, 0x80, 0x93, 0xa7, 0x15, 0x52, 0x93, 0xa6, 0x19, 0xab, 0x13, + 0xb5, 0xa9, 0x24, 0xa4, 0x11, 0xce, 0x01, 0xe8, 0x25, 0x23, 0xc4, 0x55, 0xb5, 0x6b, 0xa3, 0x94, 0x47, 0x3e, 0xc2, + 0xc4, 0xef, 0x21, 0x4b, 0x92, 0xc6, 0x09, 0xcb, 0x17, 0xdd, 0x50, 0x8b, 0xda, 0xe5, 0xf9, 0x28, 0xca, 0x81, 0x0e, + 0x1a, 0x6a, 0xab, 0xd3, 0x51, 0x69, 0x90, 0xd5, 0xfe, 0x90, 0xc4, 0x5c, 0xa5, 0x6c, 0xb1, 0xdc, 0xa5, 0xbf, 0xa2, + 0x76, 0xb9, 0xbf, 0xa2, 0xdc, 0x50, 0x9d, 0xce, 0x81, 0x6a, 0xa8, 0xed, 0x23, 0x7b, 0x6b, 0x8f, 0x0b, 0x6e, 0x54, + 0x1a, 0xcf, 0x46, 0x2a, 0x37, 0xf8, 0x6b, 0x7a, 0x7f, 0xa3, 0x72, 0xd0, 0x4a, 0xcc, 0x41, 0x2d, 0x80, 0x5a, 0x09, + 0xe1, 0x6f, 0xc8, 0xb2, 0xb0, 0x01, 0x01, 0x53, 0x05, 0xab, 0xb3, 0xe9, 0x94, 0x8d, 0xd3, 0x44, 0x17, 0x92, 0xad, + 0x3c, 0xc4, 0x3b, 0xb8, 0xf6, 0xee, 0xb9, 0xea, 0x4f, 0x10, 0xe8, 0x46, 0x44, 0x42, 0xe4, 0x00, 0x89, 0x9b, 0x5a, + 0xfd, 0x64, 0x51, 0x8b, 0xe5, 0x89, 0xe2, 0xbd, 0x80, 0xec, 0xbb, 0xa6, 0x1c, 0x41, 0xe3, 0x54, 0xaf, 0xd8, 0x8d, + 0x51, 0x6e, 0x7a, 0xba, 0x1d, 0x01, 0x6e, 0x43, 0x1a, 0x6b, 0xe7, 0x4d, 0xc7, 0xb1, 0x33, 0xd5, 0x00, 0x07, 0xeb, + 0x8f, 0x95, 0xc3, 0x43, 0x64, 0xd1, 0x55, 0xcf, 0xde, 0xbe, 0xfa, 0xf3, 0xe9, 0xeb, 0x5d, 0xf1, 0x10, 0x36, 0xd9, + 0x86, 0x26, 0x57, 0xe1, 0x96, 0x46, 0x7f, 0xf9, 0xe9, 0x61, 0xcd, 0xb6, 0x9c, 0x17, 0x8e, 0x6a, 0x90, 0x4d, 0xbc, + 0x84, 0x8d, 0xc7, 0xd1, 0x35, 0x8b, 0x3f, 0x7b, 0x1a, 0xe4, 0xc6, 0xeb, 0xc1, 0x7d, 0xfb, 0xf3, 0xe9, 0x4f, 0x3b, + 0x83, 0x7a, 0xe8, 0xc0, 0xe1, 0x02, 0xb1, 0xe7, 0x03, 0x46, 0xd7, 0x86, 0x73, 0x14, 0x44, 0x09, 0x6b, 0x80, 0xe0, + 0xd5, 0xd9, 0xdb, 0xf7, 0x38, 0x5d, 0x05, 0xe3, 0x43, 0x4d, 0x7d, 0xde, 0xe0, 0x7f, 0x7e, 0x77, 0xfa, 0xfe, 0xbd, + 0x6a, 0x60, 0x8a, 0xf0, 0x44, 0x6e, 0x9d, 0x6f, 0xe2, 0x7b, 0xe8, 0x5c, 0xed, 0x5e, 0x27, 0x5a, 0x4a, 0xd7, 0xf7, + 0xf2, 0x68, 0xa8, 0x6c, 0x63, 0x9b, 0x73, 0x1a, 0xcb, 0x7b, 0xa6, 0x3b, 0xf7, 0x4e, 0xe3, 0xaa, 0xc1, 0x4a, 0xdb, + 0x09, 0x79, 0xa9, 0x64, 0xe1, 0x87, 0x57, 0x35, 0xa5, 0xde, 0x6d, 0x4d, 0x29, 0x5c, 0x5a, 0x37, 0xb0, 0xf2, 0x2a, + 0x5a, 0x48, 0x4c, 0x10, 0xbb, 0xbd, 0x7f, 0xba, 0xa4, 0x9b, 0xe3, 0x67, 0x00, 0xcd, 0x53, 0xbc, 0x54, 0xa1, 0xae, + 0x29, 0xe6, 0xd7, 0xbd, 0x7c, 0x6e, 0xc7, 0x01, 0x78, 0x02, 0x30, 0x59, 0xf9, 0x59, 0x66, 0x90, 0xb9, 0x1f, 0x8f, + 0x5b, 0xb9, 0x8b, 0xd0, 0x67, 0xa4, 0x30, 0xe2, 0x94, 0x6c, 0xe9, 0x4d, 0xc0, 0xbc, 0xde, 0x1c, 0x45, 0x69, 0x1a, + 0x2d, 0x7a, 0x8e, 0xbd, 0xbc, 0x55, 0x95, 0xbe, 0x10, 0xb1, 0x70, 0xeb, 0xff, 0xde, 0xbf, 0xfe, 0x59, 0x41, 0xf3, + 0x54, 0x8e, 0x44, 0x81, 0xc5, 0x5e, 0xba, 0x8a, 0x59, 0xa6, 0xfc, 0xeb, 0x7f, 0x5e, 0x55, 0xc4, 0x09, 0x7d, 0xf9, + 0x1b, 0xba, 0x48, 0xc8, 0x9f, 0x5c, 0x05, 0xd1, 0xcd, 0x5e, 0xe1, 0xe7, 0x77, 0x4f, 0xe5, 0xb9, 0x3f, 0x9b, 0xe7, + 0xb5, 0x4f, 0xd2, 0x2d, 0x63, 0x13, 0xd0, 0x93, 0x16, 0x42, 0x39, 0x8b, 0x6e, 0x7a, 0xff, 0xfa, 0x67, 0x2e, 0x26, + 0xba, 0x77, 0xd7, 0xd5, 0x03, 0x5a, 0x5e, 0xd1, 0xfa, 0x3a, 0x1b, 0x4b, 0x8c, 0x44, 0xb3, 0xba, 0xc0, 0x1b, 0x85, + 0xb4, 0x2b, 0x37, 0x35, 0x82, 0x5b, 0xc6, 0xf4, 0x9d, 0x3f, 0x9b, 0x7f, 0xee, 0xa0, 0x60, 0x42, 0xef, 0x1d, 0x15, + 0x54, 0xfa, 0x02, 0xc3, 0x1a, 0xf6, 0x76, 0x5f, 0xb0, 0xcf, 0x1c, 0xd7, 0x7d, 0x43, 0xfa, 0x12, 0xa3, 0xe1, 0xf2, + 0xe2, 0xf7, 0xc3, 0x61, 0x9e, 0x22, 0x57, 0xfe, 0x1e, 0x3c, 0x15, 0x4f, 0x36, 0x4a, 0x38, 0x7b, 0xd1, 0xb3, 0x75, + 0x0a, 0x21, 0xb4, 0xc3, 0x84, 0xa0, 0xcd, 0x7d, 0xcd, 0x74, 0x34, 0xe3, 0x6b, 0x72, 0x9d, 0xdb, 0xe8, 0x7b, 0x03, + 0x59, 0x43, 0x29, 0xa6, 0x57, 0xcd, 0x75, 0x95, 0x46, 0x3d, 0x38, 0x37, 0xb1, 0xb7, 0x24, 0xd5, 0x84, 0x82, 0x7a, + 0x1a, 0x10, 0xf5, 0x54, 0xee, 0xee, 0xd7, 0x5e, 0x70, 0xbd, 0xdb, 0x35, 0xae, 0x99, 0x82, 0x21, 0x69, 0xfe, 0xf7, + 0x11, 0x6f, 0xa4, 0xcb, 0x0f, 0xa6, 0xdd, 0x37, 0x5e, 0xca, 0xe2, 0xab, 0x39, 0xf8, 0x18, 0x0b, 0x99, 0x05, 0x44, + 0xef, 0xdd, 0x86, 0x94, 0x4b, 0x6c, 0x69, 0x0d, 0x1a, 0x2d, 0x30, 0xdc, 0x6f, 0xc3, 0xdd, 0x5f, 0x08, 0x73, 0xf7, + 0x4e, 0xc1, 0x0b, 0xf4, 0x77, 0xc3, 0xde, 0xdb, 0x28, 0xd3, 0xff, 0x63, 0xef, 0xff, 0x44, 0xec, 0xbd, 0xb5, 0x9f, + 0xdf, 0xb2, 0xb0, 0xff, 0x07, 0xb0, 0x7c, 0x8f, 0xb9, 0xa7, 0x1c, 0xd3, 0x6b, 0x9a, 0xe7, 0x6a, 0x71, 0xe9, 0xf0, + 0x22, 0x5e, 0xdd, 0x50, 0xeb, 0xf2, 0x10, 0x6f, 0xdc, 0x5e, 0xd1, 0x43, 0x64, 0xbf, 0xe5, 0x28, 0xff, 0xfe, 0x88, + 0x3e, 0xa1, 0xbc, 0x58, 0x12, 0xa6, 0xef, 0x9d, 0x1a, 0x49, 0x69, 0x24, 0xde, 0x8d, 0x77, 0xb7, 0x0b, 0xde, 0x11, + 0xc0, 0x7e, 0x73, 0xe3, 0xdd, 0xd5, 0x01, 0xdb, 0x88, 0x5e, 0xab, 0x9d, 0x9d, 0x80, 0x6f, 0x51, 0x0f, 0x1d, 0x8b, + 0x8c, 0x61, 0xc2, 0xd2, 0x13, 0x28, 0x74, 0x1f, 0xaf, 0xf7, 0xaa, 0x15, 0xb3, 0x21, 0x78, 0x5d, 0x4b, 0x80, 0x47, + 0x25, 0xc0, 0xfd, 0xe4, 0x2a, 0x0a, 0x1f, 0x02, 0xf9, 0xcf, 0x20, 0x72, 0xfa, 0xcd, 0xa0, 0x63, 0x77, 0x1b, 0xb0, + 0x63, 0x69, 0x15, 0x78, 0x2c, 0xac, 0x42, 0xdf, 0xaf, 0xd7, 0x10, 0x54, 0x08, 0x2d, 0xd2, 0x58, 0x46, 0x84, 0x56, + 0x01, 0x6d, 0x8e, 0x02, 0x9a, 0xb5, 0x0a, 0xc9, 0xf5, 0xc3, 0x69, 0xec, 0xc5, 0x6c, 0xd2, 0x7c, 0x05, 0x28, 0xd9, + 0x44, 0xdf, 0x59, 0xc9, 0x6a, 0xb9, 0x8c, 0xe2, 0x34, 0xb9, 0xc2, 0xe8, 0x30, 0x0b, 0x1f, 0x2e, 0x14, 0x90, 0xc7, + 0x2c, 0x8f, 0x15, 0x7c, 0x5a, 0x27, 0x55, 0x37, 0x98, 0x5b, 0x4e, 0xf1, 0xc1, 0x7d, 0x7e, 0x0c, 0xee, 0x35, 0x34, + 0x97, 0xb4, 0x26, 0x73, 0x2b, 0x8d, 0xfd, 0x85, 0xa6, 0x1b, 0x8e, 0xad, 0xeb, 0x42, 0xbe, 0x32, 0x77, 0x07, 0x7b, + 0x14, 0xe3, 0x78, 0xae, 0x43, 0xac, 0x44, 0xf4, 0xa3, 0x01, 0x0b, 0xbd, 0x97, 0xab, 0xe9, 0x94, 0xc5, 0x9a, 0x08, + 0x06, 0x09, 0xd1, 0x68, 0xc9, 0x04, 0x11, 0xbc, 0x2b, 0x3f, 0xf8, 0xec, 0x06, 0xb2, 0x4e, 0x15, 0xc1, 0xdc, 0xc1, + 0xc3, 0x94, 0x8c, 0xd8, 0x21, 0xa3, 0x5d, 0xda, 0x6e, 0x69, 0x93, 0x67, 0x07, 0xc6, 0x1c, 0x42, 0x40, 0x15, 0x4e, + 0xf9, 0x18, 0x5d, 0xd0, 0x0f, 0xd3, 0x2e, 0xf6, 0x00, 0x0d, 0xc0, 0xe1, 0x0d, 0xdc, 0xdc, 0x1b, 0x4b, 0x19, 0xe7, + 0x0d, 0xce, 0xdd, 0x41, 0xf0, 0xdc, 0x25, 0xed, 0x12, 0x5a, 0x0b, 0xbe, 0x9a, 0x7b, 0xf1, 0xab, 0x68, 0xc2, 0x10, + 0xd0, 0x51, 0x1a, 0x81, 0x8f, 0xa8, 0x14, 0xfc, 0x07, 0x63, 0xff, 0x98, 0xa5, 0x78, 0x40, 0xfb, 0x50, 0x74, 0x25, + 0x17, 0xb9, 0xcf, 0x1f, 0xef, 0x1b, 0x70, 0xd2, 0xea, 0x57, 0x5a, 0x2c, 0x1a, 0x5f, 0xea, 0xda, 0x57, 0xf2, 0x6e, + 0x7d, 0xe5, 0xc5, 0xb1, 0xcf, 0x62, 0x45, 0xfb, 0xee, 0x57, 0x5d, 0xde, 0xb4, 0x25, 0x35, 0x12, 0xd7, 0x6d, 0x2b, + 0x18, 0x03, 0x6f, 0xea, 0xb3, 0x60, 0xe2, 0xaa, 0x63, 0xfa, 0x30, 0x57, 0x19, 0xb5, 0xbb, 0xb6, 0x6d, 0x73, 0x35, + 0xad, 0x43, 0x3f, 0x41, 0x4d, 0x0b, 0x3f, 0xe1, 0xa1, 0x24, 0xd4, 0xec, 0x12, 0x17, 0xb1, 0x41, 0xce, 0x6a, 0x21, + 0x7c, 0x47, 0x51, 0x84, 0x1e, 0x02, 0x1b, 0x8f, 0x36, 0x24, 0x40, 0x73, 0x04, 0x58, 0x05, 0x4c, 0x15, 0x80, 0x3a, + 0x0f, 0x01, 0xe8, 0xdc, 0x5f, 0xf8, 0xe1, 0x2c, 0x69, 0x84, 0x08, 0x95, 0xb5, 0x25, 0x78, 0x52, 0xfa, 0x42, 0x55, + 0x70, 0x0d, 0xe7, 0x51, 0x00, 0xd9, 0x8f, 0x54, 0x66, 0xcd, 0x2c, 0xe5, 0x85, 0x6d, 0xdb, 0x86, 0x79, 0x00, 0x79, + 0x06, 0x3b, 0x87, 0xb6, 0x61, 0xc2, 0x5f, 0x96, 0x65, 0xd5, 0x48, 0x81, 0xfb, 0x0b, 0x3f, 0x34, 0xe9, 0xb1, 0x65, + 0xef, 0x06, 0xef, 0xbd, 0xb6, 0xc4, 0x09, 0xd7, 0xc8, 0x8d, 0x72, 0x87, 0x55, 0x6d, 0xe4, 0x26, 0x65, 0x0b, 0x3b, + 0x8b, 0xc2, 0x3c, 0xf1, 0x28, 0x1c, 0x15, 0x62, 0x34, 0x2a, 0xbf, 0x45, 0xb6, 0x34, 0xae, 0x66, 0xcf, 0x50, 0xbf, + 0xe7, 0x60, 0xf5, 0x94, 0x57, 0xd1, 0x2a, 0x98, 0xa0, 0x11, 0x16, 0x58, 0x4c, 0x2b, 0x85, 0x2d, 0x6a, 0x25, 0xc5, + 0x15, 0x64, 0x30, 0xc7, 0xf4, 0x6e, 0xef, 0x91, 0x38, 0x45, 0xb1, 0xf6, 0x14, 0xa7, 0xf8, 0xa2, 0x6e, 0x0b, 0x5e, + 0x3e, 0x85, 0x28, 0x46, 0x3b, 0x7c, 0xc0, 0xf7, 0x05, 0xd4, 0x0f, 0x76, 0xa9, 0x2f, 0xd6, 0xed, 0xf2, 0x29, 0x85, + 0xba, 0xf5, 0x3e, 0x7d, 0xda, 0x1b, 0x7f, 0xfa, 0xb4, 0xb7, 0x91, 0x1f, 0xa4, 0x79, 0x84, 0xb4, 0x31, 0x18, 0x0f, + 0x6c, 0x02, 0xd1, 0x8a, 0x08, 0xe8, 0xef, 0xa1, 0xbc, 0xe7, 0xf1, 0x18, 0x59, 0xf4, 0x34, 0x36, 0x78, 0x87, 0xf4, + 0x18, 0x64, 0x95, 0x49, 0x99, 0xbb, 0x1e, 0x89, 0x79, 0x3e, 0x7d, 0xe2, 0xc7, 0xcd, 0x98, 0xb8, 0xe3, 0xbc, 0xc8, + 0x51, 0x8d, 0x95, 0x1b, 0xe4, 0x8f, 0x2a, 0x82, 0xbc, 0xe2, 0x18, 0xb3, 0x80, 0xf8, 0xc6, 0x8b, 0x43, 0x19, 0xe0, + 0x9f, 0x22, 0x85, 0x77, 0xab, 0xf0, 0x38, 0xac, 0x93, 0xea, 0x6a, 0x4c, 0x5d, 0xa6, 0xad, 0x08, 0x07, 0x0a, 0xfb, + 0x3a, 0xa9, 0x81, 0x73, 0x81, 0xed, 0x31, 0x19, 0xab, 0x18, 0x20, 0x7a, 0x75, 0xe3, 0xc9, 0x9d, 0x88, 0x61, 0xbd, + 0xf3, 0x6e, 0x7a, 0x2b, 0xf1, 0x70, 0x4a, 0x86, 0xf8, 0xbd, 0x69, 0xee, 0x2d, 0xbd, 0x24, 0x5f, 0xc7, 0x99, 0xfb, + 0x6d, 0xac, 0x2d, 0x8d, 0xd4, 0x50, 0x05, 0x19, 0x51, 0x75, 0x63, 0x51, 0x17, 0xd6, 0xb5, 0xbf, 0xe0, 0x41, 0x6e, + 0x34, 0xb1, 0x15, 0xae, 0xa6, 0xe8, 0x21, 0x11, 0x8e, 0xef, 0x30, 0x6c, 0x73, 0xf1, 0x9e, 0x40, 0xb9, 0xe2, 0x39, + 0xff, 0x26, 0xf2, 0x2b, 0x58, 0x70, 0xd5, 0x98, 0xea, 0x06, 0x79, 0x1e, 0xcc, 0xbe, 0xa4, 0x93, 0x01, 0x45, 0x72, + 0x5e, 0x48, 0x41, 0x66, 0x85, 0xdb, 0xc1, 0x55, 0xc5, 0xed, 0xa0, 0x66, 0x3e, 0x95, 0x98, 0x25, 0xcb, 0x28, 0x84, + 0xbb, 0xe2, 0x55, 0xe1, 0x57, 0x76, 0xb5, 0xe9, 0x57, 0x56, 0xf3, 0x29, 0xbe, 0xa1, 0xef, 0x40, 0x11, 0x7e, 0xfe, + 0x5f, 0x15, 0xbf, 0x00, 0x41, 0xea, 0x31, 0x37, 0xfa, 0x69, 0x93, 0x3f, 0xf9, 0xf7, 0xf7, 0xfb, 0x93, 0x9f, 0xed, + 0xe4, 0x4f, 0xfe, 0xfd, 0x17, 0xf7, 0x27, 0x3f, 0x95, 0xfd, 0xc9, 0x81, 0x04, 0x9f, 0xb2, 0x9d, 0xdc, 0x77, 0x85, + 0x23, 0x4d, 0x74, 0x93, 0xb8, 0x0e, 0xd7, 0xe7, 0x25, 0xe3, 0x39, 0x03, 0x03, 0x09, 0xce, 0xea, 0x06, 0xd1, 0x0c, + 0xbc, 0x6c, 0x9b, 0xfd, 0x68, 0xbf, 0x94, 0x17, 0x6d, 0x10, 0xcd, 0x54, 0x29, 0x3b, 0x5c, 0x28, 0xb2, 0xc3, 0x41, + 0x44, 0xbc, 0xbf, 0xdd, 0x3a, 0x2f, 0x2f, 0x9c, 0x7e, 0xdb, 0x81, 0xe8, 0xaa, 0xa0, 0xf3, 0xc6, 0x02, 0xbb, 0xdf, + 0x6e, 0x43, 0xc1, 0x8d, 0x54, 0xd0, 0x82, 0x02, 0x5f, 0x2a, 0xe8, 0x40, 0xc1, 0x58, 0x2a, 0x38, 0x84, 0x82, 0x89, + 0x54, 0x70, 0x04, 0x05, 0xd7, 0x6a, 0x76, 0x11, 0xe6, 0xde, 0xf2, 0x47, 0xfa, 0x65, 0x29, 0x31, 0x68, 0x6e, 0xa0, + 0x21, 0xaa, 0x1c, 0x19, 0x22, 0x4b, 0x85, 0x79, 0xa0, 0x73, 0x1e, 0x6d, 0xf8, 0xd5, 0x10, 0x30, 0x2f, 0xd8, 0xab, + 0x18, 0x60, 0xed, 0x43, 0x35, 0xdb, 0xe2, 0xb5, 0xda, 0xcb, 0xbd, 0xcb, 0x6d, 0xa3, 0x25, 0xbc, 0xb5, 0x7b, 0x18, + 0x3b, 0x44, 0x54, 0xee, 0x3c, 0x9f, 0xe7, 0x21, 0xab, 0x57, 0x6e, 0x11, 0x82, 0xa7, 0x0d, 0x89, 0x7b, 0x38, 0xaf, + 0xc6, 0x34, 0xb0, 0xd2, 0x81, 0x08, 0x2b, 0xe2, 0x14, 0x89, 0x0e, 0x14, 0x74, 0xc1, 0xef, 0x7b, 0x05, 0x0f, 0xc7, + 0x03, 0xbc, 0x13, 0xf4, 0x8b, 0x3c, 0x6e, 0x36, 0x69, 0x70, 0x57, 0x46, 0xea, 0xcd, 0x7a, 0x73, 0x83, 0xcc, 0xb7, + 0x7a, 0x33, 0x48, 0x84, 0x72, 0x32, 0xe9, 0x2d, 0x8d, 0x9b, 0x39, 0x0b, 0x7b, 0x53, 0xee, 0xec, 0x08, 0xeb, 0x4f, + 0xfe, 0x2b, 0x0b, 0x5d, 0x38, 0x5e, 0xe1, 0x9e, 0x28, 0xde, 0x12, 0x94, 0x66, 0xbe, 0x95, 0x0a, 0x9f, 0x21, 0x4d, + 0x36, 0xed, 0xfa, 0x12, 0x1e, 0x1e, 0xaf, 0xd9, 0x68, 0x35, 0x53, 0xce, 0xa2, 0xd9, 0xbd, 0xde, 0x1c, 0xf2, 0x2b, + 0x80, 0x52, 0x25, 0x1b, 0x56, 0x53, 0x6c, 0x6f, 0xde, 0x17, 0x3d, 0x66, 0xe5, 0xfa, 0x29, 0xc0, 0xa6, 0xa4, 0xc4, + 0x36, 0x40, 0x3f, 0x30, 0xdb, 0x92, 0xbf, 0xc4, 0x19, 0xcc, 0x9f, 0xf4, 0x7c, 0xee, 0x49, 0xf0, 0x0c, 0x7e, 0x64, + 0x49, 0xe2, 0xcd, 0x98, 0x8c, 0x5a, 0x4a, 0x8d, 0x03, 0x16, 0xcc, 0x95, 0xd8, 0x38, 0x81, 0xc0, 0xd8, 0xfb, 0x1b, + 0x5e, 0x30, 0xe0, 0xa8, 0x0b, 0xde, 0x61, 0xb0, 0x68, 0x85, 0xcb, 0x88, 0x6f, 0xc1, 0xf2, 0x94, 0xbd, 0x37, 0x00, + 0x89, 0x5c, 0xb3, 0xa0, 0x5a, 0x98, 0x7a, 0xb3, 0x6a, 0x11, 0xad, 0x75, 0x56, 0x42, 0x7b, 0x7a, 0xe9, 0x51, 0xe0, + 0xc2, 0xcf, 0xf0, 0x06, 0x08, 0xa2, 0xd9, 0xef, 0xea, 0x0a, 0xb0, 0xc5, 0x85, 0xe3, 0xc7, 0xd0, 0x08, 0xd3, 0xa1, + 0x85, 0x73, 0xac, 0x58, 0x30, 0x85, 0xbd, 0x30, 0x9d, 0x9b, 0x18, 0xce, 0x4e, 0x6b, 0x85, 0xba, 0x61, 0xe1, 0xda, + 0xae, 0xab, 0x41, 0x3c, 0x7b, 0xf1, 0x6c, 0xe4, 0x69, 0x4e, 0xeb, 0xc8, 0x10, 0x7f, 0x2c, 0xbb, 0xa3, 0x67, 0xd8, + 0x82, 0x32, 0xf1, 0xaf, 0xd7, 0xd3, 0x28, 0x4c, 0xcd, 0xa9, 0xb7, 0xf0, 0x83, 0xbb, 0xde, 0x22, 0x0a, 0xa3, 0x64, + 0xe9, 0x8d, 0x59, 0x5f, 0xe2, 0x47, 0x31, 0x3c, 0x34, 0x8f, 0x50, 0xe8, 0x58, 0xad, 0x98, 0x2d, 0xe8, 0xeb, 0x3c, + 0xfa, 0xf3, 0x34, 0x60, 0xb7, 0x19, 0xef, 0xbe, 0x54, 0x99, 0xaa, 0xe2, 0x96, 0xa3, 0x2f, 0x80, 0x65, 0xe6, 0xa1, + 0xa5, 0x21, 0xa1, 0x42, 0x9f, 0x4b, 0x1d, 0x7b, 0x56, 0xab, 0x13, 0xb3, 0x85, 0x62, 0x75, 0x1a, 0x1b, 0x8f, 0xa3, + 0x9b, 0x01, 0x40, 0x8b, 0x1f, 0x9b, 0x09, 0x0b, 0xa6, 0xf8, 0xc6, 0xc4, 0x68, 0x56, 0xa2, 0x1d, 0x13, 0xad, 0x19, + 0xa0, 0x35, 0xb6, 0xe8, 0xc3, 0xeb, 0x5e, 0x4b, 0xb1, 0x25, 0x7e, 0xfa, 0xc8, 0x5e, 0x4a, 0x6d, 0xc9, 0xf3, 0xa7, + 0xaf, 0xb1, 0xba, 0xa3, 0xd8, 0x7d, 0xd0, 0x1f, 0x4f, 0x83, 0xe8, 0xa6, 0x37, 0xf7, 0x27, 0x13, 0x16, 0xf6, 0x11, + 0xe6, 0xbc, 0x90, 0x05, 0x81, 0xbf, 0x4c, 0xfc, 0xa4, 0xbf, 0xf0, 0x6e, 0x79, 0xab, 0x07, 0x4d, 0xad, 0xb6, 0x79, + 0xab, 0xed, 0x9d, 0x5b, 0x95, 0x9a, 0x81, 0xc8, 0x59, 0xd4, 0x0e, 0x07, 0xad, 0xa3, 0xd8, 0x95, 0x71, 0xee, 0xdc, + 0xea, 0x32, 0x66, 0xeb, 0x85, 0x17, 0xcf, 0xfc, 0xb0, 0x67, 0x67, 0xd6, 0xf5, 0x9a, 0x36, 0xc6, 0xa3, 0x6e, 0xb7, + 0x9b, 0x59, 0x13, 0xf1, 0x64, 0x4f, 0x26, 0x99, 0x35, 0x16, 0x4f, 0xd3, 0xa9, 0x6d, 0x4f, 0xa7, 0x99, 0xe5, 0x8b, + 0x82, 0x76, 0x6b, 0x3c, 0x69, 0xb7, 0x32, 0xeb, 0x46, 0xaa, 0x91, 0x59, 0x8c, 0x3f, 0xc5, 0x6c, 0xd2, 0xc7, 0x8d, + 0xc4, 0xbd, 0xae, 0x8f, 0x6c, 0x3b, 0x43, 0x0c, 0x70, 0x51, 0xc2, 0x4d, 0x68, 0x30, 0x73, 0xb9, 0xde, 0xb9, 0xa6, + 0x52, 0x74, 0x37, 0x1e, 0xd7, 0xd6, 0x9b, 0x78, 0xf1, 0xc7, 0x4b, 0x45, 0x1a, 0x85, 0xe7, 0x51, 0xb5, 0xb5, 0x98, + 0x06, 0xf3, 0xb6, 0x07, 0x69, 0x42, 0xfa, 0xa3, 0x28, 0x86, 0x33, 0x1b, 0x7b, 0x13, 0x7f, 0x95, 0xf4, 0x9c, 0xd6, + 0xf2, 0x56, 0x14, 0xf1, 0xbd, 0x5e, 0x14, 0xe0, 0xd9, 0xeb, 0x25, 0x51, 0xe0, 0x4f, 0x44, 0x51, 0xd3, 0x59, 0x72, + 0x5a, 0x7a, 0x1f, 0xf9, 0x57, 0x1f, 0x43, 0x3d, 0x7b, 0x41, 0xa0, 0x58, 0xed, 0x44, 0x61, 0x5e, 0x82, 0x46, 0x7a, + 0x8a, 0x9d, 0xd0, 0xbc, 0x60, 0x40, 0x5c, 0xe7, 0x60, 0x79, 0x9b, 0xef, 0x79, 0xe7, 0x70, 0x79, 0x9b, 0x7d, 0xbd, + 0x60, 0x13, 0xdf, 0x53, 0xb4, 0x62, 0x37, 0x39, 0x36, 0x18, 0xf2, 0xe9, 0xeb, 0x86, 0x6d, 0x2a, 0x8e, 0x05, 0xa4, + 0x53, 0xda, 0xf3, 0x17, 0x20, 0x87, 0xf1, 0xc2, 0x34, 0xcb, 0x86, 0x97, 0x59, 0xd6, 0x3f, 0xf3, 0xb5, 0x8b, 0xff, + 0xd6, 0x88, 0x16, 0x92, 0xe1, 0x6b, 0xa6, 0x5f, 0x1a, 0xa7, 0x4c, 0x76, 0xd2, 0x01, 0x32, 0x86, 0x0e, 0x3a, 0x72, + 0x65, 0xa2, 0xb7, 0x9b, 0x95, 0x69, 0x92, 0xf3, 0xea, 0xe4, 0xf3, 0x53, 0xae, 0x82, 0x14, 0x08, 0x2a, 0x9c, 0x32, + 0xf7, 0x4c, 0xf2, 0xf8, 0x01, 0xa6, 0x07, 0x2b, 0x53, 0x2c, 0xa3, 0xd7, 0x4d, 0xbc, 0xe7, 0xf9, 0xfd, 0xbc, 0xe7, + 0x5f, 0xd3, 0x5d, 0x78, 0xcf, 0xf3, 0x2f, 0xce, 0x7b, 0xbe, 0xde, 0x8c, 0x65, 0x74, 0x1e, 0xb9, 0x6a, 0x6e, 0xa6, + 0x09, 0xa4, 0x29, 0xa6, 0x2c, 0x01, 0xaf, 0xd3, 0xdf, 0x1a, 0x54, 0x46, 0xb4, 0x86, 0x44, 0x81, 0xf3, 0xa9, 0x20, + 0x66, 0x7d, 0x1b, 0xba, 0x7f, 0x8e, 0xe5, 0xe7, 0xe9, 0xd4, 0x7d, 0x1d, 0x49, 0x05, 0xf9, 0x13, 0xf7, 0x60, 0x29, + 0x45, 0x74, 0xa6, 0x37, 0xb9, 0x8f, 0x11, 0xe4, 0xbc, 0x86, 0x80, 0xb0, 0xe4, 0x50, 0x3e, 0xc9, 0x3d, 0xfd, 0xfa, + 0x65, 0x10, 0xb4, 0xdc, 0xb5, 0x56, 0x84, 0xfd, 0xda, 0xb0, 0x8c, 0x9a, 0x31, 0x21, 0x03, 0x78, 0x79, 0xf7, 0xfd, + 0x44, 0x3b, 0x8f, 0xf4, 0xcc, 0x4f, 0xde, 0x56, 0x83, 0x6e, 0x09, 0x3d, 0x97, 0x3c, 0x9c, 0x8c, 0x7b, 0xeb, 0x49, + 0xb1, 0x75, 0xf1, 0x35, 0x7d, 0x7e, 0x52, 0x1a, 0x69, 0x4f, 0xfe, 0xb0, 0x4f, 0x91, 0xc6, 0x37, 0x88, 0x31, 0x0f, + 0x4e, 0xb3, 0xe6, 0x5c, 0xde, 0x1a, 0x9f, 0x21, 0x56, 0xe9, 0x84, 0x3e, 0xf7, 0x27, 0x59, 0xa6, 0xf7, 0xc5, 0x44, + 0x48, 0x84, 0x96, 0xdd, 0xc7, 0xc4, 0x25, 0x85, 0x10, 0x88, 0x4b, 0x7c, 0xc8, 0x86, 0xfa, 0x1c, 0xbc, 0x12, 0xb8, + 0xc5, 0x35, 0x9f, 0x33, 0x55, 0xa1, 0xe9, 0x23, 0x6f, 0x15, 0x69, 0x40, 0x60, 0x46, 0x2f, 0xfb, 0x78, 0x95, 0x16, + 0x64, 0xd3, 0x9d, 0x96, 0x26, 0x07, 0xdd, 0x2a, 0x20, 0xb2, 0xb0, 0x10, 0x0b, 0x11, 0xda, 0xe1, 0x75, 0xf0, 0x21, + 0x53, 0x73, 0xde, 0x0f, 0xb7, 0xdf, 0xe0, 0x78, 0x1f, 0x3e, 0x18, 0x54, 0x94, 0x6e, 0xf7, 0x78, 0x83, 0x02, 0x2b, + 0x91, 0xdc, 0x18, 0x56, 0x72, 0xa3, 0x3c, 0x5b, 0x8b, 0xa8, 0xdc, 0xa9, 0xb7, 0x34, 0x41, 0xcb, 0x83, 0xb8, 0x97, + 0x63, 0x3c, 0x29, 0x00, 0x78, 0x7f, 0x95, 0x00, 0x6e, 0x44, 0x39, 0x0a, 0xe2, 0x9f, 0xfe, 0x78, 0x15, 0x27, 0x51, + 0xdc, 0x5b, 0x46, 0x7e, 0x98, 0xb2, 0x38, 0x23, 0xc1, 0x0a, 0xce, 0x8f, 0x98, 0x9e, 0xcb, 0x75, 0xb4, 0xf4, 0xc6, + 0x7e, 0x7a, 0xd7, 0xb3, 0x39, 0x4b, 0x61, 0xf7, 0x39, 0x77, 0x60, 0xd7, 0xd6, 0xef, 0xf1, 0xd9, 0x7c, 0x8e, 0x8c, + 0x5f, 0xbc, 0xc9, 0xce, 0xc8, 0xdb, 0xbc, 0x2f, 0xbd, 0xa5, 0xb8, 0xe4, 0xc0, 0x7e, 0x78, 0xb1, 0x39, 0x03, 0x2c, + 0x0f, 0x4b, 0x6d, 0x4f, 0xd8, 0xcc, 0x40, 0xac, 0x0d, 0xfe, 0x0e, 0xe2, 0x8f, 0xd5, 0xd1, 0x15, 0xbb, 0xbe, 0x18, + 0x38, 0x1e, 0x7d, 0x17, 0xc8, 0x7a, 0xde, 0x34, 0x65, 0xb1, 0xb1, 0x4b, 0xcd, 0x11, 0x9b, 0x46, 0x31, 0xa3, 0x1c, + 0x76, 0x4e, 0x77, 0x79, 0xbb, 0x7b, 0xf3, 0xdb, 0x87, 0x5f, 0xdf, 0x4e, 0x18, 0xa5, 0x9a, 0x68, 0x4c, 0xbf, 0xa7, + 0xb5, 0x4d, 0x7a, 0x06, 0xac, 0x21, 0xcd, 0xfc, 0x98, 0xa4, 0x20, 0x10, 0x7f, 0xac, 0x36, 0x55, 0xc8, 0x32, 0xe2, + 0x34, 0x2f, 0x66, 0x81, 0x97, 0xfa, 0xd7, 0x82, 0x67, 0x6c, 0x1f, 0x2e, 0x6f, 0xc5, 0x1a, 0x23, 0xc1, 0x7b, 0xc0, + 0x22, 0x55, 0x40, 0x11, 0x8b, 0x54, 0x2d, 0xc6, 0x45, 0xea, 0x6f, 0x8c, 0x46, 0x44, 0xcf, 0xae, 0x50, 0xfa, 0xce, + 0xf2, 0x56, 0x26, 0xd1, 0xc5, 0x67, 0x39, 0xa5, 0xae, 0xa6, 0x3d, 0x59, 0xf8, 0x93, 0x49, 0xc0, 0xb2, 0xd2, 0x42, + 0x97, 0xd7, 0x52, 0x9a, 0x9c, 0x7c, 0x1e, 0xbc, 0x51, 0x12, 0x05, 0xab, 0x94, 0xd5, 0x4f, 0x97, 0x90, 0xe8, 0x16, + 0x93, 0x83, 0xbf, 0xcb, 0xb0, 0x76, 0x80, 0xdd, 0x86, 0x6d, 0x62, 0xf7, 0x21, 0xcb, 0xa1, 0xd9, 0x2e, 0x83, 0x0e, + 0xaf, 0x72, 0xa0, 0x8d, 0x9a, 0x81, 0x18, 0x40, 0x96, 0x08, 0x7b, 0x2b, 0x96, 0xc3, 0xcb, 0xf2, 0x4c, 0x6f, 0x79, + 0x51, 0x56, 0x1e, 0xcc, 0xef, 0x73, 0xc6, 0x5e, 0xd4, 0x9f, 0xb1, 0x17, 0xe2, 0x8c, 0x6d, 0xdf, 0x99, 0x8f, 0xa6, + 0x0e, 0xfc, 0xd7, 0x2f, 0x06, 0xd4, 0xb3, 0x95, 0xf6, 0xf2, 0x56, 0x71, 0x96, 0xb7, 0x8a, 0xd9, 0x5a, 0xde, 0x2a, + 0xd8, 0x34, 0x3a, 0xd5, 0x18, 0x56, 0x4b, 0x37, 0x6c, 0x05, 0x0a, 0xe1, 0x8f, 0x5d, 0x7a, 0xe5, 0x1c, 0xc0, 0x3b, + 0xf8, 0xaa, 0xb3, 0xf9, 0xae, 0xb5, 0xfd, 0xa8, 0xd3, 0x59, 0x12, 0x48, 0x5b, 0xb7, 0x52, 0x6f, 0x34, 0x02, 0x51, + 0x66, 0x34, 0x5e, 0x25, 0xff, 0xe0, 0xf0, 0xf3, 0x49, 0xdc, 0x8a, 0x08, 0x2a, 0xed, 0x88, 0x4f, 0x41, 0x51, 0x78, + 0xcd, 0x44, 0x0b, 0xeb, 0x7c, 0x9d, 0x7a, 0x94, 0x92, 0xb1, 0x65, 0x1d, 0xd4, 0x6c, 0xf2, 0xfa, 0x89, 0xfe, 0xdd, + 0x56, 0xa9, 0x19, 0xc5, 0x7c, 0xc6, 0xb4, 0x6c, 0x9d, 0x8e, 0x87, 0xcf, 0x06, 0x5f, 0x4d, 0xbb, 0x5b, 0x0f, 0xee, + 0x85, 0xe8, 0xe9, 0x52, 0x10, 0x15, 0x4e, 0xb7, 0x78, 0x00, 0x90, 0xed, 0xad, 0x36, 0xed, 0x91, 0x8d, 0x56, 0xb7, + 0x10, 0x84, 0xa2, 0xee, 0x8e, 0x58, 0xfe, 0xd1, 0x8b, 0x03, 0xf8, 0x8f, 0xb8, 0xfa, 0xbf, 0xa6, 0x75, 0x8c, 0xfa, + 0xeb, 0xb4, 0xc4, 0xa8, 0x13, 0xab, 0x84, 0x8c, 0xf8, 0xee, 0xf5, 0xa7, 0xd3, 0x87, 0x7d, 0xb0, 0x73, 0x6d, 0xf2, + 0x47, 0xab, 0xd6, 0x7e, 0x19, 0x45, 0x01, 0xf3, 0xc2, 0xcd, 0xea, 0x62, 0x7a, 0x28, 0xb8, 0x40, 0xea, 0xc2, 0x47, + 0xe2, 0x1e, 0x41, 0xae, 0x10, 0x2a, 0x7e, 0x43, 0x57, 0x89, 0xb3, 0xa6, 0xab, 0xc4, 0xbb, 0xfb, 0xaf, 0x12, 0x3f, + 0xec, 0x74, 0x95, 0x78, 0xf7, 0xc5, 0xaf, 0x12, 0x67, 0x9b, 0x57, 0x89, 0xb3, 0x48, 0x38, 0x21, 0x1b, 0x6f, 0x56, + 0xfc, 0xe7, 0x07, 0xb2, 0xf7, 0x7d, 0x17, 0xb9, 0x1d, 0x9b, 0xd2, 0x2c, 0x9e, 0xff, 0xe6, 0x8b, 0x05, 0x6e, 0xc4, + 0x77, 0xe8, 0x93, 0x57, 0x5c, 0x2d, 0x38, 0x66, 0xc7, 0x7e, 0xa4, 0xe2, 0x20, 0x0a, 0x67, 0x3f, 0x83, 0xbd, 0x37, + 0x88, 0x03, 0x63, 0xe9, 0x85, 0x9f, 0xfc, 0x1c, 0x2d, 0x57, 0x4b, 0x54, 0x54, 0x7d, 0xf0, 0x13, 0x7f, 0x14, 0xb0, + 0x3c, 0xae, 0x25, 0x69, 0x5d, 0xb9, 0x6c, 0x1d, 0x14, 0xaf, 0xe2, 0xa7, 0x77, 0x2b, 0x7e, 0xa2, 0x63, 0x2f, 0xff, + 0x4d, 0xce, 0x89, 0x6a, 0xfd, 0x45, 0x44, 0x58, 0x88, 0x49, 0x40, 0x3f, 0xfc, 0x32, 0x72, 0x26, 0x22, 0x88, 0x95, + 0x46, 0x29, 0xdc, 0x37, 0x1a, 0xdb, 0x61, 0xd5, 0x76, 0xde, 0xac, 0x74, 0x23, 0x4f, 0xfb, 0xb1, 0x29, 0xce, 0x5f, + 0x44, 0xab, 0x84, 0x4d, 0xa2, 0x9b, 0x50, 0x35, 0x42, 0xae, 0x57, 0x8d, 0x50, 0xa6, 0x9e, 0x7f, 0x53, 0x56, 0x38, + 0xaa, 0xd6, 0x12, 0xe6, 0xd0, 0x24, 0x0d, 0xb6, 0x89, 0x43, 0x54, 0x45, 0xa0, 0xa8, 0xfe, 0x9e, 0xa6, 0x45, 0xee, + 0xc3, 0xbe, 0x14, 0x9e, 0x27, 0x91, 0xc5, 0xa5, 0xc2, 0x89, 0x16, 0x0a, 0xe1, 0xa2, 0x88, 0xbd, 0x5d, 0xb3, 0x70, + 0xfc, 0x0d, 0xc5, 0xa5, 0x2c, 0xde, 0x82, 0xae, 0x2a, 0x5b, 0xf1, 0xf5, 0xe0, 0x91, 0xa8, 0xe9, 0xf1, 0x95, 0x34, + 0x8d, 0x6f, 0xaf, 0x59, 0x1c, 0x78, 0x77, 0x9a, 0x9e, 0x45, 0xe1, 0x8f, 0x30, 0x01, 0xaf, 0xa3, 0x9b, 0x50, 0xae, + 0x80, 0x09, 0xe2, 0x6b, 0xf6, 0x52, 0x6d, 0xcc, 0x74, 0x88, 0x14, 0x22, 0x41, 0xe0, 0x5b, 0x4b, 0x6f, 0xc6, 0xfe, + 0xcb, 0xa0, 0x7f, 0xff, 0x5b, 0xcf, 0x8c, 0x77, 0x51, 0xde, 0xd1, 0x2f, 0xcb, 0x1d, 0xba, 0x79, 0xf2, 0x64, 0xaf, + 0x79, 0xd8, 0xda, 0x38, 0x60, 0x5e, 0x2c, 0xa0, 0xa8, 0xf9, 0x5a, 0x6f, 0x3c, 0x05, 0x00, 0xc5, 0x79, 0xb4, 0x1a, + 0xcf, 0xd1, 0x5b, 0xf8, 0xcb, 0x8d, 0x37, 0x85, 0x36, 0x59, 0x72, 0x61, 0x5f, 0xe6, 0x43, 0xaf, 0x14, 0x15, 0xb3, + 0x80, 0xfd, 0x9f, 0x42, 0xd2, 0xaf, 0x7f, 0xe3, 0x34, 0x6c, 0xee, 0x9a, 0x3c, 0xd0, 0xd8, 0x83, 0x36, 0x6f, 0xdf, + 0x87, 0x58, 0x40, 0x14, 0x4e, 0x5b, 0x28, 0xe9, 0xea, 0x91, 0x4c, 0x56, 0x9d, 0x34, 0x39, 0x75, 0x4d, 0x53, 0x56, + 0x1e, 0xd1, 0x0b, 0xb3, 0x4a, 0x56, 0x23, 0x06, 0xe3, 0xd8, 0xaa, 0x82, 0x64, 0xb8, 0x37, 0x05, 0x43, 0xf4, 0x55, + 0x7d, 0xb7, 0xf0, 0x43, 0x03, 0x33, 0xcf, 0x6e, 0xbe, 0xf1, 0x6e, 0x21, 0xf7, 0x22, 0x20, 0xb7, 0xea, 0x2b, 0x28, + 0x34, 0xe4, 0x18, 0x45, 0xde, 0x64, 0xa2, 0xa9, 0xb5, 0x33, 0x21, 0xb4, 0x81, 0xc3, 0xaf, 0x14, 0x45, 0x51, 0xf2, + 0x6b, 0x84, 0x92, 0xdf, 0x23, 0xb0, 0x1c, 0xaf, 0x03, 0xa0, 0x2d, 0xc9, 0x96, 0xb7, 0x54, 0x02, 0x37, 0x03, 0xb4, + 0x9f, 0x16, 0x05, 0x3c, 0xbd, 0x10, 0x18, 0xb7, 0x50, 0x81, 0xb8, 0xd0, 0x83, 0xea, 0xdb, 0x8b, 0x21, 0x0b, 0x61, + 0x4f, 0xc1, 0x0b, 0x3b, 0xbe, 0xe5, 0x92, 0x60, 0xc5, 0xa6, 0xc7, 0x61, 0x9f, 0xd5, 0xe7, 0xa1, 0x09, 0x25, 0x2c, + 0x08, 0x5a, 0x87, 0x4a, 0x5a, 0x49, 0x83, 0xd5, 0xe0, 0x46, 0xbc, 0x17, 0xdd, 0xa6, 0x0b, 0x16, 0xae, 0x54, 0x03, + 0xac, 0x4e, 0x30, 0x2f, 0x10, 0xd4, 0x79, 0x4d, 0xcc, 0x16, 0x60, 0x9b, 0xfa, 0x2f, 0xe7, 0x44, 0x0b, 0x85, 0xa9, + 0x8a, 0x67, 0x8c, 0x79, 0xd8, 0x9d, 0x84, 0xe3, 0xb6, 0x2a, 0x85, 0xe0, 0x4b, 0x1a, 0x95, 0xb1, 0x39, 0x0f, 0xb4, + 0x85, 0x9c, 0x02, 0xd9, 0x88, 0x71, 0x71, 0x91, 0x98, 0x76, 0xcd, 0xab, 0x2e, 0x5a, 0xae, 0x91, 0xf1, 0x2a, 0x82, + 0xa2, 0x58, 0xdf, 0x6c, 0x86, 0xc3, 0x09, 0xc9, 0x10, 0x1a, 0xdb, 0x19, 0x6f, 0xb4, 0xd3, 0x30, 0xe8, 0x8f, 0xec, + 0x8e, 0x08, 0x09, 0x4d, 0xd5, 0x47, 0x76, 0x07, 0xc6, 0xe1, 0xa7, 0x20, 0x4d, 0x51, 0xb7, 0xa0, 0x6b, 0x03, 0xd2, + 0x0b, 0x8f, 0x21, 0x41, 0xc6, 0x96, 0x03, 0x64, 0x67, 0x5b, 0xb0, 0x38, 0x05, 0x41, 0x35, 0x92, 0xbe, 0x38, 0xc4, + 0x3c, 0x4e, 0x82, 0x56, 0x3b, 0xc7, 0x66, 0xcd, 0xd1, 0xd0, 0x9f, 0x39, 0xb6, 0xbd, 0xbf, 0x51, 0x1f, 0x04, 0xd9, + 0x75, 0xb5, 0x75, 0x23, 0x75, 0x1d, 0xdb, 0xf4, 0x9f, 0x59, 0xad, 0xfe, 0x06, 0x8d, 0x96, 0xf2, 0x57, 0x0d, 0x51, + 0xfc, 0x35, 0x78, 0xbc, 0xd6, 0x36, 0x0e, 0xa4, 0x5e, 0x8d, 0x3b, 0x80, 0xb0, 0x65, 0x5c, 0xfe, 0x35, 0xdc, 0x24, + 0xfd, 0x94, 0x3d, 0x8b, 0x72, 0xa9, 0x0f, 0x21, 0x03, 0xa3, 0x06, 0xc7, 0xe8, 0x4f, 0xca, 0x73, 0x45, 0xa3, 0xe3, + 0xa3, 0xeb, 0xc3, 0xbe, 0xc0, 0x28, 0x22, 0x30, 0x8f, 0xdc, 0x40, 0xa5, 0xc7, 0xa4, 0x8a, 0xe1, 0x78, 0xae, 0x37, + 0x56, 0x68, 0xf4, 0xb6, 0x72, 0x0b, 0xd8, 0x7e, 0x03, 0xf9, 0xb4, 0x46, 0x10, 0x59, 0x12, 0x6a, 0x40, 0xbe, 0xd6, + 0x7b, 0x1b, 0x5c, 0x2d, 0xcb, 0xcd, 0x95, 0x89, 0xe4, 0xee, 0x8d, 0x21, 0xd1, 0x41, 0x1d, 0x5a, 0xde, 0x5e, 0x3d, + 0xb9, 0x7b, 0x60, 0x93, 0x2c, 0x9c, 0x94, 0x1b, 0xac, 0xd0, 0xaf, 0xdd, 0x9b, 0x2b, 0x61, 0x14, 0x48, 0x64, 0x1c, + 0xd5, 0x60, 0x94, 0x2c, 0x0a, 0x71, 0xf3, 0xd3, 0x71, 0xf3, 0x77, 0xe2, 0x62, 0xf0, 0x03, 0xca, 0x42, 0x92, 0x7f, + 0x26, 0x09, 0xc5, 0x21, 0x5b, 0x26, 0xc6, 0xed, 0xd2, 0x04, 0x23, 0xda, 0xb8, 0x13, 0x53, 0xe1, 0xae, 0x58, 0x7c, + 0xe3, 0xf3, 0xfc, 0x57, 0xbb, 0x4a, 0xad, 0xfd, 0xfb, 0xa5, 0xd6, 0xe9, 0x7d, 0x52, 0x6b, 0x8a, 0x49, 0xc3, 0xed, + 0x41, 0x45, 0x6c, 0x1e, 0xc1, 0x9c, 0xcb, 0xd1, 0x8d, 0x4a, 0xa2, 0x6e, 0x0c, 0x61, 0x53, 0x63, 0x45, 0x4a, 0xad, + 0x91, 0x03, 0x22, 0x8a, 0xbf, 0xa5, 0x0b, 0x8a, 0x50, 0xa8, 0xcb, 0xb2, 0xf1, 0xb3, 0x42, 0x36, 0x4e, 0xb7, 0x9a, + 0x22, 0x1a, 0x89, 0xe0, 0xfe, 0xa5, 0x48, 0x3f, 0xf9, 0xed, 0xa0, 0x88, 0xf8, 0x53, 0x40, 0x2a, 0xc5, 0xb0, 0x29, + 0x2e, 0x1a, 0x52, 0x64, 0x24, 0x71, 0xcb, 0x28, 0x07, 0x48, 0x2a, 0x57, 0x2d, 0x42, 0xd8, 0x14, 0xe5, 0x20, 0x75, + 0x47, 0x90, 0xf3, 0x62, 0x79, 0xdb, 0x94, 0x63, 0x98, 0xc8, 0xaf, 0xa5, 0x4d, 0x92, 0x07, 0x1b, 0xa1, 0x09, 0x16, + 0x62, 0xfa, 0x8a, 0x5e, 0x3b, 0xb7, 0x81, 0x40, 0x20, 0x6b, 0x62, 0x23, 0xdd, 0x2f, 0x9d, 0xa7, 0x1c, 0xcd, 0x85, + 0xea, 0xda, 0x41, 0xea, 0x4e, 0x9a, 0x60, 0x59, 0x1e, 0x81, 0x73, 0x7d, 0x29, 0x49, 0x10, 0x7a, 0xb6, 0x62, 0xf7, + 0x6b, 0x18, 0x00, 0xa4, 0xff, 0xd5, 0x67, 0xce, 0x0a, 0x80, 0x24, 0x52, 0xb1, 0x65, 0x9d, 0x3f, 0x1e, 0x62, 0x93, + 0x2c, 0xd9, 0xb1, 0xea, 0x66, 0x9f, 0x24, 0xef, 0x59, 0xf3, 0x48, 0x24, 0x65, 0x71, 0x3e, 0xaf, 0xd1, 0x13, 0x70, + 0xf0, 0x5d, 0x16, 0xaf, 0x42, 0x4c, 0xbd, 0x6b, 0xa6, 0xb1, 0x37, 0xfe, 0xb8, 0x96, 0xfa, 0xe3, 0x22, 0x51, 0x10, + 0x17, 0x97, 0x95, 0x0a, 0x7d, 0x0f, 0x33, 0x55, 0xb1, 0x9e, 0xd5, 0x4a, 0x24, 0x41, 0x4d, 0xef, 0x91, 0xdd, 0xf6, + 0x5e, 0x4c, 0x0f, 0x2a, 0xf2, 0xd3, 0x56, 0xa7, 0x2c, 0x5d, 0xcf, 0xe1, 0x58, 0x44, 0xbf, 0xf2, 0x98, 0x4d, 0x7f, + 0x7c, 0xd7, 0x09, 0xef, 0xb3, 0xb2, 0x46, 0x9f, 0x03, 0x02, 0x7c, 0x5f, 0x52, 0x4c, 0xcb, 0x6a, 0x9a, 0x8d, 0x92, + 0x26, 0xb0, 0xa6, 0x7e, 0x10, 0x98, 0x01, 0xb8, 0x31, 0xac, 0x3f, 0x6b, 0x78, 0xd8, 0xce, 0x0a, 0x72, 0x24, 0x7e, + 0x46, 0x3b, 0xe5, 0x9d, 0x92, 0xce, 0x57, 0x8b, 0xd1, 0x5a, 0x16, 0x94, 0x4b, 0xf2, 0xf3, 0x4d, 0x99, 0xb9, 0xdc, + 0xed, 0x74, 0x3a, 0x2d, 0x4b, 0x8d, 0x6d, 0xe5, 0x00, 0x25, 0xbf, 0x8f, 0x6c, 0xdb, 0xae, 0xce, 0x6f, 0xd3, 0x41, + 0xa1, 0x83, 0x61, 0xa2, 0x10, 0xbe, 0x7b, 0xff, 0x9e, 0xfa, 0x83, 0xa0, 0xa5, 0xa6, 0x9a, 0xce, 0x23, 0x6d, 0xb5, + 0xff, 0x08, 0x50, 0x10, 0x35, 0xdc, 0x77, 0xfc, 0x37, 0xf7, 0xca, 0x96, 0x96, 0xaa, 0x07, 0xf8, 0x61, 0x1f, 0xdf, + 0xb3, 0xd7, 0x77, 0xf8, 0xb4, 0x69, 0x7b, 0x67, 0x56, 0x41, 0x76, 0x4b, 0x36, 0x4b, 0x7d, 0xb2, 0x54, 0xf2, 0x53, + 0xb6, 0x48, 0x7a, 0x63, 0x86, 0x0a, 0x52, 0x4b, 0xa2, 0xb6, 0x68, 0xd5, 0x63, 0xce, 0xc0, 0x8e, 0xcb, 0x11, 0x78, + 0xd8, 0x56, 0x50, 0x59, 0xb5, 0xa1, 0x59, 0x13, 0x9d, 0x20, 0x15, 0x5b, 0x6f, 0x2a, 0x9c, 0x70, 0x9b, 0x76, 0xec, + 0x3f, 0x95, 0xea, 0x29, 0xc0, 0x9d, 0xae, 0x85, 0xb5, 0x09, 0x29, 0x4f, 0xf0, 0xef, 0x5c, 0x39, 0xf7, 0x62, 0x79, + 0x5b, 0x36, 0xee, 0xea, 0x82, 0xba, 0xa9, 0x20, 0x65, 0x04, 0x75, 0x1d, 0xea, 0xcb, 0x4d, 0x80, 0xa6, 0xb2, 0x75, + 0x0b, 0x58, 0xd0, 0x88, 0x29, 0xa8, 0xe8, 0x08, 0x73, 0x50, 0xf1, 0x3a, 0x0b, 0x3b, 0xaf, 0x90, 0xef, 0xe3, 0x2f, + 0xc8, 0x8d, 0x0e, 0x49, 0x56, 0xfe, 0x64, 0x3c, 0xef, 0xa2, 0x72, 0xaf, 0xb4, 0x55, 0xd1, 0x54, 0x06, 0xf7, 0x80, + 0xb8, 0x91, 0x2a, 0xab, 0x38, 0x30, 0x97, 0x31, 0x9b, 0xfa, 0xb7, 0x9a, 0xbe, 0xde, 0x1c, 0x77, 0x73, 0xf3, 0x4e, + 0x07, 0xf4, 0x1a, 0x9b, 0x53, 0xb5, 0x93, 0x6a, 0xaf, 0xaa, 0xc3, 0x16, 0x70, 0xc2, 0x0a, 0x80, 0xcf, 0xac, 0x82, + 0x46, 0x43, 0x4a, 0x05, 0xf7, 0xd1, 0xa0, 0xf3, 0xb7, 0x32, 0xb2, 0x16, 0xe3, 0xc4, 0xe6, 0xea, 0xab, 0x50, 0xdb, + 0x42, 0x33, 0x08, 0x73, 0xc7, 0xb1, 0x13, 0x3e, 0x9b, 0xb0, 0x63, 0x64, 0x74, 0xe5, 0xe0, 0x0e, 0xc2, 0x53, 0x6a, + 0x52, 0xca, 0x15, 0x3a, 0xa5, 0xa8, 0x4b, 0xf8, 0xa1, 0x56, 0x78, 0x7f, 0x5e, 0x92, 0xc6, 0xf3, 0xa0, 0x13, 0x2d, + 0x7d, 0xa7, 0xda, 0x0b, 0x3f, 0xdc, 0xbd, 0xae, 0x77, 0xbb, 0x73, 0x5d, 0x60, 0x0e, 0x77, 0xae, 0x0c, 0xdc, 0x25, + 0x56, 0x3e, 0x4f, 0xdd, 0x1f, 0x24, 0xe5, 0x81, 0x1c, 0xa6, 0x51, 0xc5, 0xaf, 0xe8, 0x46, 0xff, 0xd3, 0xca, 0x1d, + 0x1e, 0x9f, 0xdc, 0x2e, 0x02, 0xe5, 0x9a, 0xc5, 0x09, 0xa4, 0xb1, 0x50, 0x1d, 0xcb, 0x56, 0x15, 0x34, 0xe8, 0xf7, + 0xc3, 0x99, 0xab, 0xfe, 0x72, 0xfe, 0xc6, 0xec, 0xaa, 0x27, 0x60, 0x8e, 0x71, 0x3d, 0x43, 0x16, 0xf7, 0xcc, 0xbb, + 0x63, 0xf1, 0x55, 0x8b, 0x7b, 0xfc, 0x10, 0x73, 0x8b, 0x65, 0x4a, 0x4b, 0xdd, 0x21, 0x11, 0xbd, 0x72, 0xed, 0xb3, + 0x9b, 0x97, 0xd1, 0xad, 0xab, 0x02, 0x62, 0x75, 0x5a, 0x5d, 0xc5, 0x69, 0x1d, 0x58, 0x87, 0x5d, 0x75, 0xf0, 0x95, + 0xa2, 0x1c, 0x4f, 0xd8, 0x34, 0x19, 0xa0, 0x38, 0xe6, 0x18, 0xf9, 0x41, 0xfa, 0xad, 0x28, 0xd6, 0x38, 0x48, 0x4c, + 0x47, 0x59, 0xf3, 0x47, 0x45, 0x01, 0x64, 0xd4, 0x53, 0x1e, 0x4d, 0x5b, 0xd3, 0x83, 0xe9, 0x8b, 0x3e, 0x2f, 0xce, + 0xbe, 0x2a, 0x55, 0x37, 0xe8, 0xdf, 0x96, 0xf4, 0x59, 0x92, 0xc6, 0xd1, 0x47, 0xc6, 0x79, 0x49, 0x25, 0x17, 0x14, + 0x55, 0x3f, 0x6d, 0x6d, 0xf6, 0xe4, 0x74, 0x47, 0xe3, 0x69, 0xab, 0xa8, 0x8e, 0x30, 0xee, 0xe7, 0x40, 0x1e, 0xef, + 0x0b, 0xd0, 0x8f, 0xe5, 0x69, 0x72, 0xcc, 0xba, 0x89, 0x72, 0x54, 0x3e, 0xc6, 0x99, 0x18, 0xdf, 0x31, 0xe4, 0x79, + 0x2b, 0xbc, 0x17, 0x13, 0xfc, 0xcc, 0x55, 0x7f, 0x74, 0x5a, 0x5d, 0xc3, 0x71, 0x0e, 0xad, 0xc3, 0xee, 0xd8, 0x36, + 0x0e, 0xac, 0x03, 0xb3, 0x6d, 0x1d, 0x1a, 0x5d, 0xb3, 0x6b, 0x74, 0xbf, 0xeb, 0x8e, 0xcd, 0x03, 0xeb, 0xc0, 0xb0, + 0xcd, 0x2e, 0x14, 0x9a, 0x5d, 0xb3, 0x7b, 0x6d, 0x1e, 0x74, 0xc7, 0x36, 0x96, 0xb6, 0xac, 0x4e, 0xc7, 0x74, 0x6c, + 0xab, 0xd3, 0x31, 0x3a, 0xd6, 0xe1, 0xa1, 0xe9, 0xb4, 0xad, 0xc3, 0xc3, 0xb3, 0x4e, 0xd7, 0x6a, 0xc3, 0xbb, 0x76, + 0x7b, 0xdc, 0xb6, 0x1c, 0xc7, 0x84, 0xbf, 0x8c, 0xae, 0xd5, 0xa2, 0x1f, 0x8e, 0x63, 0xb5, 0x1d, 0xc3, 0x0e, 0x3a, + 0x2d, 0xeb, 0xf0, 0x85, 0x81, 0x7f, 0x63, 0x35, 0x03, 0xff, 0x82, 0x66, 0x8c, 0x17, 0x56, 0xeb, 0x90, 0x7e, 0x61, + 0x83, 0xd7, 0x07, 0xdd, 0xbf, 0xaa, 0xfb, 0x8d, 0x63, 0x70, 0x68, 0x0c, 0xdd, 0x8e, 0xd5, 0x6e, 0x1b, 0x07, 0x8e, + 0xd5, 0x6d, 0xcf, 0xcd, 0x83, 0x96, 0x75, 0x78, 0x34, 0x36, 0x1d, 0xeb, 0xe8, 0xc8, 0xb0, 0xcd, 0xb6, 0xd5, 0x32, + 0x1c, 0xeb, 0xa0, 0x8d, 0x3f, 0xda, 0x56, 0xeb, 0xfa, 0xe8, 0x85, 0x75, 0xd8, 0x99, 0x1f, 0x5a, 0x07, 0x1f, 0x0e, + 0xba, 0x56, 0xab, 0x3d, 0x6f, 0x1f, 0x5a, 0xad, 0xa3, 0xeb, 0x43, 0xeb, 0x60, 0x6e, 0xb6, 0x0e, 0xb7, 0x7e, 0xe9, + 0xb4, 0x2c, 0x98, 0x23, 0x7c, 0x0d, 0x2f, 0x0c, 0xfe, 0x02, 0xfe, 0xcc, 0xf1, 0xdb, 0x3f, 0xb0, 0x99, 0x64, 0xf3, + 0xd3, 0x17, 0x56, 0xf7, 0x68, 0x4c, 0xd5, 0xa1, 0xc0, 0x14, 0x35, 0xe0, 0x93, 0x6b, 0x93, 0xba, 0xc5, 0xe6, 0x4c, + 0xd1, 0x90, 0xf8, 0xc3, 0x3b, 0xbb, 0x36, 0xa1, 0x63, 0xea, 0xf7, 0xdf, 0xda, 0x4e, 0xbe, 0xe4, 0xc7, 0xfb, 0x33, + 0xda, 0xfa, 0xb3, 0xc1, 0x57, 0xc7, 0x70, 0xb8, 0x07, 0x43, 0xe3, 0xd7, 0x26, 0xa5, 0xe4, 0xdf, 0xef, 0x57, 0x4a, + 0xbe, 0x5c, 0xed, 0xa2, 0x94, 0xfc, 0xfb, 0x17, 0x57, 0x4a, 0xfe, 0x5a, 0xf5, 0xad, 0x79, 0x53, 0xcd, 0x7d, 0xfd, + 0xc3, 0xba, 0x2a, 0x72, 0x48, 0x3c, 0xed, 0xe2, 0xa7, 0xd5, 0x25, 0x44, 0xad, 0x7f, 0x13, 0xb9, 0x2f, 0x57, 0x25, + 0x83, 0xcf, 0x08, 0x70, 0xec, 0x9b, 0x88, 0x70, 0xec, 0x87, 0x95, 0x0b, 0x56, 0x66, 0x9c, 0xcd, 0xf1, 0x27, 0xe6, + 0xdc, 0x0b, 0xa6, 0x39, 0x8b, 0x04, 0x25, 0x7d, 0x2c, 0x06, 0xbf, 0x79, 0x20, 0xcf, 0x70, 0x93, 0x59, 0x2d, 0xc2, + 0x04, 0x2c, 0x82, 0xc1, 0x92, 0x63, 0x1a, 0x67, 0x95, 0x8f, 0x2d, 0x11, 0xe7, 0xff, 0x8a, 0x7b, 0x14, 0x37, 0xbe, + 0x47, 0x03, 0xe0, 0xfa, 0xd6, 0x9d, 0xcd, 0x76, 0x15, 0xb0, 0xac, 0x13, 0x06, 0xd2, 0xc0, 0xed, 0xd7, 0xbd, 0x2f, + 0x9b, 0xe1, 0x56, 0x0c, 0xaf, 0x9b, 0x21, 0x05, 0x48, 0xaa, 0xdf, 0x3b, 0x65, 0x33, 0xde, 0xfb, 0x86, 0x59, 0xd3, + 0x7d, 0xe9, 0xf3, 0x2d, 0x36, 0xc4, 0x79, 0xc3, 0xd5, 0xa9, 0x5a, 0x97, 0xf8, 0xb4, 0xfa, 0x09, 0x29, 0x2e, 0xa8, + 0x85, 0xa1, 0x71, 0xc1, 0xa9, 0xda, 0x0a, 0xf2, 0x3b, 0xb6, 0xf4, 0xae, 0xd4, 0xa6, 0x6c, 0x9c, 0xfc, 0x6c, 0x8d, + 0xf7, 0x0a, 0xff, 0x57, 0xe0, 0x44, 0x39, 0xc7, 0x33, 0x8a, 0xe4, 0x79, 0x5e, 0x4b, 0xed, 0x92, 0x34, 0x22, 0x9b, + 0x3b, 0xeb, 0x4d, 0x5e, 0xb4, 0xd1, 0x2d, 0xc1, 0x61, 0x0b, 0xc1, 0x05, 0x61, 0xf7, 0xe4, 0x04, 0x90, 0x91, 0xa3, + 0x06, 0xfa, 0x39, 0x6c, 0x6b, 0x4c, 0xd4, 0x7b, 0x04, 0x9b, 0x98, 0x7b, 0x02, 0x2a, 0x72, 0x20, 0xd5, 0xf5, 0x34, + 0x88, 0xbc, 0xb4, 0x87, 0x6c, 0x9a, 0xc4, 0xf2, 0xb6, 0xd0, 0x63, 0xa1, 0xbf, 0xc5, 0x98, 0x4e, 0x6e, 0x98, 0x37, + 0x82, 0x9e, 0x0f, 0xdb, 0xec, 0xef, 0x72, 0x87, 0xb3, 0x75, 0xc9, 0x1c, 0xc5, 0xe9, 0x1c, 0x19, 0xce, 0xa1, 0x61, + 0x1d, 0x75, 0xf4, 0x4c, 0x1c, 0x38, 0xb9, 0xc9, 0xd2, 0x84, 0x80, 0x03, 0x44, 0x0e, 0xa6, 0x1f, 0xfa, 0xa9, 0xef, + 0x05, 0x19, 0xf0, 0xc3, 0xe5, 0x4b, 0xca, 0xdf, 0x57, 0x49, 0x0a, 0x63, 0x14, 0x4c, 0x2f, 0x3a, 0x7f, 0x98, 0x23, + 0x96, 0xde, 0x30, 0x16, 0x36, 0x18, 0xc6, 0x54, 0x7d, 0x49, 0x7e, 0x3f, 0xcb, 0xfa, 0x8c, 0xac, 0xd6, 0x46, 0x69, + 0xc8, 0xf7, 0x87, 0x70, 0x7c, 0xc8, 0x86, 0xc6, 0x77, 0x4d, 0x08, 0xf7, 0x97, 0xfb, 0x11, 0x6e, 0xca, 0x76, 0x41, + 0xb8, 0xbf, 0x7c, 0x71, 0x84, 0xfb, 0x9d, 0x8c, 0x70, 0x4b, 0xfe, 0x83, 0x85, 0x86, 0xe9, 0x3d, 0x3e, 0x6b, 0xe0, + 0x22, 0xfb, 0x5c, 0xdd, 0x27, 0x06, 0x5e, 0xd5, 0x8b, 0x9c, 0xb9, 0x7f, 0x59, 0xc9, 0x16, 0xd4, 0x28, 0x00, 0xc5, + 0x6c, 0x92, 0x3e, 0xba, 0x2e, 0xfb, 0xe0, 0xea, 0x26, 0xc2, 0x30, 0x40, 0x9b, 0xdf, 0x87, 0x69, 0x60, 0xbd, 0xe3, + 0xf7, 0x48, 0x50, 0xe8, 0xbe, 0x89, 0xe2, 0x85, 0x87, 0x89, 0x4d, 0x54, 0x1d, 0xdc, 0xe9, 0xe0, 0xc1, 0x86, 0x40, + 0x20, 0xe3, 0x28, 0x9c, 0xe4, 0x5a, 0x49, 0xe6, 0x5e, 0x10, 0xc7, 0xad, 0xde, 0x31, 0x2f, 0x56, 0x0d, 0x7a, 0x0d, + 0x8b, 0xfb, 0xac, 0x6d, 0x3f, 0x6b, 0x1d, 0x3c, 0x3b, 0xb4, 0xe1, 0x7f, 0x87, 0xb5, 0x33, 0x83, 0x57, 0x5c, 0x44, + 0x61, 0x3a, 0x2f, 0x6a, 0x36, 0x55, 0xbb, 0x61, 0xec, 0x63, 0x51, 0xeb, 0xa8, 0xbe, 0xd2, 0xc4, 0xbb, 0x2b, 0xea, + 0xd4, 0xd6, 0x98, 0x47, 0x2b, 0x09, 0xac, 0x1a, 0x68, 0xfc, 0x70, 0x05, 0x72, 0x76, 0xa9, 0x86, 0xfc, 0x9a, 0x0f, + 0xb7, 0x18, 0x17, 0x6b, 0x67, 0x97, 0x22, 0x73, 0x83, 0xda, 0x17, 0xc9, 0xfc, 0xee, 0x9d, 0x41, 0xae, 0xa2, 0xb4, + 0x31, 0xd3, 0x15, 0xe6, 0x53, 0x84, 0x3c, 0x57, 0x4c, 0x2c, 0x90, 0x47, 0x0b, 0x94, 0xc6, 0xab, 0x70, 0xac, 0xe1, + 0x4f, 0x6f, 0x94, 0x68, 0xfe, 0x7e, 0x6c, 0xf1, 0x8e, 0x75, 0x5c, 0x35, 0x6f, 0x60, 0x17, 0xa9, 0xee, 0x13, 0xb1, + 0x2a, 0xde, 0xb3, 0xd4, 0x88, 0x51, 0x8f, 0x4d, 0x4b, 0x6b, 0xba, 0xde, 0xb3, 0xfc, 0xc3, 0x67, 0xa9, 0x11, 0x3e, + 0x07, 0xdd, 0xa7, 0x6b, 0x3f, 0x79, 0x42, 0xb5, 0xf6, 0x5c, 0x31, 0xac, 0x93, 0x71, 0x91, 0x0f, 0x43, 0xf1, 0x66, + 0x11, 0xa5, 0xc4, 0xe8, 0x8d, 0x8d, 0xe8, 0xf9, 0xf3, 0x81, 0xeb, 0xe8, 0xa3, 0x98, 0x79, 0x1f, 0x33, 0x11, 0x64, + 0x3c, 0xc4, 0xac, 0xb8, 0x67, 0xbb, 0x19, 0x1a, 0xe9, 0xb5, 0xae, 0xb4, 0x4b, 0xb8, 0x33, 0xd9, 0xc2, 0x1d, 0x81, + 0x63, 0x2f, 0x77, 0x8f, 0x97, 0x80, 0x2b, 0x13, 0x19, 0xfc, 0x88, 0x3a, 0x57, 0x73, 0x2f, 0xf9, 0x21, 0x89, 0xc2, + 0x5f, 0x96, 0x10, 0x72, 0xb9, 0xb0, 0x28, 0x12, 0x97, 0xb1, 0xb6, 0x65, 0x5b, 0xb6, 0x9a, 0xb7, 0x37, 0xf5, 0x67, + 0xee, 0x3a, 0x4a, 0xbd, 0xde, 0x9e, 0x63, 0x04, 0xd1, 0x0c, 0xdc, 0xeb, 0x52, 0x3f, 0x0d, 0x58, 0x4f, 0x55, 0xc1, + 0xcf, 0x6e, 0x41, 0xd7, 0xf5, 0x8c, 0x3b, 0x3d, 0x78, 0x31, 0xe4, 0x50, 0x8f, 0xef, 0x84, 0x87, 0x2e, 0x46, 0x6e, + 0xff, 0x11, 0x68, 0xa4, 0xa6, 0x6a, 0x20, 0x32, 0x60, 0x71, 0x62, 0xca, 0x4e, 0x44, 0x3d, 0x05, 0xbe, 0xd1, 0x55, + 0x3e, 0xb6, 0x69, 0xec, 0x2d, 0x20, 0xc9, 0xef, 0x3a, 0x33, 0x38, 0x02, 0x56, 0x39, 0x06, 0x56, 0x9c, 0x17, 0x87, + 0x86, 0xd2, 0x72, 0x0c, 0xc5, 0x06, 0x2c, 0xac, 0x66, 0xc6, 0x3a, 0xbb, 0xec, 0xdf, 0x67, 0x07, 0x41, 0x68, 0xe7, + 0x11, 0x8d, 0x83, 0x2c, 0x20, 0xb8, 0x86, 0x29, 0xa5, 0x8c, 0x3d, 0x9a, 0x94, 0xce, 0xd3, 0x27, 0x5d, 0xe8, 0x39, + 0xbb, 0x4d, 0x75, 0x50, 0x28, 0x89, 0x2a, 0xbe, 0xbe, 0x46, 0x3f, 0x62, 0x3f, 0x54, 0xfc, 0x4f, 0x9f, 0x34, 0x1f, + 0x7c, 0x9c, 0x5c, 0x69, 0x7e, 0xe0, 0x59, 0x2f, 0x4d, 0x98, 0x5f, 0x68, 0xef, 0x71, 0xb2, 0xc0, 0x01, 0x11, 0xfe, + 0x2d, 0x8a, 0xc5, 0x0f, 0x6e, 0x3d, 0x61, 0x05, 0x5e, 0x38, 0x03, 0x4c, 0xe7, 0x85, 0xb3, 0x0d, 0x2b, 0x2d, 0x72, + 0x85, 0xae, 0x94, 0x16, 0x4d, 0x15, 0x16, 0x54, 0xc9, 0xcb, 0xbb, 0x73, 0x6f, 0xf6, 0x93, 0xb7, 0x60, 0x9a, 0x0a, + 0xc4, 0x0f, 0x3d, 0x77, 0x0b, 0x05, 0xef, 0x73, 0xf7, 0xe9, 0xf1, 0x82, 0xa5, 0x1e, 0x69, 0x87, 0xe0, 0x4e, 0x0c, + 0x5c, 0x82, 0xc2, 0xe9, 0x0f, 0x8f, 0x83, 0xe1, 0x52, 0x62, 0x2f, 0x22, 0x1f, 0x86, 0xc2, 0xc9, 0x97, 0x89, 0x86, + 0xa0, 0xae, 0x63, 0x90, 0x1f, 0xc2, 0xd8, 0xc3, 0xe4, 0x3e, 0x6e, 0x18, 0xa9, 0x83, 0xa7, 0xb9, 0xcb, 0x66, 0xd3, + 0x22, 0x04, 0x7e, 0xf8, 0xf1, 0x22, 0x66, 0xc1, 0x3f, 0xdc, 0xa7, 0x40, 0xcf, 0x9f, 0x5e, 0xaa, 0x7a, 0x3f, 0xb5, + 0xe6, 0x31, 0x9b, 0xba, 0x4f, 0xe1, 0x9e, 0xda, 0x43, 0xab, 0x59, 0x60, 0xe6, 0x9f, 0xdf, 0x2e, 0x02, 0x03, 0x6f, + 0xfd, 0x04, 0x8b, 0xda, 0x6e, 0x15, 0x41, 0xd6, 0xdb, 0x3b, 0xdd, 0xf5, 0x07, 0xfc, 0x12, 0x0f, 0x17, 0xc3, 0x75, + 0xe9, 0xea, 0xed, 0xf4, 0xf1, 0x5a, 0x3d, 0x0a, 0xbc, 0xf1, 0xc7, 0x3e, 0xbd, 0x29, 0x3d, 0x98, 0x40, 0xc4, 0xc7, + 0xde, 0xb2, 0x87, 0x54, 0x57, 0x2e, 0x04, 0xa7, 0x6a, 0x2a, 0xcd, 0x19, 0xbe, 0xda, 0xbd, 0x8c, 0x5b, 0x79, 0x8d, + 0x3d, 0x63, 0x57, 0x37, 0x73, 0x3f, 0x65, 0xa2, 0x2b, 0x7c, 0xc8, 0x32, 0x71, 0x7f, 0xa7, 0x9b, 0x2b, 0xde, 0xb7, + 0xad, 0xb6, 0xe2, 0x74, 0xbf, 0xeb, 0x5c, 0x3b, 0xf6, 0xbc, 0xe5, 0x58, 0xdd, 0x0f, 0x4e, 0x77, 0xde, 0xb6, 0x8e, + 0x02, 0xb3, 0x6d, 0x1d, 0xc1, 0x9f, 0x0f, 0x47, 0x56, 0x77, 0x6e, 0xb6, 0xac, 0x83, 0x0f, 0x4e, 0x2b, 0x30, 0xbb, + 0xd6, 0x11, 0xfc, 0x39, 0xa3, 0xaf, 0xe0, 0x5e, 0x44, 0xd7, 0xa0, 0xa7, 0x25, 0xe4, 0x20, 0xfd, 0xce, 0x55, 0xb5, + 0x46, 0x89, 0xea, 0xd5, 0xa8, 0x7b, 0x97, 0x18, 0x5c, 0x42, 0x24, 0xd3, 0xc1, 0xd0, 0x43, 0x5a, 0xe8, 0x32, 0x4a, + 0x72, 0x2b, 0x0c, 0xdf, 0x84, 0x87, 0x7a, 0x91, 0x75, 0x55, 0x3a, 0x41, 0xbc, 0x6e, 0x3f, 0xa1, 0xed, 0x2e, 0x85, + 0x95, 0xd3, 0x2a, 0xc7, 0xae, 0x21, 0xdf, 0xb2, 0x6e, 0x80, 0xea, 0x18, 0x10, 0x53, 0x11, 0x0e, 0x4a, 0xab, 0x45, + 0x5b, 0x02, 0x9b, 0x25, 0x2c, 0xa5, 0x22, 0x4d, 0x7c, 0x09, 0xc4, 0x46, 0xd7, 0x45, 0x9a, 0x64, 0x6c, 0x98, 0xd7, + 0x60, 0x3c, 0x9f, 0x73, 0xed, 0xab, 0x7e, 0x15, 0x5f, 0x82, 0x57, 0xbc, 0x15, 0x46, 0x37, 0x68, 0xf5, 0x71, 0xdf, + 0xdc, 0x61, 0x9c, 0x01, 0x26, 0x6c, 0xce, 0xca, 0xc0, 0xf2, 0xd0, 0xd9, 0xd5, 0x0e, 0x37, 0x10, 0xf4, 0x83, 0x3a, + 0x94, 0xd2, 0x83, 0x7f, 0x56, 0x3b, 0x3c, 0x8a, 0x85, 0x9c, 0xa2, 0x73, 0xe2, 0xc7, 0x39, 0x78, 0x12, 0x45, 0x71, + 0xea, 0xf3, 0xa3, 0xea, 0x06, 0xc5, 0x72, 0x62, 0xf1, 0xb5, 0x17, 0x48, 0x76, 0x77, 0xd2, 0x97, 0x7b, 0x39, 0xa1, + 0x7a, 0xf2, 0xa4, 0x00, 0xce, 0xac, 0xc0, 0x7d, 0xec, 0x74, 0x80, 0x4b, 0xe8, 0xb0, 0xf6, 0x56, 0x13, 0x50, 0xba, + 0x98, 0x6d, 0x73, 0x05, 0x2f, 0xd2, 0x41, 0x09, 0x33, 0x2f, 0x61, 0x60, 0xd2, 0x68, 0x87, 0xba, 0x61, 0x5e, 0x02, + 0xf9, 0xf4, 0x2a, 0x37, 0x33, 0x55, 0xef, 0x87, 0xc2, 0x5a, 0x22, 0xdc, 0x92, 0x09, 0x8f, 0x5f, 0x1d, 0x55, 0x98, + 0x9a, 0x2d, 0xe3, 0xb8, 0xc7, 0x9f, 0xfd, 0xdf, 0x3d, 0x08, 0xf4, 0x2d, 0x05, 0xf3, 0x8e, 0x0a, 0x16, 0x29, 0xf9, + 0x1a, 0xe6, 0xf4, 0x9e, 0x08, 0x3d, 0x4b, 0x4e, 0x54, 0x28, 0x52, 0x7b, 0x2a, 0xfa, 0xb1, 0xa9, 0xb9, 0x6d, 0x6b, + 0x4e, 0xc5, 0x8a, 0x02, 0xc3, 0xc7, 0xac, 0xa3, 0xc2, 0xcf, 0x55, 0x7f, 0xf2, 0xa4, 0x91, 0x38, 0x92, 0x2d, 0x51, + 0xc2, 0x52, 0x71, 0x9f, 0xd0, 0x54, 0x19, 0xef, 0xaa, 0x32, 0xea, 0xcb, 0xdb, 0x45, 0x6c, 0x26, 0x4c, 0x72, 0x69, + 0xef, 0xe1, 0xcf, 0x11, 0xf3, 0x52, 0x8b, 0xeb, 0x76, 0x35, 0x89, 0xe9, 0x30, 0x00, 0x6d, 0x64, 0x84, 0x42, 0xf2, + 0x61, 0x0e, 0x1f, 0xaf, 0xff, 0xb2, 0xe2, 0x41, 0x28, 0xa0, 0x8d, 0x4f, 0x9f, 0xec, 0x22, 0x6e, 0xe8, 0xdb, 0xd4, + 0xa3, 0xb8, 0x6d, 0x32, 0x2f, 0x10, 0xa5, 0x1e, 0xd9, 0x9f, 0xf8, 0x18, 0x6a, 0xa7, 0x3e, 0x82, 0x98, 0x14, 0xa9, + 0x62, 0xf0, 0xf6, 0xfc, 0x1b, 0x85, 0x1f, 0x00, 0xb2, 0x6e, 0xc0, 0x8b, 0x17, 0xc5, 0xc7, 0x71, 0x29, 0x3e, 0x8e, + 0xc2, 0xf3, 0x3d, 0x43, 0x66, 0xda, 0x6c, 0x9f, 0xa6, 0x10, 0x05, 0xe6, 0x64, 0xf3, 0xb1, 0x58, 0x05, 0xa9, 0xbf, + 0xf4, 0xe2, 0x74, 0x1f, 0x83, 0xe3, 0x60, 0xb0, 0x9d, 0xa6, 0xf8, 0x15, 0x64, 0x36, 0x22, 0x72, 0xa8, 0xa4, 0xa1, + 0xb0, 0x1b, 0x99, 0xfa, 0x41, 0x6e, 0x36, 0x22, 0x3a, 0xf0, 0xc6, 0x63, 0xb6, 0x4c, 0xdd, 0x52, 0x10, 0x9e, 0x68, + 0x9c, 0xb2, 0xd4, 0x4c, 0xd2, 0x98, 0x79, 0x0b, 0x35, 0x0f, 0xca, 0xb5, 0xd9, 0x5e, 0xb2, 0x1a, 0x41, 0x54, 0x21, + 0x11, 0x1e, 0x8c, 0x06, 0x08, 0x06, 0x1c, 0x00, 0x22, 0x04, 0xc5, 0xa1, 0x29, 0x3c, 0x8b, 0x66, 0x95, 0x2d, 0x55, + 0xb0, 0x54, 0x27, 0x98, 0x4a, 0x8d, 0x6e, 0x5e, 0x20, 0xdd, 0x1e, 0x47, 0xc1, 0x15, 0x8f, 0xb9, 0x91, 0xe7, 0xe4, + 0x51, 0x07, 0xc7, 0xfc, 0x3a, 0xae, 0x60, 0xb8, 0x19, 0xb5, 0x63, 0x43, 0xb2, 0xb8, 0xa6, 0x68, 0x1c, 0xfb, 0xbc, + 0x32, 0xd0, 0x4c, 0x6a, 0x19, 0xf3, 0x7d, 0x12, 0x2c, 0xe7, 0x40, 0xb2, 0x4a, 0x06, 0x3e, 0x73, 0x67, 0x90, 0xbb, + 0x7f, 0x22, 0x54, 0x48, 0xd5, 0x3e, 0x7d, 0x7a, 0x3f, 0xfc, 0xd7, 0x3f, 0x21, 0x29, 0xe9, 0xdc, 0x11, 0x31, 0x30, + 0x2e, 0xe4, 0x5a, 0x9c, 0x2d, 0x36, 0x86, 0x68, 0xdc, 0xc5, 0x26, 0x22, 0x3a, 0xa1, 0xd8, 0x5b, 0xd9, 0xf0, 0x52, + 0xc4, 0xd5, 0x83, 0x74, 0xc6, 0xba, 0x88, 0xd4, 0x31, 0x84, 0xe5, 0x1d, 0x8a, 0x18, 0x2e, 0xca, 0xdf, 0x6e, 0x5f, + 0x1e, 0x29, 0x45, 0xb8, 0xc7, 0x3a, 0x0b, 0x24, 0xda, 0x43, 0x83, 0x63, 0x4f, 0x41, 0x6e, 0x0a, 0xf9, 0xa2, 0xa4, + 0xb7, 0x0f, 0xc3, 0x9c, 0x47, 0x0b, 0x66, 0xf9, 0xd1, 0xfe, 0x0d, 0x1b, 0x99, 0xde, 0xd2, 0x27, 0x3b, 0x22, 0x94, + 0x13, 0x2a, 0xc4, 0x92, 0xe6, 0xe6, 0x39, 0xc4, 0xf8, 0x67, 0xc5, 0x54, 0x46, 0x95, 0xc0, 0x6d, 0xad, 0x42, 0x6f, + 0x79, 0xc0, 0x83, 0xa2, 0x89, 0x9a, 0x83, 0xe3, 0x7d, 0x6f, 0x50, 0xce, 0xcf, 0x63, 0x89, 0x3c, 0xb3, 0x65, 0x2a, + 0x70, 0x42, 0x69, 0x76, 0x44, 0x46, 0x9d, 0xe2, 0xc1, 0x8c, 0xa6, 0x53, 0x39, 0xa7, 0x8e, 0x55, 0x06, 0x2f, 0x9f, + 0xb4, 0x62, 0x4b, 0x47, 0x4b, 0xea, 0x69, 0xb3, 0x8b, 0xfc, 0xa7, 0xda, 0xc3, 0x64, 0x5a, 0x30, 0x66, 0x38, 0xef, + 0x1b, 0xb9, 0x79, 0xf2, 0x19, 0x7b, 0x44, 0x95, 0x38, 0x22, 0xa9, 0x66, 0x82, 0x6c, 0x60, 0xa9, 0xf6, 0x5c, 0x97, + 0xf0, 0x5c, 0x15, 0xdd, 0xc1, 0x24, 0xd6, 0xe4, 0xdc, 0x85, 0xc1, 0xa6, 0xf0, 0xa1, 0x49, 0xee, 0xbd, 0xf8, 0x51, + 0x75, 0x38, 0x9b, 0x30, 0xee, 0x7b, 0x62, 0xfb, 0x95, 0x36, 0x28, 0x6c, 0x3c, 0xbe, 0xee, 0x80, 0xe0, 0x45, 0x3b, + 0x15, 0x3c, 0xaf, 0x7c, 0x4d, 0x28, 0xdd, 0x0c, 0xbc, 0xbb, 0x48, 0x32, 0xbb, 0xe2, 0x11, 0x58, 0xce, 0xb0, 0xf4, + 0x5c, 0x78, 0x3e, 0x6f, 0x1c, 0x34, 0xa4, 0x61, 0x90, 0x9b, 0x74, 0xf3, 0xb0, 0x15, 0x04, 0x38, 0x60, 0xf7, 0x9d, + 0x35, 0xb9, 0x6e, 0x79, 0x30, 0x88, 0x3c, 0xb3, 0xe2, 0x1c, 0x96, 0x5e, 0x22, 0x5a, 0xc8, 0x8e, 0xf7, 0x61, 0x7c, + 0x94, 0x6d, 0x51, 0x30, 0x79, 0xc2, 0xbe, 0x10, 0x6f, 0xbd, 0x7e, 0xd3, 0xad, 0xb7, 0xca, 0xa3, 0x94, 0x59, 0x2f, + 0x5f, 0x87, 0xd8, 0x36, 0x5e, 0x42, 0xb6, 0x67, 0xdf, 0x4f, 0x38, 0x61, 0x90, 0x7a, 0xc9, 0x83, 0x61, 0x96, 0xea, + 0xe9, 0x5b, 0x83, 0xc4, 0x50, 0x9e, 0xdf, 0x0f, 0x2b, 0x4c, 0xf2, 0x9b, 0xf5, 0x53, 0x26, 0x62, 0x36, 0x9c, 0xa5, + 0x0d, 0x61, 0x1d, 0x9a, 0xaa, 0x10, 0x1f, 0xbe, 0xa5, 0x42, 0xb1, 0xcd, 0xb7, 0xd5, 0x2a, 0x38, 0xab, 0xa2, 0x9a, + 0xa7, 0xa9, 0x8f, 0xf0, 0x40, 0x6c, 0xd4, 0xc6, 0x52, 0x0c, 0x36, 0x91, 0xba, 0x50, 0x55, 0xa8, 0x16, 0xbc, 0xe5, + 0x92, 0x2a, 0xeb, 0xfd, 0xe3, 0x7d, 0xba, 0x4e, 0x0f, 0x68, 0x03, 0x0e, 0x8e, 0xc1, 0x32, 0x9d, 0xf6, 0x84, 0xb7, + 0x5c, 0xf2, 0x15, 0xa7, 0x5f, 0xf4, 0x66, 0x7f, 0x9e, 0x2e, 0x82, 0xc1, 0xff, 0x02, 0xf1, 0xc9, 0x8b, 0x98, 0x7c, + 0x7b, 0x03, 0x00}; #else // Brotli (default, smaller) constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x5b, 0x05, 0x7b, 0x53, 0xc1, 0xb6, 0x69, 0x3d, 0x41, 0xeb, 0x04, 0x30, 0xf6, 0xd6, 0x77, 0x35, 0xdb, 0xa3, 0x08, - 0x36, 0x0e, 0x04, 0x80, 0x90, 0x4f, 0xf1, 0xb2, 0x21, 0xa4, 0x82, 0xee, 0x00, 0xaa, 0x20, 0x7f, 0x3b, 0xff, 0x00, - 0xaa, 0x9a, 0x73, 0x74, 0x8c, 0xe1, 0xa6, 0x1f, 0xa0, 0xa2, 0x59, 0xf5, 0xaa, 0x92, 0x79, 0x50, 0x43, 0x1f, 0xe8, - 0x3c, 0x52, 0x68, 0x2c, 0xf6, 0x36, 0x31, 0x90, 0x4e, 0x2b, 0x91, 0x69, 0x38, 0xb4, 0x61, 0xa7, 0xbb, 0x57, 0x79, - 0x3b, 0x6d, 0x62, 0x85, 0x9f, 0x24, 0x24, 0xda, 0x45, 0xc1, 0xe2, 0x85, 0x44, 0x50, 0x6c, 0x8c, 0x1e, 0xab, 0xbb, - 0x7b, 0x1e, 0x30, 0x98, 0x59, 0xe5, 0xb9, 0xd1, 0x34, 0x03, 0x0b, 0x79, 0x37, 0x8c, 0x4b, 0x68, 0x2c, 0xbe, 0x21, - 0xcc, 0x30, 0xc4, 0x9c, 0x4d, 0x34, 0xd6, 0x01, 0xc1, 0xe1, 0x66, 0x48, 0x3c, 0x94, 0xf4, 0x3c, 0x5b, 0x12, 0xfd, - 0x15, 0xb4, 0x05, 0xdb, 0xaf, 0xc1, 0x59, 0x5d, 0x9f, 0xc5, 0x8d, 0xaf, 0xf7, 0xc6, 0x5f, 0x58, 0xfb, 0xc3, 0x74, - 0x45, 0x97, 0x23, 0xac, 0x2e, 0x4a, 0xb9, 0xab, 0xd7, 0x5b, 0xe5, 0x85, 0x6f, 0x21, 0xba, 0x7e, 0xa3, 0x57, 0xa5, - 0xb3, 0x24, 0x7e, 0x60, 0x30, 0x0e, 0x15, 0x1f, 0x04, 0x5e, 0xba, 0x06, 0x04, 0xff, 0x02, 0x26, 0x2d, 0x03, 0x6c, - 0x97, 0x1b, 0x23, 0x84, 0x28, 0x25, 0xb4, 0xe2, 0xca, 0xbd, 0xc8, 0xc3, 0x96, 0x4a, 0xef, 0xed, 0xe7, 0x65, 0x4f, - 0xb2, 0x2d, 0x6c, 0x02, 0x91, 0x04, 0x76, 0xae, 0xba, 0x67, 0x9c, 0xe3, 0x38, 0xd6, 0x96, 0x61, 0x0c, 0xd3, 0x80, - 0xe4, 0x4a, 0x43, 0x2e, 0x92, 0xff, 0xa7, 0xdb, 0xd2, 0xec, 0xf5, 0xf5, 0x90, 0x4a, 0x22, 0x74, 0x92, 0x40, 0x80, - 0xed, 0x2b, 0xb5, 0xbc, 0xb6, 0xe0, 0x71, 0xda, 0x35, 0x3b, 0xe9, 0xec, 0xeb, 0xac, 0xbe, 0x6a, 0x2f, 0xf0, 0xde, - 0x96, 0xf7, 0x43, 0x51, 0x4d, 0xce, 0xbb, 0xd3, 0x70, 0xc2, 0x08, 0x2c, 0xf0, 0xc8, 0xac, 0x25, 0x82, 0x67, 0xeb, - 0xcd, 0xf5, 0x7d, 0x7d, 0xb7, 0x8b, 0xca, 0x2a, 0x15, 0x66, 0x68, 0xf3, 0x6e, 0xb6, 0xda, 0x5b, 0x81, 0x7b, 0x2e, - 0xe6, 0xc1, 0xdc, 0x5e, 0x0a, 0x7a, 0xda, 0x4a, 0x2c, 0x68, 0xa4, 0xd0, 0x92, 0xe7, 0xb2, 0xb3, 0xef, 0xab, 0xda, - 0xd7, 0xef, 0x89, 0xe7, 0x22, 0xd0, 0x93, 0xe5, 0x08, 0xcc, 0xea, 0x82, 0xdb, 0xde, 0x1a, 0xdd, 0x49, 0x47, 0x26, - 0x23, 0xc1, 0x16, 0x7b, 0x2a, 0x99, 0x23, 0xe9, 0x4c, 0xf8, 0x65, 0xaa, 0xfe, 0xe9, 0x6a, 0x41, 0x0d, 0x8f, 0xfb, - 0x48, 0x9a, 0x49, 0x4e, 0x0b, 0x2e, 0x91, 0xfc, 0x76, 0x6e, 0x55, 0xee, 0xb4, 0xbb, 0x3c, 0x63, 0xc1, 0x95, 0x5a, - 0xf8, 0x37, 0x35, 0xb1, 0xaa, 0xcd, 0xa9, 0x99, 0x2f, 0x3d, 0x52, 0xd0, 0x01, 0xd9, 0x06, 0x27, 0x41, 0x92, 0xdd, - 0x6d, 0x4a, 0xee, 0xad, 0xa6, 0x33, 0xe1, 0xff, 0xeb, 0x6b, 0x5a, 0xff, 0xfd, 0xf3, 0x05, 0x0a, 0xbb, 0xc4, 0x55, - 0x1d, 0x28, 0x69, 0x66, 0x7f, 0x4f, 0x49, 0xce, 0xb2, 0xec, 0x8a, 0x0a, 0x15, 0x2d, 0x63, 0x1b, 0x6f, 0x28, 0x00, - 0xf5, 0x44, 0x47, 0xd6, 0xf5, 0xb5, 0xd7, 0x6f, 0xff, 0xf5, 0x0b, 0x3d, 0xd9, 0x1d, 0x42, 0xe9, 0xb3, 0x32, 0xa5, - 0xf6, 0xa3, 0x1b, 0xc3, 0x58, 0x8a, 0x7c, 0x24, 0xc7, 0x03, 0xbc, 0x92, 0xd5, 0x4f, 0xbf, 0xfc, 0xfa, 0x3d, 0x3b, - 0x6d, 0x8c, 0xc5, 0xdb, 0x9d, 0x77, 0x97, 0xd2, 0x5e, 0x82, 0x85, 0x53, 0x9a, 0xaf, 0xb6, 0x09, 0x45, 0x51, 0x25, - 0x90, 0x44, 0x85, 0xa4, 0xc6, 0x23, 0x18, 0xfb, 0xbb, 0x56, 0xef, 0xe9, 0x3c, 0x13, 0x62, 0xf0, 0xaf, 0x6b, 0xe9, - 0xa2, 0xa7, 0x26, 0x32, 0xa1, 0x8d, 0x5d, 0x56, 0x22, 0xa0, 0xe8, 0xb8, 0x06, 0xc1, 0x07, 0xb4, 0x7e, 0xf5, 0xbe, - 0xae, 0xff, 0xfa, 0x95, 0xa7, 0xb8, 0xe2, 0x34, 0x6a, 0xc9, 0x3c, 0xee, 0xb3, 0x45, 0x67, 0xae, 0x93, 0x6c, 0x20, - 0xbc, 0x42, 0xc5, 0xe1, 0x91, 0x52, 0xb9, 0x8c, 0x62, 0x0d, 0x46, 0xbb, 0x46, 0x72, 0x69, 0x26, 0xe0, 0xac, 0x67, - 0xec, 0xb5, 0x51, 0x5f, 0x9d, 0xae, 0x5d, 0x25, 0x15, 0x72, 0xf0, 0xc2, 0x2f, 0x67, 0xe6, 0x68, 0x08, 0xac, 0xdd, - 0xcb, 0xd5, 0xe3, 0x03, 0x9d, 0x49, 0x4d, 0xa1, 0xcd, 0x81, 0xbf, 0x09, 0xd9, 0xdd, 0x2d, 0xe1, 0x7b, 0x7f, 0x9a, - 0x7d, 0xfd, 0x92, 0xd9, 0x26, 0x19, 0x8d, 0x9d, 0x2b, 0x6d, 0x38, 0x99, 0x2b, 0x2d, 0xb5, 0xfa, 0xed, 0xe3, 0xb0, - 0x11, 0x2c, 0x17, 0x79, 0xe0, 0x24, 0xb1, 0x86, 0x68, 0x15, 0xb3, 0x7f, 0x4b, 0xf3, 0xeb, 0xd7, 0xc3, 0xdd, 0x24, - 0x64, 0x69, 0x0f, 0x4e, 0xae, 0xdb, 0xc7, 0x71, 0x4e, 0xaa, 0x64, 0x18, 0xec, 0xd1, 0x7b, 0x59, 0xa8, 0x31, 0x05, - 0x80, 0x2b, 0xcb, 0x0c, 0x91, 0x02, 0x85, 0x0d, 0x16, 0x1d, 0x1b, 0xa6, 0x6e, 0x48, 0x13, 0x04, 0xa1, 0xc5, 0xff, - 0x33, 0xa7, 0xdf, 0xb4, 0x67, 0x98, 0xb0, 0x60, 0xdb, 0x52, 0xfa, 0xaf, 0x26, 0xbf, 0x98, 0xed, 0xc1, 0x7b, 0x18, - 0x2e, 0x02, 0x9e, 0x60, 0x3c, 0xef, 0xff, 0xef, 0xad, 0xb4, 0xdc, 0xfe, 0x88, 0x74, 0x45, 0x88, 0xec, 0x01, 0xc8, - 0x71, 0x86, 0x23, 0xeb, 0xf7, 0x9d, 0x99, 0x55, 0xe0, 0x34, 0x40, 0xb2, 0xc7, 0x99, 0x95, 0xb4, 0xd6, 0x62, 0x53, - 0x71, 0xef, 0x7d, 0xef, 0x32, 0xbf, 0x8b, 0xce, 0xf8, 0x61, 0x58, 0x61, 0x32, 0x1b, 0x69, 0x87, 0x65, 0xbb, 0xb2, - 0x9c, 0x06, 0x00, 0xc1, 0xfb, 0xde, 0xfb, 0x51, 0xf8, 0xff, 0x47, 0x16, 0xe7, 0x47, 0x64, 0x81, 0x8a, 0xcc, 0x2a, - 0xce, 0xc9, 0x2c, 0x60, 0x46, 0x55, 0x00, 0x67, 0x54, 0x00, 0xfb, 0xe8, 0x80, 0x1c, 0x0b, 0x82, 0x46, 0xd3, 0x64, - 0xb3, 0x8f, 0x86, 0x6c, 0x79, 0xbf, 0xd2, 0x62, 0x05, 0x51, 0xae, 0x67, 0x64, 0x5b, 0xf2, 0xbb, 0x1d, 0x28, 0x6b, - 0x96, 0xd2, 0x4a, 0x47, 0xff, 0xeb, 0x96, 0xeb, 0x89, 0x6e, 0x15, 0x9f, 0x68, 0xa7, 0xdc, 0x30, 0x8c, 0xfc, 0x9f, - 0xc0, 0x23, 0xe1, 0x0c, 0x4e, 0xc3, 0x69, 0xa8, 0xc2, 0xd5, 0xa0, 0xa6, 0x5b, 0xc7, 0x8e, 0xb3, 0xe9, 0x34, 0x54, - 0xfd, 0x31, 0xfc, 0x1e, 0x4a, 0x0b, 0x83, 0xa9, 0x33, 0x4d, 0xd3, 0x7f, 0x77, 0x8d, 0x72, 0x55, 0x71, 0x4d, 0x74, - 0xe3, 0xaa, 0x55, 0x19, 0xf8, 0x9b, 0xa6, 0x69, 0xf8, 0x87, 0xe4, 0x6e, 0x5f, 0xac, 0xf9, 0x3d, 0xda, 0x61, 0xa0, - 0x50, 0xca, 0x2e, 0x93, 0x38, 0x3e, 0xe4, 0x5b, 0x96, 0x25, 0xd9, 0xf0, 0x75, 0x2c, 0xad, 0x37, 0x37, 0x2a, 0x15, - 0x48, 0xe8, 0xbd, 0xf6, 0x19, 0xb5, 0x55, 0xf0, 0x10, 0x5d, 0x14, 0x49, 0xa4, 0xa7, 0xff, 0xa7, 0xaa, 0x7a, 0xbc, - 0x43, 0xa6, 0x66, 0xdd, 0xd8, 0xc4, 0x05, 0xf2, 0xe9, 0x1b, 0x34, 0x4f, 0x12, 0x1a, 0x1b, 0xac, 0xf3, 0x32, 0x32, - 0xad, 0x2b, 0xeb, 0xd8, 0x29, 0x4e, 0xfe, 0x6f, 0x80, 0xa1, 0x0d, 0xad, 0x4a, 0xc2, 0xb6, 0x83, 0xa0, 0xff, 0x50, - 0x68, 0x62, 0xad, 0x71, 0x00, 0x77, 0x5a, 0x66, 0xb4, 0x05, 0x9d, 0x01, 0x51, 0x9c, 0xbb, 0xf8, 0xd3, 0x64, 0x82, - 0x69, 0xb6, 0x87, 0x82, 0xef, 0x73, 0x14, 0x62, 0x0c, 0x22, 0xcb, 0x73, 0xdb, 0xb3, 0x0f, 0x4c, 0xfe, 0x77, 0x60, - 0x45, 0xff, 0x14, 0xea, 0x9f, 0xb0, 0x02, 0xa0, 0x6e, 0xbd, 0xe4, 0xd7, 0x0a, 0xbc, 0xe7, 0xf7, 0x00, 0x44, 0x26, - 0xc2, 0x96, 0xe5, 0x37, 0xda, 0x32, 0xca, 0xef, 0x42, 0xb4, 0xdd, 0xe2, 0x70, 0xf0, 0xe0, 0xc1, 0x93, 0xd4, 0xed, - 0x18, 0xbf, 0x54, 0xdd, 0xb0, 0x5b, 0x86, 0x3e, 0x46, 0x7f, 0xbe, 0x1b, 0x61, 0x83, 0x52, 0xe1, 0x6a, 0x62, 0x7a, - 0xa6, 0x7e, 0x6f, 0xc4, 0x5b, 0x63, 0x55, 0x1a, 0xb8, 0xa5, 0x87, 0xd9, 0x84, 0xd4, 0xcf, 0xd7, 0xd4, 0x62, 0x26, - 0x28, 0x13, 0x41, 0x44, 0xca, 0x76, 0xf6, 0x21, 0x63, 0x4b, 0xae, 0xaf, 0xe7, 0xc7, 0x4d, 0x79, 0x5d, 0x8f, 0xc3, - 0x04, 0x0d, 0xc9, 0x06, 0x39, 0xa0, 0xd8, 0xde, 0x87, 0xb3, 0xd7, 0x4b, 0x65, 0x4e, 0xa3, 0xa6, 0xff, 0x79, 0xab, - 0x0c, 0xed, 0xb4, 0x01, 0x69, 0xdc, 0xa6, 0x15, 0x98, 0x98, 0x82, 0xc8, 0x86, 0x4d, 0x06, 0x77, 0xbd, 0x39, 0x6c, - 0x73, 0x52, 0x73, 0x48, 0xd6, 0xe4, 0x8a, 0x4a, 0x83, 0x9a, 0xf5, 0xc9, 0xeb, 0xa5, 0x2e, 0xa9, 0x70, 0x4b, 0x9d, - 0xb9, 0xbe, 0xc8, 0xe4, 0x9e, 0x17, 0xf2, 0xca, 0x95, 0x97, 0xd3, 0x14, 0xd8, 0x9b, 0xef, 0xb1, 0xaf, 0x13, 0x9a, - 0xf5, 0x3d, 0x8f, 0x74, 0xe3, 0x5d, 0x47, 0x07, 0x26, 0xd2, 0x60, 0xa2, 0xef, 0x11, 0x34, 0x2f, 0x6e, 0xb3, 0xfe, - 0xf0, 0xa3, 0x42, 0x7d, 0xfb, 0x47, 0x5c, 0x75, 0x98, 0x0f, 0xe6, 0x0f, 0x4e, 0xe5, 0x67, 0x1a, 0x4f, 0x93, 0x47, - 0x5f, 0x04, 0x7c, 0xff, 0x7a, 0xf9, 0x79, 0x6b, 0x46, 0x47, 0xc6, 0x0c, 0xd5, 0x90, 0x30, 0xd5, 0xfc, 0xde, 0x8b, - 0xd5, 0x65, 0x7f, 0x87, 0x2d, 0x9a, 0xd5, 0xe4, 0x3f, 0xf9, 0xed, 0x07, 0xfb, 0xa2, 0xb6, 0xd3, 0xe1, 0xbf, 0xbd, - 0x40, 0x78, 0x7a, 0x67, 0x24, 0x0b, 0x6b, 0x0e, 0xed, 0xcf, 0x7a, 0x6a, 0x7c, 0xdb, 0x36, 0x61, 0x5b, 0xc3, 0x7c, - 0x5d, 0xfc, 0xf6, 0x1c, 0xc2, 0xa9, 0xba, 0x12, 0x15, 0x35, 0x11, 0x87, 0x41, 0x13, 0xa5, 0xe5, 0x73, 0x07, 0xfa, - 0xc6, 0xe3, 0x96, 0x43, 0x01, 0x76, 0x4b, 0x53, 0x28, 0xad, 0x09, 0x7e, 0x88, 0x0f, 0x26, 0x90, 0x04, 0xfd, 0x2a, - 0x0d, 0x4c, 0xcd, 0x5c, 0xbf, 0x10, 0xd6, 0x47, 0xaf, 0xe2, 0x12, 0x80, 0x00, 0x59, 0xaa, 0x9b, 0x18, 0x58, 0x96, - 0xc8, 0x80, 0x67, 0xc2, 0xb9, 0x4e, 0x5d, 0x86, 0x1e, 0x79, 0xf5, 0xcf, 0xb0, 0x81, 0x1f, 0x9e, 0x4f, 0x34, 0x1d, - 0x7c, 0xf2, 0x4a, 0x4d, 0xbd, 0x42, 0x06, 0xbc, 0x73, 0x56, 0xbc, 0x9d, 0x43, 0xa9, 0xe6, 0x44, 0x0c, 0xcd, 0xcd, - 0xe4, 0x4e, 0xde, 0xb3, 0x0e, 0x25, 0x35, 0xb6, 0xb6, 0xf6, 0xcc, 0xae, 0x6f, 0x91, 0x82, 0x59, 0xa1, 0xdc, 0x8b, - 0xaa, 0x4f, 0x64, 0x26, 0xd0, 0xa5, 0xe7, 0x38, 0xf3, 0xf5, 0xcd, 0x4f, 0x05, 0x62, 0x8c, 0x38, 0xc3, 0x96, 0x13, - 0x68, 0xb2, 0xe4, 0xd9, 0xcf, 0x4a, 0x5f, 0x44, 0x57, 0xf6, 0x49, 0x47, 0xae, 0x16, 0x81, 0xa1, 0xa7, 0x2d, 0xd8, - 0xb3, 0x35, 0x74, 0x6a, 0xc2, 0xbc, 0xc0, 0x7d, 0xae, 0xf0, 0x88, 0xe4, 0xd0, 0x28, 0x7c, 0x22, 0x98, 0x95, 0xa3, - 0x2a, 0x81, 0x16, 0x0b, 0xc7, 0x4a, 0xf3, 0x07, 0xb8, 0xa1, 0x56, 0xbf, 0xdf, 0x36, 0x6b, 0xa3, 0x84, 0x8b, 0xbf, - 0x24, 0x99, 0xc1, 0x09, 0x7e, 0xff, 0x99, 0x8c, 0x1c, 0xd1, 0x43, 0x7c, 0xb1, 0x46, 0x9d, 0x2e, 0x65, 0x92, 0xa9, - 0xa0, 0xd0, 0x45, 0x92, 0x47, 0x37, 0x9c, 0x3c, 0x5f, 0xf1, 0xf3, 0x0d, 0x1e, 0x37, 0xeb, 0x3d, 0xb6, 0x7c, 0x33, - 0x35, 0xaf, 0xf3, 0x08, 0x54, 0x33, 0x56, 0x02, 0x4f, 0x18, 0xda, 0xc0, 0xbb, 0xc5, 0x4a, 0x42, 0xd7, 0xef, 0xbd, - 0xa4, 0xec, 0x61, 0x77, 0x1b, 0xa2, 0x57, 0x47, 0x00, 0xee, 0x8b, 0xd3, 0x56, 0xd4, 0xbd, 0x01, 0xa2, 0x8f, 0xee, - 0xef, 0xb1, 0xac, 0xe1, 0x03, 0x87, 0x8d, 0x2b, 0x3c, 0x8e, 0x15, 0x84, 0x96, 0xb4, 0xfe, 0x56, 0xd5, 0x1e, 0xc0, - 0x83, 0x66, 0x79, 0x15, 0x8a, 0x60, 0xb7, 0x15, 0x21, 0x3b, 0xce, 0x44, 0x71, 0x9f, 0x6f, 0xe0, 0x70, 0xde, 0xb8, - 0x7a, 0x74, 0x43, 0xcd, 0x4d, 0x7a, 0x90, 0xd2, 0x4b, 0x9b, 0xc8, 0x6a, 0xa2, 0xc6, 0xdf, 0x1a, 0x55, 0x85, 0x34, - 0xdd, 0x1d, 0x1a, 0x7c, 0x88, 0x7c, 0x81, 0x3d, 0xd8, 0x12, 0xf4, 0x32, 0x8d, 0xc6, 0xc1, 0x56, 0x0d, 0xe5, 0x8d, - 0x75, 0x00, 0x03, 0x61, 0x93, 0xa0, 0x44, 0x06, 0x5b, 0x67, 0x8b, 0x58, 0xf5, 0x9c, 0xb0, 0x79, 0x7f, 0xbd, 0x2e, - 0xb1, 0x17, 0xb3, 0x85, 0xcd, 0x54, 0x5f, 0xc8, 0x38, 0xeb, 0xa7, 0xcd, 0xfe, 0xbf, 0x2d, 0x80, 0x83, 0x12, 0xc3, - 0x0b, 0x02, 0x41, 0x44, 0xd5, 0x07, 0xe5, 0xcd, 0xb0, 0x24, 0x2c, 0x0a, 0x6c, 0x1b, 0x1f, 0xb9, 0x7b, 0x48, 0x9e, - 0x55, 0x42, 0x7c, 0x2b, 0x63, 0xd3, 0xd1, 0x76, 0x18, 0x61, 0xa8, 0x86, 0x2d, 0x11, 0x5a, 0x41, 0x04, 0x6c, 0xea, - 0xcf, 0x34, 0xf6, 0x71, 0xe7, 0xda, 0x41, 0xba, 0x28, 0xcb, 0x2c, 0x1c, 0x47, 0x50, 0xa7, 0x83, 0x41, 0xad, 0x84, - 0x9e, 0xec, 0x1e, 0xfc, 0xc6, 0xc6, 0xb8, 0xa0, 0xb8, 0xa1, 0x70, 0xeb, 0x5a, 0x9f, 0x46, 0x06, 0xa6, 0xf8, 0x72, - 0xa5, 0xff, 0xfe, 0x80, 0x1e, 0x07, 0xbb, 0xd4, 0x48, 0xf9, 0x6c, 0xd6, 0x13, 0xf8, 0xee, 0x86, 0x06, 0x67, 0x78, - 0x38, 0xf2, 0x83, 0xc3, 0x3b, 0x25, 0xf0, 0xa0, 0x60, 0x56, 0xbe, 0x7d, 0x10, 0x8a, 0xd4, 0x17, 0x81, 0x0e, 0x17, - 0x5f, 0x53, 0xaf, 0x87, 0x23, 0xe4, 0x56, 0xe4, 0xb1, 0xc0, 0x9d, 0xc8, 0x38, 0x25, 0x47, 0x18, 0x18, 0x27, 0x57, - 0xdf, 0x84, 0xcd, 0x7c, 0xdb, 0x31, 0xff, 0xc2, 0xe5, 0x83, 0x03, 0xd1, 0xac, 0x9f, 0x2d, 0xd8, 0xa5, 0xa4, 0x40, - 0xe0, 0x1e, 0xe9, 0x18, 0x3d, 0x85, 0xa9, 0x1b, 0x9c, 0xa6, 0x14, 0x51, 0x9a, 0x88, 0xd9, 0x42, 0x70, 0x6c, 0x6b, - 0x63, 0x2e, 0xfd, 0x46, 0xd9, 0xd5, 0xb4, 0xd9, 0x8f, 0x2c, 0xf8, 0x42, 0xf9, 0xb6, 0x27, 0xb8, 0x69, 0xc5, 0xed, - 0x5c, 0xea, 0xff, 0x76, 0x9d, 0x49, 0x1b, 0x5a, 0xf7, 0xaa, 0x2d, 0x04, 0x36, 0x45, 0x05, 0xa8, 0x99, 0x5e, 0x24, - 0x53, 0x3b, 0x89, 0xd9, 0x0f, 0x4d, 0x24, 0x27, 0x44, 0x2b, 0xfb, 0x3b, 0xd9, 0x8b, 0x36, 0xe9, 0x18, 0x4a, 0x30, - 0xfc, 0xc8, 0xa5, 0xf4, 0xd5, 0xd5, 0x72, 0x2d, 0x3b, 0x5f, 0xc3, 0x4e, 0x98, 0x0c, 0x08, 0xb2, 0xff, 0x59, 0x68, - 0x6b, 0xc0, 0xe4, 0x52, 0x8f, 0x29, 0x78, 0x74, 0x6d, 0xbe, 0xfb, 0x13, 0x8a, 0x25, 0x0b, 0x31, 0xe5, 0xd0, 0xae, - 0xbf, 0x1e, 0x13, 0x23, 0xa0, 0x2c, 0x89, 0x10, 0x6e, 0xa5, 0x1c, 0xa8, 0x7f, 0xbf, 0x62, 0x52, 0xb0, 0xa5, 0xf6, - 0x46, 0x9c, 0x5d, 0xbd, 0xac, 0x81, 0xc4, 0x46, 0xf3, 0x61, 0x62, 0x47, 0x08, 0x27, 0x4d, 0xed, 0x4b, 0x45, 0x91, - 0x48, 0xcf, 0x52, 0xc4, 0x20, 0xe3, 0x8a, 0xe9, 0x12, 0x2d, 0xac, 0x99, 0xb0, 0x1c, 0x1b, 0x91, 0xa4, 0xcc, 0x6d, - 0x11, 0x3f, 0xbe, 0xe9, 0x06, 0x24, 0x40, 0x3d, 0x62, 0x90, 0x0f, 0xbe, 0x25, 0x20, 0xd7, 0x25, 0x09, 0xca, 0xb5, - 0xcf, 0x25, 0x64, 0x42, 0x3b, 0x19, 0x09, 0x13, 0xf3, 0x46, 0x90, 0x72, 0xf7, 0x74, 0x4a, 0xd7, 0x00, 0x4b, 0x39, - 0x59, 0xcd, 0x21, 0x62, 0xe4, 0x78, 0x5d, 0x75, 0xb5, 0x80, 0x58, 0x0a, 0xb7, 0xa3, 0xed, 0xc8, 0xe4, 0x5c, 0xdc, - 0xa1, 0xf3, 0xce, 0x99, 0x5f, 0x18, 0xa7, 0x1c, 0x9c, 0x1e, 0xe6, 0x2e, 0x20, 0x20, 0xa7, 0xae, 0xfa, 0xc1, 0x19, - 0x19, 0xa4, 0xb8, 0x9a, 0x77, 0x5a, 0x24, 0x9a, 0x11, 0xf9, 0xac, 0x18, 0xaa, 0xdb, 0x2a, 0x37, 0xc2, 0x62, 0xad, - 0x5c, 0x82, 0x29, 0x72, 0x72, 0x1b, 0x7c, 0xb3, 0x83, 0xc7, 0xcd, 0x13, 0x16, 0xc0, 0x59, 0x8f, 0xe5, 0x62, 0xc2, - 0xa1, 0xea, 0x36, 0x7e, 0x0d, 0x64, 0x0a, 0xbc, 0x72, 0xd4, 0x59, 0x92, 0xe3, 0x0b, 0x0d, 0xaa, 0x81, 0xbf, 0xf6, - 0x91, 0xe7, 0x41, 0x6e, 0x50, 0x35, 0xd5, 0x34, 0x8b, 0x42, 0x4f, 0x31, 0xcf, 0x84, 0xcc, 0x5f, 0x35, 0x8a, 0x4e, - 0xc2, 0x8c, 0xa7, 0xc9, 0x96, 0x3a, 0xdd, 0xa7, 0x32, 0xa1, 0x80, 0xd8, 0x43, 0xe0, 0x14, 0xb8, 0xf7, 0xa6, 0xc2, - 0x3c, 0x9d, 0x92, 0x49, 0x1c, 0x9e, 0xcc, 0xb3, 0x59, 0x03, 0x66, 0xc0, 0x8d, 0x12, 0xe8, 0xe6, 0x8c, 0xfc, 0x70, - 0x0b, 0xb7, 0x55, 0x71, 0x1e, 0x93, 0x15, 0x68, 0xc9, 0x4d, 0x04, 0xc9, 0xf0, 0xca, 0xb8, 0x80, 0xd2, 0x7b, 0x13, - 0x67, 0xc6, 0xdd, 0xe2, 0xab, 0x8a, 0x4f, 0xc0, 0x79, 0xdc, 0x97, 0xdb, 0x8a, 0x53, 0x9a, 0x2a, 0x1c, 0x80, 0x92, - 0x17, 0xc4, 0x63, 0xe1, 0x9b, 0xd3, 0x2b, 0x99, 0x61, 0xe0, 0x62, 0x46, 0x35, 0x15, 0xdd, 0x85, 0x74, 0xc2, 0x74, - 0x90, 0xf0, 0x96, 0x34, 0x06, 0x77, 0x40, 0xf1, 0xbe, 0x00, 0x14, 0x11, 0x8e, 0xc2, 0x77, 0x76, 0x4c, 0x47, 0xab, - 0x92, 0xf0, 0x68, 0x99, 0x2d, 0xda, 0x79, 0xf9, 0x46, 0x25, 0xab, 0x1c, 0x70, 0x34, 0x00, 0x6c, 0x5e, 0x7f, 0x48, - 0x7c, 0x06, 0x81, 0x1c, 0x1f, 0x27, 0x76, 0x3e, 0x34, 0x4d, 0x95, 0xc2, 0x9f, 0x8d, 0xf6, 0x26, 0x2c, 0x70, 0xc7, - 0x29, 0x13, 0x3a, 0x1e, 0x1b, 0xd1, 0x0d, 0x41, 0xe7, 0x5f, 0xb0, 0x03, 0xb6, 0xda, 0x66, 0x7b, 0xbf, 0x7a, 0xbd, - 0x2c, 0x4e, 0x0e, 0x98, 0xe4, 0x9d, 0xcd, 0xbd, 0xb7, 0xdb, 0xf9, 0x2f, 0x27, 0x1f, 0x99, 0xb0, 0x40, 0xe1, 0x55, - 0x4e, 0x59, 0x64, 0x24, 0x3a, 0xa0, 0xc4, 0x2b, 0x4d, 0xe7, 0x62, 0x74, 0x2d, 0x12, 0xaf, 0x4a, 0xb1, 0x2b, 0x24, - 0xa9, 0x61, 0xe6, 0x0d, 0x38, 0xc8, 0x66, 0x9d, 0xa6, 0x46, 0x41, 0x11, 0xb2, 0xcc, 0xc5, 0xc6, 0x2c, 0xb1, 0x58, - 0xf3, 0x96, 0x33, 0x6d, 0x4e, 0x61, 0x44, 0xe0, 0xe4, 0x80, 0xa8, 0xfe, 0xac, 0xd6, 0xd8, 0xe0, 0xd6, 0xf3, 0x6a, - 0x18, 0x61, 0xe0, 0xdd, 0x50, 0x92, 0xf2, 0xc4, 0x18, 0x2b, 0x21, 0xc9, 0xa9, 0x23, 0x8e, 0xfd, 0xc8, 0xf2, 0x15, - 0xdf, 0xef, 0xb9, 0xc6, 0x14, 0x97, 0x07, 0x13, 0x63, 0x16, 0x43, 0xa6, 0x76, 0x83, 0xca, 0x22, 0xa9, 0x9f, 0x8e, - 0x6a, 0x59, 0x39, 0x93, 0x7b, 0x0c, 0xf7, 0x51, 0xe7, 0x92, 0xbc, 0x7b, 0x74, 0x05, 0x01, 0xd9, 0x5d, 0x82, 0xd5, - 0x27, 0x87, 0x24, 0x8a, 0x9c, 0xb0, 0x9f, 0xeb, 0x5f, 0x56, 0x23, 0x7f, 0x95, 0x63, 0x29, 0x5f, 0x0f, 0x79, 0x4b, - 0x19, 0x62, 0x2a, 0xad, 0xe5, 0x9e, 0x53, 0x90, 0x71, 0xe9, 0xb2, 0x6c, 0xf1, 0x40, 0x2c, 0x21, 0x7c, 0xb0, 0x98, - 0x7d, 0xde, 0x2e, 0x1c, 0xc8, 0xa4, 0x50, 0x7e, 0xdf, 0xe5, 0xf2, 0xfc, 0xf0, 0xc9, 0x46, 0x2d, 0xc6, 0x18, 0xa7, - 0x54, 0x5b, 0xdf, 0x38, 0xa8, 0x15, 0xa3, 0x0d, 0x3c, 0xbf, 0xb0, 0xdd, 0x6e, 0x6f, 0xd9, 0x2b, 0x20, 0x2b, 0x6b, - 0x2b, 0x70, 0x53, 0x27, 0x9d, 0x1f, 0x36, 0xc2, 0x49, 0x13, 0xca, 0x40, 0x7a, 0x39, 0x43, 0x2d, 0x90, 0x44, 0x37, - 0x45, 0x2d, 0x8c, 0x16, 0xeb, 0x5b, 0x8e, 0xbe, 0xf5, 0x6b, 0x18, 0x32, 0x4a, 0xe9, 0x98, 0xa6, 0xc3, 0xbd, 0x2e, - 0xd9, 0x77, 0x11, 0x44, 0x8b, 0x6c, 0xd6, 0x6c, 0xc3, 0x2f, 0x52, 0xae, 0xbd, 0x10, 0xde, 0x92, 0xe6, 0x28, 0xf2, - 0xce, 0x11, 0xa9, 0xa5, 0x70, 0xaa, 0xa9, 0xc7, 0x24, 0x1c, 0xbb, 0x04, 0x5a, 0x29, 0x98, 0xee, 0xe1, 0xc5, 0x4e, - 0xb6, 0xa1, 0xc2, 0x22, 0x2d, 0x04, 0x69, 0x14, 0x73, 0xf8, 0xfd, 0x20, 0x11, 0x59, 0x06, 0xd8, 0x79, 0xd4, 0xe7, - 0xc0, 0x1e, 0x0e, 0xe3, 0xd1, 0x55, 0x11, 0xfb, 0xee, 0x0f, 0x13, 0x14, 0x62, 0x9d, 0xa6, 0x09, 0x0a, 0xf7, 0x7c, - 0x84, 0x8e, 0xcd, 0x31, 0x3f, 0x9d, 0x85, 0xb6, 0x7d, 0xb3, 0x7a, 0x28, 0xbc, 0xfc, 0x2e, 0xc5, 0x3e, 0xa5, 0xb7, - 0xf3, 0x2f, 0xd3, 0x39, 0xc5, 0x3c, 0xb3, 0xeb, 0x14, 0x53, 0xa1, 0x89, 0xcd, 0x85, 0x97, 0x28, 0x12, 0xe7, 0xc3, - 0x4b, 0x69, 0xe0, 0xcd, 0xd0, 0x29, 0x91, 0x60, 0xdc, 0x08, 0x4f, 0x62, 0x14, 0x61, 0x0d, 0x98, 0xee, 0xe6, 0x3d, - 0x3b, 0xa9, 0x38, 0x77, 0xca, 0x92, 0x2b, 0xba, 0xfb, 0xb9, 0x41, 0x00, 0x40, 0xc5, 0xce, 0xe3, 0x0b, 0x9f, 0x2c, - 0xaf, 0xe5, 0xf4, 0x1c, 0x57, 0x86, 0x10, 0x5a, 0x1a, 0x71, 0x42, 0xb0, 0x91, 0x4a, 0x60, 0x5b, 0x61, 0xb9, 0x53, - 0x25, 0x4a, 0x06, 0x7d, 0x60, 0x8c, 0xec, 0x0e, 0x8a, 0x13, 0xd9, 0x10, 0xda, 0x9a, 0x8e, 0x08, 0x62, 0xe6, 0x7f, - 0x1f, 0xe4, 0x0c, 0xf0, 0xf8, 0xad, 0x64, 0x48, 0x85, 0x00, 0xcd, 0x1c, 0x2f, 0x2f, 0x03, 0x76, 0x31, 0xc4, 0x95, - 0xb2, 0xb8, 0x00, 0xc4, 0x66, 0x1f, 0x9b, 0x71, 0x90, 0xfb, 0x41, 0xea, 0x1c, 0xd6, 0x65, 0x07, 0xa2, 0xd2, 0x0b, - 0xe1, 0xf9, 0x35, 0x0d, 0xd5, 0xa4, 0x09, 0xe3, 0x75, 0xcc, 0x20, 0x66, 0x45, 0x69, 0x23, 0x05, 0x61, 0x95, 0x82, - 0x43, 0x60, 0x8e, 0x34, 0xcb, 0x84, 0xfa, 0xd2, 0x22, 0xc1, 0x1b, 0x0a, 0x01, 0xdb, 0xf1, 0x5a, 0xa2, 0x01, 0x1c, - 0x18, 0xb3, 0x61, 0x87, 0xa4, 0x95, 0x05, 0xf5, 0x40, 0xf9, 0x59, 0x5f, 0x78, 0x3c, 0x23, 0x99, 0xf0, 0xc1, 0x03, - 0x47, 0xf3, 0xa5, 0xa8, 0xe7, 0xe6, 0x44, 0x4b, 0xda, 0xe4, 0x10, 0x7f, 0x22, 0xaa, 0xb5, 0x68, 0xc5, 0x03, 0x5f, - 0x01, 0xa8, 0x90, 0xa6, 0x82, 0x4a, 0x64, 0x49, 0x59, 0x54, 0x64, 0x1e, 0x4c, 0xcc, 0xb5, 0x05, 0x56, 0xd6, 0xa8, - 0x77, 0xfd, 0x08, 0x7e, 0xa6, 0x84, 0x4a, 0xad, 0x3b, 0xc4, 0x3f, 0x65, 0xfc, 0x35, 0x84, 0x14, 0x99, 0x9f, 0x19, - 0xb9, 0xcb, 0x63, 0x9e, 0x3d, 0xb2, 0xfa, 0xb7, 0x7d, 0x1b, 0x8e, 0x2e, 0xdc, 0x63, 0x92, 0xab, 0xf7, 0x48, 0x64, - 0x64, 0x33, 0x01, 0x41, 0xb7, 0xf1, 0x7a, 0x38, 0x4a, 0xc5, 0x1f, 0x9b, 0x14, 0x6f, 0xab, 0x0a, 0xa2, 0xf6, 0xa2, - 0x85, 0x54, 0x93, 0x9e, 0x03, 0x69, 0xd0, 0x9c, 0x34, 0x6a, 0xd0, 0xb5, 0x07, 0x2a, 0x54, 0x04, 0xe5, 0x8f, 0x0e, - 0x27, 0x26, 0xca, 0xf0, 0x14, 0xee, 0x2f, 0x4c, 0x46, 0x98, 0x39, 0x02, 0x55, 0xed, 0xa2, 0xe5, 0xf3, 0x16, 0x63, - 0x02, 0x79, 0xef, 0xfe, 0x0d, 0xf3, 0x23, 0xaa, 0x61, 0xb3, 0xe5, 0xbf, 0xf9, 0x33, 0x4f, 0x29, 0x2a, 0x27, 0xc2, - 0x54, 0x48, 0xa8, 0xb1, 0xb3, 0xa4, 0xd0, 0xa3, 0x8b, 0x58, 0xc3, 0x2c, 0x3f, 0x59, 0x73, 0x75, 0x63, 0x08, 0x19, - 0x23, 0x10, 0x9c, 0x21, 0xc4, 0xa8, 0xf2, 0x44, 0x39, 0x78, 0x9b, 0x00, 0xb8, 0x04, 0xf5, 0x18, 0x2c, 0x73, 0xfb, - 0x12, 0xa1, 0xb9, 0x9c, 0xf7, 0x1f, 0x71, 0x29, 0x19, 0x29, 0x7e, 0x96, 0x97, 0x59, 0xf7, 0x52, 0x79, 0x2a, 0x6e, - 0x5d, 0xd1, 0x56, 0xdc, 0x06, 0x0f, 0x98, 0x82, 0x3e, 0xca, 0x4a, 0x6d, 0xf4, 0x69, 0x16, 0x35, 0xbb, 0x64, 0xdb, - 0x63, 0x77, 0x63, 0x79, 0x52, 0x70, 0x27, 0xbc, 0x84, 0x69, 0x19, 0x4a, 0xdd, 0x36, 0x5e, 0x8a, 0x7d, 0x38, 0xc7, - 0x79, 0xf9, 0x2d, 0x53, 0xbc, 0xff, 0x8e, 0xe2, 0xd3, 0xd7, 0x2c, 0xcd, 0x30, 0x3f, 0x3a, 0x2a, 0x94, 0xc0, 0xcc, - 0x7a, 0xac, 0xe6, 0x51, 0x12, 0x6b, 0x28, 0x40, 0x87, 0x05, 0x43, 0x7b, 0xbc, 0xde, 0x82, 0x78, 0xa8, 0xb2, 0x1e, - 0x2c, 0xbc, 0x27, 0x33, 0x74, 0xb5, 0xa4, 0x0d, 0xad, 0x6b, 0xa2, 0xa2, 0x4f, 0xef, 0x91, 0xc5, 0xdd, 0xfa, 0x38, - 0x4e, 0x9a, 0x18, 0x15, 0x97, 0xa2, 0xdd, 0x59, 0x37, 0x5e, 0x84, 0x99, 0x78, 0x8c, 0x9c, 0x11, 0x19, 0x6b, 0xe9, - 0x8c, 0x96, 0xdc, 0xba, 0x04, 0x99, 0x4f, 0x06, 0x7a, 0x27, 0x50, 0x7e, 0xfe, 0x28, 0x07, 0x8e, 0x08, 0x00, 0xb5, - 0xdb, 0x16, 0x84, 0x2c, 0x38, 0xf0, 0xbf, 0x2c, 0xb7, 0x7d, 0x9f, 0xbc, 0xb3, 0x7c, 0x31, 0x2b, 0xe0, 0x7c, 0x63, - 0xa3, 0xbd, 0x11, 0xb7, 0xb3, 0x91, 0x0d, 0x1d, 0x4f, 0x24, 0xd4, 0x1f, 0x66, 0xe6, 0xd9, 0x31, 0x1f, 0x18, 0x09, - 0xce, 0x46, 0x47, 0x60, 0xbb, 0x2a, 0x73, 0xfd, 0x37, 0x49, 0xde, 0x59, 0x12, 0x23, 0x5c, 0x51, 0x81, 0xac, 0x1e, - 0x64, 0x41, 0xb0, 0xb8, 0xa5, 0x2b, 0xba, 0xde, 0xe7, 0x15, 0x89, 0x36, 0x91, 0x29, 0xb9, 0x1e, 0x20, 0xa0, 0xab, - 0x57, 0x3d, 0x3e, 0x55, 0xf7, 0xc6, 0xfb, 0x52, 0xe3, 0x85, 0xf6, 0xf7, 0xb5, 0x9d, 0x3a, 0xdc, 0xbc, 0xe7, 0x7a, - 0x9e, 0xf6, 0x8f, 0x12, 0x75, 0x7a, 0x2d, 0x32, 0x9e, 0x87, 0xfb, 0x35, 0x94, 0xd3, 0x48, 0x35, 0x69, 0x25, 0xa1, - 0xb8, 0x11, 0xcb, 0xbc, 0xe3, 0x96, 0x07, 0xbc, 0x0a, 0xbf, 0x69, 0xb5, 0x10, 0xef, 0x7d, 0x64, 0x58, 0x9e, 0x7a, - 0x77, 0x34, 0x61, 0xf6, 0x50, 0x2b, 0xbb, 0x29, 0x26, 0x60, 0x3f, 0xad, 0xfe, 0xc9, 0xae, 0xca, 0x17, 0x17, 0xf3, - 0x3f, 0xbb, 0xae, 0xb2, 0x91, 0xf1, 0x64, 0x9a, 0xf5, 0xdd, 0xf3, 0xf9, 0x87, 0x3f, 0x7b, 0x1a, 0x4e, 0xe6, 0xc3, - 0x2c, 0xda, 0x7f, 0x29, 0xbf, 0x2c, 0x1a, 0x42, 0x5b, 0xfe, 0xe8, 0x2c, 0xf5, 0xcc, 0x42, 0xef, 0x85, 0x00, 0x3e, - 0x2d, 0xda, 0x1d, 0x1c, 0xd5, 0x74, 0x3d, 0x8c, 0xf7, 0x41, 0x0b, 0xb7, 0x2e, 0x77, 0x20, 0xce, 0x6c, 0xc4, 0x49, - 0x7e, 0x51, 0xb3, 0xeb, 0x5d, 0x36, 0xb3, 0x69, 0x97, 0xbf, 0x23, 0x08, 0x9f, 0x4d, 0x90, 0xd1, 0x3a, 0xd6, 0x36, - 0xa9, 0xbc, 0x0a, 0x2c, 0xff, 0x8d, 0xe4, 0xd3, 0xb9, 0x36, 0x8c, 0x6e, 0x05, 0xfb, 0xf9, 0xa7, 0xf0, 0x6d, 0xd5, - 0x77, 0xc7, 0x9d, 0xff, 0x7c, 0xba, 0x5f, 0xfd, 0x3b, 0xd5, 0x93, 0xb9, 0x59, 0xf4, 0xde, 0xf3, 0xe0, 0xfb, 0xd3, - 0xa0, 0xdb, 0xb6, 0x7a, 0xb2, 0x7d, 0xfc, 0x7f, 0x17, 0xef, 0xff, 0x27, 0xfa, 0xfa, 0x15, 0x7b, 0x64, 0x3e, 0xf4, - 0xe2, 0xf8, 0x6c, 0xea, 0xe2, 0xf2, 0xff, 0x5c, 0xab, 0xdb, 0x3b, 0xcf, 0x6a, 0xb4, 0x6b, 0x6e, 0xb9, 0xee, 0xb5, - 0xed, 0x32, 0xaa, 0x9c, 0xc0, 0xad, 0xff, 0x7a, 0xea, 0x2b, 0x08, 0xf2, 0x79, 0xe3, 0xe5, 0x7e, 0xf6, 0xf0, 0xb6, - 0x26, 0xb3, 0xcf, 0x96, 0x7b, 0x27, 0x2f, 0xfc, 0xbc, 0x1e, 0x6c, 0xfb, 0x54, 0xea, 0x28, 0xd5, 0xac, 0xf8, 0x61, - 0x4d, 0x72, 0xb6, 0x2b, 0xe7, 0xfc, 0xd3, 0xf2, 0x79, 0xfd, 0xff, 0xc5, 0xf3, 0x6f, 0x76, 0xba, 0x47, 0xd8, 0x6f, - 0x61, 0x5f, 0x63, 0x3f, 0xab, 0x4f, 0xe6, 0x70, 0xf5, 0x29, 0xde, 0xba, 0xee, 0x6d, 0xb5, 0x6b, 0xee, 0xe2, 0x8a, - 0x39, 0x75, 0xc9, 0x95, 0xb3, 0x7a, 0x2a, 0x88, 0x0b, 0x62, 0x11, 0x48, 0x7c, 0x57, 0xff, 0x1d, 0xdc, 0xd5, 0x43, - 0xef, 0x95, 0x8b, 0x6d, 0xcf, 0x6d, 0x02, 0xec, 0x9a, 0xc8, 0x8c, 0x11, 0x2b, 0x62, 0x1b, 0xed, 0x28, 0xf0, 0x01, - 0x8b, 0xb6, 0xcf, 0x9f, 0x12, 0x9f, 0xf5, 0xbb, 0x90, 0x6b, 0x70, 0x7d, 0x7f, 0xfd, 0x5a, 0x3c, 0x93, 0x3f, 0x08, - 0x29, 0xfa, 0xf9, 0xc0, 0x53, 0xfd, 0x8b, 0x54, 0x98, 0x42, 0x54, 0x27, 0x2c, 0x6b, 0x86, 0x7d, 0x65, 0xdf, 0x47, - 0x8b, 0x43, 0x05, 0x6a, 0x3d, 0xde, 0x27, 0x44, 0x3e, 0xaa, 0x48, 0x77, 0xf9, 0x77, 0x2a, 0x82, 0xc0, 0x88, 0x28, - 0x30, 0x34, 0xc8, 0xa7, 0x9d, 0x03, 0x7f, 0x81, 0xe5, 0xfe, 0xa2, 0xb8, 0x7a, 0xb7, 0x4b, 0x4a, 0x35, 0x5c, 0xcd, - 0x1b, 0xeb, 0x14, 0x20, 0x25, 0x36, 0x11, 0x01, 0xc3, 0x7f, 0x72, 0xe5, 0x0b, 0xb6, 0x5e, 0xe7, 0x69, 0x3e, 0x19, - 0x55, 0xa7, 0x26, 0x37, 0x5b, 0x0b, 0x23, 0x5d, 0x0b, 0xaf, 0x3a, 0x3a, 0xe1, 0x94, 0x63, 0xcc, 0x57, 0x94, 0x36, - 0x1e, 0x71, 0xa7, 0xfe, 0xf6, 0x2a, 0xfe, 0xdc, 0x80, 0x84, 0x72, 0xcb, 0x6c, 0x70, 0x95, 0x99, 0xbd, 0x56, 0x70, - 0xa3, 0x24, 0x4c, 0xf1, 0x12, 0xf2, 0x8c, 0xdf, 0x73, 0xb1, 0x32, 0x05, 0xb6, 0x69, 0x7a, 0xff, 0xc7, 0xf6, 0x24, - 0x28, 0x04, 0x3a, 0xf1, 0x52, 0x80, 0x04, 0xaa, 0xbe, 0x21, 0xc8, 0x37, 0x3d, 0xe2, 0xa0, 0xad, 0xa9, 0x8d, 0xf3, - 0x4c, 0xb3, 0x68, 0x33, 0xde, 0xd5, 0x95, 0x92, 0xb9, 0x9e, 0x11, 0x8d, 0xbc, 0xb6, 0xed, 0xf5, 0x66, 0xcc, 0xec, - 0x7a, 0x38, 0x07, 0x74, 0xbe, 0x0e, 0xa0, 0x9f, 0x55, 0x07, 0x96, 0x2a, 0x4e, 0x7f, 0xb9, 0x03, 0x32, 0xb0, 0xa4, - 0x43, 0x0f, 0x53, 0x0a, 0x9f, 0xa5, 0xf4, 0x1f, 0x01, 0x3b, 0x05, 0x0e, 0x4b, 0x39, 0x16, 0xd9, 0x8d, 0xd9, 0xcc, - 0xda, 0xd6, 0xe4, 0xa4, 0x33, 0x17, 0x51, 0xf6, 0x7c, 0x6d, 0x57, 0xff, 0x5d, 0xac, 0xfa, 0x78, 0xc9, 0xc6, 0x29, - 0x20, 0x05, 0x05, 0x05, 0xb1, 0xc7, 0xf2, 0xf3, 0xd0, 0x2f, 0x19, 0x91, 0x41, 0x34, 0xbd, 0x1a, 0x74, 0xfa, 0x79, - 0xd2, 0x5f, 0x58, 0x94, 0xd4, 0x4a, 0xe8, 0x0f, 0xb8, 0xe1, 0x03, 0x77, 0xe7, 0x8c, 0x38, 0x37, 0x84, 0xfc, 0x34, - 0xf5, 0x23, 0xe6, 0x6e, 0xe6, 0x64, 0x16, 0x3f, 0x07, 0x72, 0xd2, 0x82, 0xd5, 0xf8, 0x13, 0x36, 0x82, 0xdc, 0x37, - 0x7e, 0x69, 0xa9, 0x02, 0x47, 0x97, 0x2b, 0xe9, 0xe9, 0xd2, 0xc9, 0x62, 0xa1, 0x75, 0x70, 0x77, 0x8a, 0xe0, 0x8b, - 0xb7, 0xc2, 0x3a, 0xff, 0xe5, 0x72, 0xeb, 0xae, 0x38, 0x1b, 0x36, 0xa2, 0x7e, 0x76, 0xbf, 0xba, 0x75, 0x1a, 0xbe, - 0x47, 0xbf, 0x8b, 0x57, 0xdf, 0xf5, 0x78, 0x21, 0x47, 0xb7, 0x6e, 0x0d, 0x56, 0xf3, 0x74, 0x05, 0x45, 0x33, 0xb0, - 0x2f, 0x5e, 0x94, 0x83, 0x2f, 0x60, 0x34, 0xee, 0x78, 0x11, 0x6c, 0x0a, 0xed, 0xf3, 0xce, 0xf3, 0xe5, 0x15, 0x55, - 0x93, 0x85, 0xf4, 0x76, 0xcd, 0xc6, 0xf0, 0xfe, 0xba, 0xbd, 0xf9, 0x59, 0xfc, 0x54, 0x7c, 0x85, 0x46, 0xe8, 0xb7, - 0x0b, 0x40, 0xba, 0xfa, 0x92, 0x15, 0x8f, 0x3e, 0x6f, 0x85, 0x58, 0xcd, 0xf7, 0x8e, 0x67, 0xb1, 0x42, 0xe0, 0xe3, - 0x5b, 0xd1, 0xeb, 0x25, 0x3c, 0xb3, 0x9a, 0x75, 0x82, 0xb9, 0x03, 0x88, 0x50, 0x44, 0x1a, 0x34, 0x11, 0xe4, 0xbb, - 0xb3, 0x61, 0x2e, 0x54, 0x67, 0x35, 0x2f, 0xff, 0xbc, 0xbc, 0xa2, 0x1e, 0x51, 0x2f, 0x7e, 0x73, 0xbd, 0x81, 0x44, - 0xab, 0xae, 0x85, 0x1a, 0xbf, 0x4b, 0x8d, 0x53, 0xe6, 0x41, 0x03, 0x24, 0x69, 0xe8, 0x50, 0xcb, 0xfb, 0x10, 0x8f, - 0x8b, 0xed, 0x6b, 0x34, 0x7e, 0xdf, 0xc4, 0x5a, 0x11, 0x15, 0x79, 0x4f, 0x81, 0x2c, 0x0e, 0x94, 0x50, 0x5a, 0x5e, - 0xc8, 0xdf, 0x15, 0xa6, 0x5a, 0x64, 0xce, 0xc3, 0xc4, 0x59, 0xa0, 0xfe, 0x7a, 0xf4, 0xd4, 0xfb, 0x52, 0x07, 0x7c, - 0x63, 0x61, 0x03, 0xd7, 0x73, 0x43, 0x8d, 0x60, 0x1c, 0xa4, 0x48, 0x35, 0xa8, 0x0d, 0x33, 0x6a, 0x30, 0x79, 0x4d, - 0xc7, 0x96, 0x62, 0xaf, 0xa4, 0x3f, 0xe4, 0x99, 0x2e, 0xac, 0xf2, 0x6d, 0xd5, 0x23, 0x3b, 0xbd, 0x8d, 0x0b, 0xfe, - 0x59, 0xf3, 0xde, 0x7c, 0x8d, 0x78, 0xba, 0x6c, 0x48, 0xb0, 0xa4, 0xe7, 0xc9, 0x71, 0xeb, 0xfc, 0xa2, 0xba, 0x9e, - 0xab, 0x78, 0x04, 0x99, 0x12, 0x2e, 0x09, 0xb9, 0x8e, 0x73, 0xbd, 0x97, 0x30, 0x10, 0x21, 0x5f, 0xd7, 0x67, 0x09, - 0x50, 0xda, 0xd6, 0x97, 0x50, 0x0b, 0x1f, 0x4a, 0xb3, 0xaf, 0xdd, 0x12, 0x8e, 0xcc, 0x7d, 0x57, 0xec, 0x5d, 0x1d, - 0x39, 0x5d, 0x38, 0x24, 0x15, 0xa0, 0xef, 0xb9, 0xe8, 0xf0, 0x8d, 0xb1, 0xb0, 0x48, 0xc4, 0xa6, 0x37, 0xd1, 0x8d, - 0x66, 0xdd, 0xe0, 0x57, 0x27, 0x9d, 0x06, 0x5f, 0x1b, 0x38, 0x15, 0xb1, 0xa6, 0x00, 0x3b, 0xf4, 0xb8, 0xb1, 0x3b, - 0x40, 0x88, 0x6f, 0x5a, 0xed, 0x84, 0xce, 0xd6, 0x8d, 0x23, 0x14, 0x04, 0x03, 0xea, 0x45, 0xd4, 0x8a, 0xf2, 0xfb, - 0x4a, 0x27, 0x11, 0xf5, 0x06, 0xbf, 0x7b, 0x7d, 0xef, 0xe5, 0xa9, 0x2a, 0xbe, 0xde, 0xab, 0x07, 0x7a, 0xb2, 0x1d, - 0x90, 0xd8, 0x34, 0x15, 0x6b, 0x40, 0x93, 0x67, 0x4e, 0x81, 0xb8, 0xc1, 0x3c, 0xf7, 0x61, 0x3f, 0xc6, 0x7c, 0x8d, - 0x40, 0x8d, 0x4e, 0x84, 0x6a, 0x49, 0xa4, 0x2f, 0x57, 0x2a, 0x63, 0xdd, 0x2c, 0xe2, 0xd9, 0x45, 0x93, 0xf7, 0x7f, - 0x6f, 0x1c, 0x5c, 0xd7, 0x54, 0x50, 0x6e, 0xda, 0x33, 0xff, 0xeb, 0x9a, 0xc2, 0x06, 0xc0, 0x03, 0x7c, 0x5d, 0x42, - 0x72, 0x65, 0xc0, 0x87, 0x6f, 0x0a, 0x75, 0x5e, 0xdd, 0xe6, 0x2d, 0x64, 0x65, 0x40, 0xe4, 0xb4, 0x7d, 0x80, 0xf0, - 0x36, 0x60, 0x0c, 0x23, 0x2e, 0x40, 0xf4, 0xd1, 0x31, 0xdd, 0xaa, 0x69, 0x20, 0xee, 0xca, 0x56, 0x1f, 0xcf, 0x04, - 0xbc, 0x12, 0x7e, 0x63, 0x18, 0xc7, 0x10, 0xe2, 0x33, 0x8e, 0xed, 0xf1, 0x25, 0x54, 0x0e, 0x34, 0xca, 0x83, 0x55, - 0x79, 0xfc, 0x39, 0xf7, 0xfe, 0xea, 0xab, 0x66, 0x64, 0xd3, 0x80, 0xb9, 0xd1, 0xb4, 0xa1, 0x63, 0xc5, 0xba, 0xe4, - 0x6f, 0xbd, 0x43, 0x63, 0xb0, 0x2f, 0x5b, 0x5f, 0xc2, 0xfd, 0x4e, 0x46, 0xc3, 0x0d, 0x8c, 0xc0, 0x18, 0x0c, 0x54, - 0x95, 0x52, 0x90, 0xfe, 0xe2, 0xa5, 0x9d, 0x8b, 0x92, 0xf7, 0xa4, 0x93, 0xbd, 0x11, 0xca, 0x83, 0x42, 0x8b, 0x81, - 0x8b, 0x7e, 0x53, 0x6b, 0x25, 0xee, 0x63, 0xf9, 0xae, 0x8f, 0xd6, 0x54, 0xba, 0x99, 0x11, 0xd9, 0x52, 0x87, 0x51, - 0xdc, 0x9c, 0x3b, 0x69, 0xe6, 0xa1, 0x53, 0x60, 0x91, 0xe6, 0x66, 0x79, 0x00, 0xe2, 0x5b, 0xd4, 0xc8, 0xe6, 0x94, - 0xff, 0x89, 0x47, 0xdd, 0x40, 0x88, 0xc8, 0xb2, 0xbe, 0x6b, 0x8a, 0x33, 0x28, 0x94, 0xe4, 0x06, 0x85, 0xf0, 0xde, - 0xc8, 0xa0, 0x40, 0xc9, 0x52, 0xd9, 0x48, 0xfa, 0xfd, 0x27, 0x1e, 0x54, 0xe8, 0xe9, 0xce, 0x91, 0x64, 0xeb, 0x36, - 0x0b, 0x6b, 0x28, 0x8d, 0x32, 0x31, 0xbb, 0xd9, 0xc9, 0xb7, 0x05, 0x05, 0x45, 0x49, 0x39, 0x51, 0xa4, 0x19, 0x0e, - 0x77, 0xfa, 0x5f, 0xee, 0x51, 0xde, 0xb1, 0x40, 0xb9, 0xcd, 0x9c, 0x96, 0x00, 0x01, 0x44, 0xfd, 0x5c, 0x40, 0x34, - 0x51, 0xa4, 0x14, 0x72, 0x79, 0x23, 0x2f, 0xf3, 0xd1, 0xad, 0x79, 0xca, 0x41, 0xfb, 0xca, 0xfe, 0x94, 0x30, 0xe7, - 0xb6, 0x92, 0x3e, 0x92, 0x31, 0x31, 0x52, 0x17, 0xdc, 0xd0, 0x81, 0x61, 0xbd, 0x77, 0xe8, 0xe5, 0x53, 0x63, 0xc2, - 0xef, 0x2f, 0x82, 0x22, 0x88, 0x42, 0x00, 0x80, 0x69, 0x59, 0xb6, 0xa4, 0xf0, 0x49, 0x12, 0x05, 0x90, 0xf5, 0xb8, - 0xf4, 0xc0, 0xb5, 0x24, 0x30, 0x3c, 0xaa, 0x09, 0x68, 0xde, 0x2e, 0x50, 0x38, 0xa0, 0x85, 0x95, 0xeb, 0xb0, 0x76, - 0x42, 0xaa, 0x26, 0x45, 0xab, 0x9b, 0xd5, 0x92, 0x94, 0x67, 0x06, 0x4c, 0x15, 0x91, 0xa7, 0xf5, 0x3f, 0x64, 0xbe, - 0xb4, 0x40, 0xf4, 0xc6, 0x7c, 0x16, 0x5c, 0x3f, 0x56, 0x3b, 0x8e, 0x5e, 0x37, 0x4c, 0x6b, 0x37, 0x48, 0x02, 0x44, - 0x3e, 0x95, 0xd5, 0xd5, 0x2b, 0x15, 0xa4, 0xa1, 0xc6, 0x8f, 0x7c, 0xaf, 0x14, 0xe4, 0x4a, 0xe9, 0xa5, 0xa0, 0x00, - 0xdd, 0x78, 0xe9, 0x88, 0xa7, 0x6c, 0xe9, 0xc5, 0xa6, 0xb0, 0x71, 0xc2, 0xd8, 0xeb, 0xd9, 0x8a, 0x93, 0x7a, 0xec, - 0xaa, 0x4e, 0xb2, 0x04, 0x5d, 0x5c, 0x4b, 0x5e, 0xc5, 0x91, 0xe9, 0xd2, 0xd4, 0x31, 0xf5, 0xef, 0x1a, 0xed, 0x89, - 0x15, 0xba, 0xff, 0x2d, 0x91, 0x3b, 0xaf, 0x4c, 0xd3, 0x02, 0x41, 0xd6, 0x82, 0x90, 0xe0, 0x7c, 0x27, 0x44, 0x1e, - 0x95, 0xc7, 0xa4, 0x65, 0xee, 0xf1, 0xb5, 0x6e, 0xc6, 0x53, 0xda, 0x03, 0x51, 0x3e, 0xcc, 0x71, 0x97, 0x12, 0xe6, - 0x9e, 0x3d, 0xb0, 0x32, 0x4c, 0x4c, 0xec, 0x83, 0x1e, 0x3d, 0xae, 0x59, 0x01, 0xc1, 0x30, 0xfd, 0xda, 0xa5, 0xdd, - 0xed, 0xfa, 0x61, 0x0b, 0xe0, 0x5d, 0x2e, 0x84, 0xfa, 0xb9, 0x3a, 0x71, 0x53, 0x78, 0x75, 0x83, 0xb6, 0x8a, 0xd5, - 0x1a, 0x54, 0xb4, 0xdc, 0xa1, 0x6d, 0xeb, 0xcf, 0x69, 0x06, 0x1f, 0x3b, 0x39, 0x10, 0x6a, 0x3a, 0x22, 0x98, 0x89, - 0x72, 0x24, 0x0d, 0x9e, 0xb8, 0xea, 0x6c, 0x91, 0xaa, 0x77, 0x73, 0x02, 0x64, 0x48, 0xea, 0x0b, 0x32, 0x84, 0x36, - 0x20, 0x74, 0xac, 0xa9, 0xf2, 0xb5, 0x41, 0xed, 0xd6, 0x13, 0x63, 0x6f, 0xdf, 0x84, 0x16, 0x45, 0x85, 0xbe, 0x2d, - 0x16, 0xbb, 0x64, 0x8c, 0xbe, 0xe0, 0x6f, 0xcc, 0x7e, 0x92, 0xd1, 0xc3, 0x67, 0xb5, 0xd1, 0x45, 0x3b, 0x88, 0x5d, - 0x60, 0xfc, 0xa3, 0xc9, 0xdb, 0x9a, 0x55, 0x5c, 0x7e, 0x75, 0x41, 0x55, 0xab, 0xd9, 0x62, 0xfa, 0xb3, 0xae, 0x70, - 0x89, 0x64, 0xa2, 0xc4, 0x0c, 0x7f, 0x10, 0x68, 0xf5, 0x7d, 0xe0, 0xdc, 0xe7, 0xb9, 0x9e, 0xa0, 0xff, 0xe2, 0xa5, - 0x77, 0x28, 0xa3, 0x42, 0xc8, 0xc7, 0x91, 0x2f, 0xa4, 0x88, 0x55, 0xba, 0x7b, 0xb4, 0xd1, 0x12, 0x39, 0x0b, 0x64, - 0xcd, 0x6a, 0xca, 0x34, 0xd4, 0x85, 0x63, 0x8b, 0x2e, 0xd3, 0x6c, 0x17, 0xd0, 0x32, 0xac, 0xa4, 0x63, 0xeb, 0x9d, - 0x05, 0x84, 0x22, 0x22, 0x80, 0x29, 0x69, 0xf0, 0x9f, 0x5d, 0x69, 0x8b, 0xc5, 0xdc, 0xb4, 0x94, 0x7d, 0x2e, 0x23, - 0x31, 0xd7, 0x13, 0xb2, 0x1b, 0xb8, 0x5e, 0xdc, 0x08, 0x4d, 0x6b, 0x84, 0xe4, 0x24, 0xd1, 0xa3, 0x5e, 0xa8, 0x37, - 0x55, 0x59, 0x34, 0x7a, 0x40, 0x54, 0x03, 0xd7, 0xbb, 0x69, 0x27, 0x52, 0x12, 0x2c, 0x15, 0xdd, 0x07, 0x6e, 0xdd, - 0xad, 0x75, 0x98, 0x70, 0x85, 0x9c, 0x97, 0x50, 0x23, 0x86, 0x84, 0xfb, 0x80, 0x3d, 0x94, 0x4c, 0x00, 0x98, 0x82, - 0x13, 0x68, 0x09, 0xb0, 0xed, 0x56, 0x50, 0x02, 0x06, 0xac, 0xcc, 0x34, 0xa2, 0x30, 0xf3, 0xd0, 0x15, 0x26, 0xe4, - 0x38, 0x37, 0x8f, 0x3a, 0x58, 0x90, 0x2a, 0x44, 0xdb, 0xef, 0x4d, 0x0f, 0xe6, 0x38, 0x33, 0xce, 0x91, 0x0b, 0x80, - 0xe3, 0x2d, 0x28, 0xd5, 0x30, 0x34, 0x6c, 0xff, 0xaa, 0xc9, 0x2a, 0x67, 0x44, 0x62, 0xd5, 0x2b, 0x9b, 0xfd, 0x2a, - 0xfe, 0x18, 0x0a, 0x2a, 0x69, 0x3a, 0xbe, 0x49, 0x4c, 0x3d, 0x5b, 0x5e, 0x7d, 0x65, 0x78, 0xd2, 0xb3, 0x7d, 0xc0, - 0x15, 0x8f, 0xc0, 0xba, 0x29, 0xf9, 0x51, 0x27, 0x83, 0x06, 0xe0, 0xa8, 0x45, 0x3b, 0x54, 0x9d, 0x62, 0x10, 0x30, - 0xe2, 0x74, 0x5a, 0x96, 0xfc, 0x25, 0x8a, 0x0d, 0x34, 0xf1, 0x18, 0x2f, 0x58, 0x3a, 0xb1, 0xbd, 0xa3, 0xf9, 0xaa, - 0x44, 0x23, 0xcb, 0xac, 0x0d, 0x92, 0xfc, 0x36, 0xbd, 0xd6, 0x2a, 0x23, 0xed, 0x6d, 0xd9, 0x21, 0xfe, 0x11, 0xc8, - 0x82, 0x31, 0xa3, 0x22, 0x51, 0x31, 0x45, 0xc6, 0xa5, 0xf1, 0x56, 0xb2, 0x00, 0x5d, 0xa6, 0x67, 0x6b, 0xf3, 0x8a, - 0xbc, 0x7d, 0x12, 0xcd, 0x7d, 0x30, 0x55, 0x61, 0xff, 0x72, 0x34, 0x5b, 0x1e, 0xab, 0xf0, 0x8f, 0x55, 0x75, 0x04, - 0x9a, 0xb6, 0xab, 0xa7, 0x40, 0x8e, 0x4e, 0xd5, 0xc5, 0x21, 0x39, 0xf6, 0xc2, 0x8c, 0x43, 0x12, 0x72, 0xb2, 0x78, - 0x1b, 0xac, 0x4f, 0x32, 0xb4, 0x46, 0x80, 0x2f, 0x17, 0x61, 0xd5, 0x2b, 0xcd, 0x1e, 0x65, 0xb2, 0x1a, 0x59, 0x2b, - 0x28, 0x4d, 0x10, 0x45, 0xf3, 0x14, 0x09, 0x03, 0xcf, 0x72, 0xa2, 0x30, 0x61, 0x38, 0x25, 0xec, 0x43, 0xa2, 0x8b, - 0xf6, 0x0f, 0x33, 0xcb, 0x87, 0x12, 0xb0, 0xa5, 0x79, 0x12, 0x20, 0x46, 0x80, 0x51, 0xa5, 0x58, 0xd1, 0x3f, 0x38, - 0x4f, 0x1c, 0x0f, 0x73, 0x49, 0x22, 0x3f, 0xe3, 0xfd, 0x91, 0x79, 0xd3, 0xcd, 0xcb, 0x23, 0xdb, 0x90, 0x26, 0x66, - 0xaa, 0xa7, 0x70, 0x8d, 0xd8, 0x6e, 0xbb, 0x80, 0x2d, 0x54, 0xba, 0x41, 0xb5, 0x2f, 0x8a, 0x20, 0xf4, 0x2f, 0x75, - 0x90, 0xd6, 0xfc, 0x37, 0x47, 0x1b, 0x4c, 0x8c, 0xde, 0x64, 0x07, 0x8c, 0xfb, 0x66, 0xaa, 0xba, 0x96, 0x40, 0xc7, - 0xa6, 0x2a, 0xfc, 0x76, 0x70, 0x09, 0x89, 0xb9, 0x32, 0x16, 0xbd, 0xd5, 0x19, 0x59, 0xe5, 0xfe, 0xdf, 0x36, 0x1d, - 0x41, 0xb7, 0x7f, 0x9d, 0x5d, 0xcd, 0xce, 0x03, 0x64, 0x91, 0x07, 0x8e, 0x88, 0xa5, 0x7a, 0x6a, 0xf3, 0x68, 0x58, - 0x58, 0xaa, 0x2b, 0xc7, 0xfb, 0xb8, 0x92, 0x36, 0x9f, 0x97, 0x86, 0x03, 0x22, 0x72, 0x30, 0xbd, 0x35, 0xf0, 0x5b, - 0x24, 0x32, 0xaf, 0x6a, 0x1c, 0xd1, 0xa9, 0x8b, 0x71, 0x31, 0xae, 0x15, 0x94, 0x46, 0x7e, 0xdc, 0x49, 0x3f, 0x46, - 0x47, 0x4b, 0x1f, 0x9f, 0x6e, 0xad, 0x8a, 0xee, 0xd5, 0x2f, 0x72, 0x28, 0xe6, 0x65, 0x19, 0x1d, 0x08, 0x19, 0x24, - 0x7b, 0x4f, 0xbe, 0xf3, 0x9e, 0xb8, 0xcc, 0x45, 0x4f, 0x8d, 0x0a, 0x0e, 0xbd, 0xbd, 0x8d, 0x2c, 0x53, 0x39, 0x72, - 0x07, 0xcc, 0xce, 0xf8, 0xda, 0xde, 0x40, 0x6c, 0xef, 0x85, 0xc8, 0xad, 0xf0, 0x48, 0x61, 0xfa, 0x71, 0x65, 0x84, - 0xab, 0x31, 0xe9, 0x50, 0x99, 0x4c, 0xf3, 0xc2, 0x2e, 0x57, 0x59, 0xd0, 0x61, 0x19, 0x54, 0x33, 0x99, 0x99, 0x66, - 0xb2, 0x69, 0xa4, 0xe1, 0x0a, 0xc5, 0x34, 0x06, 0x2e, 0x97, 0x2a, 0x52, 0xf6, 0xbc, 0x92, 0xa5, 0xe7, 0x38, 0x0b, - 0x1d, 0xa6, 0x4d, 0x07, 0xcf, 0x53, 0xe2, 0x92, 0x70, 0x84, 0x35, 0x13, 0x4c, 0x93, 0xac, 0xb4, 0x40, 0xb9, 0xa8, - 0xa4, 0x18, 0xba, 0x3e, 0xaf, 0x24, 0x65, 0xee, 0x68, 0x19, 0x4f, 0x69, 0xf4, 0x8c, 0xf2, 0x15, 0xb5, 0x66, 0xfe, - 0xc9, 0xf2, 0xef, 0x20, 0x85, 0xd6, 0x57, 0x40, 0x05, 0xa6, 0x14, 0xac, 0x04, 0xf9, 0xfb, 0xc5, 0x8d, 0x56, 0x11, - 0x97, 0x82, 0xf3, 0x2a, 0xe6, 0x65, 0x53, 0x0d, 0x69, 0xbe, 0xfe, 0xe4, 0x7f, 0xa6, 0x93, 0x83, 0x4a, 0x1c, 0x6e, - 0x00, 0x33, 0x86, 0x5c, 0x2c, 0xe8, 0x4f, 0xa5, 0x57, 0x5f, 0xa9, 0x97, 0xa2, 0x46, 0x5d, 0xe8, 0xee, 0x96, 0xdc, - 0x5a, 0xcf, 0x46, 0x9a, 0x68, 0x56, 0x2a, 0xdf, 0x0f, 0x92, 0x66, 0x86, 0x1a, 0xe1, 0x62, 0x2f, 0x36, 0x60, 0xdc, - 0x1a, 0xa7, 0x50, 0x7b, 0x2f, 0x59, 0xc2, 0x67, 0x8b, 0xcb, 0x41, 0x95, 0xc2, 0x18, 0xdf, 0x81, 0xb9, 0x21, 0xf7, - 0xc1, 0x93, 0xde, 0x7e, 0xbb, 0xf3, 0x53, 0xbc, 0x0c, 0xec, 0x12, 0x11, 0x0f, 0xa2, 0xdf, 0xdc, 0x2a, 0x6d, 0xaf, - 0x37, 0x16, 0x36, 0x57, 0xc5, 0x0f, 0x2a, 0x55, 0xe0, 0xce, 0x2b, 0x77, 0x61, 0x50, 0x1e, 0x41, 0x0e, 0xfa, 0x4d, - 0xe3, 0xe6, 0x7e, 0x27, 0x54, 0x61, 0xd8, 0xa5, 0x87, 0x49, 0x59, 0xe7, 0x4b, 0x7a, 0x18, 0x33, 0xc4, 0xce, 0xcc, - 0x32, 0xa9, 0xd0, 0xae, 0x65, 0x41, 0xe3, 0xa7, 0xe0, 0x8f, 0x28, 0xa3, 0x48, 0x2b, 0x26, 0xb0, 0x0f, 0x32, 0x01, - 0xc7, 0x07, 0xc1, 0xa8, 0x2e, 0xe2, 0x13, 0x9c, 0xee, 0xce, 0x0b, 0x0e, 0x54, 0x32, 0xb4, 0x48, 0xb0, 0xc4, 0x1e, - 0xf1, 0xb0, 0xa9, 0x1f, 0xec, 0x9d, 0xda, 0x55, 0x38, 0x6f, 0x16, 0xeb, 0x31, 0x48, 0xf5, 0xfc, 0xb6, 0xf9, 0x84, - 0x03, 0xfc, 0x51, 0x9d, 0xea, 0xf1, 0x4d, 0x1d, 0xaf, 0x71, 0x08, 0xab, 0x43, 0xe5, 0x16, 0x7f, 0x52, 0x90, 0xce, - 0xb8, 0xa0, 0x87, 0xfd, 0x2b, 0x69, 0xf1, 0x05, 0x65, 0x37, 0x01, 0x1b, 0xbd, 0xf5, 0xa0, 0x04, 0xa1, 0xf3, 0xfe, - 0xe1, 0xd1, 0x7d, 0x16, 0x14, 0x6b, 0x44, 0x1d, 0x35, 0xf1, 0x6e, 0xb4, 0x9b, 0x54, 0x5c, 0x10, 0xab, 0x36, 0x5b, - 0xed, 0xb0, 0x0c, 0xd1, 0xfb, 0x37, 0x19, 0x59, 0x80, 0xa2, 0xbd, 0xe9, 0x79, 0x19, 0xac, 0x56, 0x4f, 0x13, 0x12, - 0x86, 0x6f, 0x20, 0xab, 0x29, 0x6c, 0x33, 0xdd, 0xca, 0xe8, 0x73, 0x60, 0x8e, 0x9e, 0x74, 0xd6, 0xd4, 0x82, 0xb1, - 0x65, 0xd4, 0x9f, 0x29, 0x0b, 0x27, 0x1f, 0xcb, 0xe0, 0xe7, 0x85, 0x29, 0x75, 0x07, 0x0d, 0xc9, 0x62, 0xc4, 0xca, - 0x4d, 0x3c, 0x74, 0xe8, 0xaa, 0x04, 0x83, 0xf5, 0xdb, 0x7a, 0xe3, 0xac, 0xd7, 0x38, 0x20, 0xf4, 0xde, 0x0f, 0x5c, - 0x2d, 0xfc, 0x10, 0x89, 0x11, 0xde, 0x90, 0x36, 0x47, 0x9d, 0xf1, 0xe2, 0x37, 0xde, 0x1b, 0x43, 0xb9, 0xbd, 0xae, - 0xf8, 0xa3, 0x5f, 0xd7, 0x95, 0x2a, 0x74, 0x25, 0x71, 0x66, 0xee, 0x63, 0x49, 0xd1, 0x23, 0x53, 0xda, 0xc5, 0x3d, - 0x00, 0x58, 0x98, 0x8d, 0x8a, 0xd0, 0xa4, 0x91, 0xb8, 0xfc, 0x54, 0x61, 0xa5, 0x52, 0x9f, 0x50, 0x72, 0x22, 0x30, - 0x0c, 0xbe, 0xff, 0x28, 0xd2, 0x15, 0x47, 0x3f, 0xc0, 0x3f, 0x22, 0x20, 0x50, 0x6b, 0x16, 0x69, 0xa8, 0x1d, 0x90, - 0x8c, 0x9f, 0x0e, 0x17, 0xce, 0xce, 0xcc, 0x88, 0x20, 0x53, 0x77, 0x03, 0x02, 0x84, 0xe1, 0x1a, 0x81, 0x2e, 0xff, - 0x4a, 0x49, 0xdb, 0x96, 0x3b, 0xd4, 0x61, 0x90, 0x5d, 0xe8, 0x20, 0x5a, 0x2d, 0xfa, 0xa5, 0xca, 0xf8, 0x16, 0xd1, - 0xc9, 0xfa, 0xfa, 0xfd, 0xc7, 0xe5, 0x5e, 0xe4, 0x0f, 0x6e, 0x2d, 0x00, 0x98, 0x8d, 0x38, 0x1b, 0x27, 0xbc, 0x6a, - 0x1d, 0x8b, 0x8f, 0xd2, 0x35, 0x86, 0xed, 0x14, 0xb4, 0xe2, 0x41, 0xab, 0x46, 0x54, 0x8a, 0x75, 0x7e, 0xdc, 0x2b, - 0x0f, 0xed, 0x76, 0xef, 0x87, 0xc0, 0xb9, 0x60, 0x47, 0xcc, 0x13, 0xc0, 0xbc, 0xac, 0x5c, 0x15, 0x32, 0x4d, 0xb0, - 0x11, 0x07, 0x39, 0xc8, 0xb4, 0xeb, 0x1e, 0x98, 0xb2, 0x4d, 0x8b, 0xdd, 0x2d, 0x66, 0x61, 0x03, 0x19, 0x21, 0x05, - 0x9b, 0x84, 0x0f, 0xd9, 0x32, 0x09, 0xa5, 0x07, 0x0e, 0x33, 0xd0, 0xd6, 0x7a, 0x14, 0xfb, 0x35, 0x6d, 0x13, 0x5d, - 0xb2, 0xc0, 0xa5, 0x46, 0xb6, 0x6f, 0xfa, 0x84, 0x8e, 0xc5, 0xe4, 0x86, 0xef, 0x77, 0xe7, 0xd5, 0x18, 0x96, 0xed, - 0x63, 0x65, 0x2f, 0xeb, 0xaf, 0x57, 0x2b, 0x50, 0x33, 0x9d, 0xb9, 0x7c, 0xab, 0xe4, 0xdf, 0xf5, 0x61, 0xa0, 0xf6, - 0x42, 0xe1, 0xa3, 0x98, 0x40, 0x59, 0x4b, 0xea, 0x14, 0xbc, 0x35, 0xde, 0xaf, 0xda, 0x61, 0xdc, 0xbf, 0xb9, 0x0b, - 0x15, 0x37, 0xbe, 0xfe, 0xa7, 0x9b, 0xda, 0xe0, 0xe8, 0x8d, 0x70, 0x49, 0xe7, 0x7e, 0xbd, 0x82, 0x00, 0x51, 0x6f, - 0x61, 0xca, 0x3a, 0x6f, 0xaf, 0xae, 0x6b, 0xf2, 0x68, 0x4b, 0x3f, 0xee, 0x82, 0x0e, 0x9b, 0xac, 0x64, 0x6d, 0xa1, - 0x7b, 0x64, 0x35, 0xee, 0x7e, 0x46, 0x38, 0x74, 0xb0, 0x83, 0xd4, 0x2d, 0x3e, 0xf4, 0x0e, 0xd3, 0xeb, 0x94, 0x54, - 0x6f, 0xf5, 0x9b, 0xfa, 0xcb, 0x97, 0xc6, 0xb9, 0x1a, 0x35, 0xac, 0x5d, 0xd4, 0xa4, 0x64, 0x66, 0x0a, 0xa6, 0xdb, - 0x20, 0x85, 0xab, 0xbe, 0xfa, 0xca, 0xe0, 0xc8, 0xf7, 0x73, 0x42, 0x05, 0x9b, 0x10, 0xaa, 0xc7, 0x2f, 0x88, 0xae, - 0x64, 0xfe, 0x71, 0xbb, 0x32, 0x06, 0x49, 0xe8, 0xdb, 0x11, 0x6f, 0xa5, 0xa9, 0xb3, 0x43, 0x3e, 0xe6, 0x6c, 0x82, - 0x5f, 0xd2, 0x84, 0x40, 0xb3, 0xf0, 0x2f, 0x0d, 0xd8, 0xee, 0x70, 0x6c, 0x3d, 0xd0, 0xb8, 0xf8, 0x0f, 0x94, 0x1b, - 0xd1, 0x99, 0x85, 0x1d, 0xef, 0x66, 0xe6, 0x4b, 0x87, 0xc3, 0x9e, 0x61, 0x09, 0x54, 0x65, 0x18, 0xd0, 0x0f, 0xdd, - 0x90, 0xed, 0x52, 0x1d, 0x3b, 0x07, 0x09, 0xeb, 0x3d, 0x14, 0x62, 0x3f, 0x9a, 0xab, 0xcb, 0xeb, 0xe5, 0x81, 0xfb, - 0x0a, 0xc3, 0xe5, 0xc1, 0xb0, 0xf8, 0x98, 0x15, 0x52, 0x75, 0xe9, 0xba, 0x8e, 0x3d, 0xad, 0x31, 0x20, 0x1f, 0x33, - 0x0c, 0x7f, 0x0e, 0x06, 0x8b, 0x76, 0x64, 0xdb, 0x32, 0x38, 0xc3, 0xca, 0xdb, 0x32, 0x65, 0xa6, 0xec, 0x2e, 0xd8, - 0x9e, 0x1a, 0xf8, 0xb3, 0x93, 0x94, 0x11, 0x14, 0x2a, 0xd3, 0x11, 0x34, 0xfa, 0xc7, 0x57, 0x45, 0xad, 0xc8, 0x46, - 0xd6, 0xfc, 0xb6, 0x78, 0x67, 0x8c, 0x53, 0x5a, 0x97, 0xb3, 0x7a, 0x17, 0xab, 0x46, 0x1f, 0x9a, 0x44, 0x2b, 0x92, - 0xb5, 0xaf, 0xb1, 0xc7, 0xc0, 0x10, 0x46, 0xc8, 0x72, 0xb3, 0xd6, 0x66, 0x36, 0x38, 0x89, 0xe3, 0x51, 0x07, 0xd6, - 0xdb, 0x79, 0xe5, 0x15, 0x0c, 0x02, 0xe0, 0x5f, 0x43, 0xcc, 0xb3, 0x0d, 0xfd, 0xce, 0x74, 0x53, 0xd5, 0xcb, 0x25, - 0x14, 0x46, 0x7f, 0x8c, 0x49, 0x07, 0xe5, 0xa5, 0x6a, 0x2a, 0x0d, 0x92, 0x51, 0x3d, 0x16, 0xa4, 0xa3, 0xcb, 0x73, - 0xc6, 0x67, 0x1d, 0xec, 0x69, 0xd2, 0xcd, 0x00, 0x10, 0x69, 0x87, 0x32, 0x26, 0x2a, 0x84, 0x15, 0x1e, 0x19, 0xa1, - 0xca, 0x1d, 0xf8, 0x28, 0xe0, 0xf3, 0xee, 0xb4, 0x20, 0x98, 0xd5, 0x25, 0x87, 0xb7, 0x42, 0x54, 0x14, 0xb7, 0xb2, - 0x9f, 0x90, 0xcc, 0xc7, 0x66, 0x26, 0xda, 0x6b, 0xc6, 0x9a, 0x77, 0x7f, 0x0e, 0x41, 0xa3, 0x20, 0x74, 0x58, 0x44, - 0xf3, 0x43, 0x0e, 0x83, 0xe4, 0x95, 0xd5, 0xd5, 0xc9, 0xf0, 0x5b, 0x81, 0x64, 0x05, 0x42, 0x74, 0xe2, 0x12, 0x84, - 0xde, 0x7e, 0x32, 0xec, 0x82, 0x57, 0xa0, 0x70, 0x28, 0x1c, 0x2f, 0x81, 0xcd, 0x27, 0x46, 0xc7, 0x72, 0xec, 0x74, - 0xc8, 0x55, 0x85, 0x3a, 0x59, 0x45, 0xd0, 0xda, 0x92, 0x9f, 0xf4, 0x95, 0x82, 0x98, 0x64, 0xcb, 0x96, 0x20, 0xa6, - 0x26, 0xc7, 0xc7, 0x09, 0xbd, 0x3f, 0xb1, 0x48, 0x1a, 0x92, 0x84, 0xef, 0x3e, 0xc1, 0x19, 0x23, 0x18, 0x63, 0x95, - 0x12, 0x63, 0x43, 0x59, 0x66, 0x7f, 0x38, 0x7d, 0x33, 0xc1, 0x81, 0x5f, 0x42, 0x91, 0xf2, 0x2a, 0x39, 0xe1, 0x19, - 0xc3, 0x5c, 0xca, 0xf1, 0xac, 0xe8, 0x1b, 0x75, 0xf8, 0x4b, 0x84, 0x22, 0x90, 0xe0, 0x2e, 0x2f, 0x67, 0xea, 0x8b, - 0xca, 0x4c, 0x69, 0x11, 0x6e, 0xf1, 0x1c, 0x6a, 0x8f, 0x1b, 0xb2, 0x13, 0x6f, 0xba, 0xf7, 0x7a, 0x84, 0x0f, 0x54, - 0x8a, 0x70, 0xde, 0x15, 0x83, 0xe5, 0x6e, 0x2c, 0xcc, 0xa5, 0x2f, 0x26, 0x7f, 0xa0, 0xe7, 0xb3, 0xb4, 0x9c, 0xf8, - 0xaa, 0xf9, 0xe6, 0xa8, 0x8b, 0x3f, 0xe2, 0x69, 0x89, 0x6d, 0xb9, 0xbd, 0x49, 0x2f, 0x3e, 0xdf, 0xe7, 0x7f, 0xd7, - 0x78, 0xb0, 0x50, 0xfd, 0x6c, 0x96, 0xe2, 0x06, 0xab, 0x07, 0x3e, 0x04, 0xb9, 0xc8, 0x4c, 0xe5, 0xda, 0xa6, 0xc7, - 0x9a, 0x3c, 0x6c, 0xc6, 0x3a, 0xb1, 0x5a, 0xf9, 0x36, 0xd8, 0xf4, 0x6a, 0x5b, 0xab, 0x5b, 0x13, 0xfa, 0x43, 0xad, - 0x64, 0x5b, 0xfd, 0x82, 0x07, 0x75, 0x34, 0x5f, 0x76, 0x86, 0xd5, 0xc5, 0xfe, 0x94, 0xc3, 0xa4, 0xf8, 0xbb, 0xb4, - 0xa2, 0x88, 0xfb, 0x4b, 0x06, 0x35, 0x75, 0x7e, 0x8c, 0x5f, 0xac, 0x0e, 0xa1, 0xdf, 0xc7, 0x11, 0xb5, 0x54, 0xfe, - 0x87, 0x13, 0xa5, 0x93, 0x49, 0x1e, 0xed, 0xf9, 0x34, 0x2d, 0x5e, 0xd1, 0xa5, 0xdb, 0xf1, 0x32, 0xf9, 0x56, 0x14, - 0x79, 0xbc, 0x58, 0x65, 0xc0, 0xec, 0x75, 0x68, 0xfa, 0xc9, 0xd3, 0x28, 0x39, 0x16, 0x57, 0x0c, 0x6e, 0x3d, 0x0e, - 0x1e, 0x91, 0x37, 0x35, 0xec, 0x4d, 0x28, 0x58, 0xec, 0xc0, 0x20, 0x3f, 0x06, 0x87, 0xa1, 0x43, 0x3d, 0x22, 0xde, - 0x35, 0xbe, 0x9c, 0xd4, 0x47, 0x58, 0x30, 0xab, 0x89, 0xf0, 0xdb, 0x82, 0xbc, 0x47, 0x22, 0x5a, 0x9f, 0x24, 0x8d, - 0xad, 0x74, 0xe0, 0xed, 0x2b, 0x41, 0x36, 0x39, 0xd0, 0x93, 0xde, 0xc2, 0x6c, 0xe7, 0x7c, 0xb4, 0xf2, 0xf7, 0x22, - 0xf9, 0x29, 0x24, 0xd2, 0x55, 0x15, 0x34, 0x75, 0x64, 0x1f, 0x91, 0x61, 0xed, 0x4b, 0xdd, 0xaf, 0x7d, 0x2d, 0xa4, - 0x24, 0xf8, 0xff, 0x69, 0x14, 0x0b, 0xba, 0x0b, 0x98, 0x77, 0x57, 0xc3, 0x30, 0x91, 0x93, 0xd5, 0xc4, 0x65, 0x89, - 0x6e, 0xea, 0x40, 0x61, 0x0c, 0xfb, 0x6d, 0xb4, 0x38, 0x5c, 0xd8, 0x97, 0x3c, 0x50, 0xa9, 0x5b, 0x8e, 0xe7, 0xb7, - 0x32, 0xa7, 0xf2, 0x62, 0x39, 0xa8, 0x0c, 0x5b, 0x03, 0xf0, 0x0c, 0x61, 0x18, 0x10, 0x0d, 0x39, 0x2e, 0x49, 0xb8, - 0xc2, 0xb0, 0x46, 0xf6, 0x58, 0x34, 0x52, 0x86, 0xd5, 0xc5, 0x8d, 0x2c, 0x4e, 0xa6, 0x89, 0x18, 0xce, 0xa7, 0x69, - 0x71, 0x02, 0xfc, 0xc0, 0x84, 0x1b, 0xec, 0xfa, 0xaa, 0xb0, 0x1c, 0xd0, 0x6c, 0x13, 0x1a, 0x2d, 0x52, 0x93, 0x66, - 0x95, 0x74, 0x52, 0xfe, 0x2f, 0xc7, 0x34, 0xd6, 0x19, 0xe9, 0x9c, 0x30, 0x22, 0xf2, 0x0f, 0x8d, 0x52, 0x76, 0x10, - 0xb6, 0x3a, 0x3c, 0x9c, 0x4d, 0x7e, 0x40, 0xd5, 0x46, 0xf7, 0x83, 0xaf, 0x2e, 0x64, 0xb0, 0x07, 0xc1, 0x91, 0xeb, - 0xe6, 0xa8, 0x49, 0xfe, 0x97, 0xa3, 0xfc, 0x73, 0x2e, 0x4e, 0x3a, 0x92, 0xb4, 0xb1, 0x62, 0x44, 0x64, 0x63, 0xfa, - 0x83, 0x4d, 0x9b, 0xc2, 0xa1, 0x0f, 0x0b, 0x3a, 0x9e, 0xa2, 0x40, 0x1a, 0xbd, 0xaa, 0xe4, 0xa0, 0x30, 0x19, 0x20, - 0x7b, 0x25, 0x65, 0xde, 0x2f, 0xe4, 0x32, 0xc8, 0x7c, 0xab, 0x56, 0x66, 0xcb, 0x67, 0x0c, 0xc4, 0x08, 0x37, 0xc2, - 0xe0, 0x57, 0x8a, 0xa9, 0xaf, 0x59, 0xa6, 0xb8, 0x9f, 0x86, 0x90, 0x1d, 0x12, 0x86, 0x1f, 0x68, 0x53, 0x26, 0xa7, - 0x4d, 0x13, 0x2f, 0xf5, 0xd5, 0xd5, 0x30, 0x79, 0x81, 0x4c, 0xde, 0xe0, 0x3e, 0x72, 0xdd, 0x8f, 0xfa, 0x6d, 0x93, - 0x60, 0x7f, 0x95, 0xe0, 0xff, 0x0e, 0x21, 0x5b, 0x0b, 0x60, 0x06, 0x89, 0x8d, 0xbe, 0x5d, 0xf0, 0x71, 0x51, 0xe4, - 0xeb, 0x44, 0xac, 0xa8, 0x8c, 0x4b, 0xb2, 0x5b, 0x07, 0xbb, 0xd2, 0x36, 0x18, 0x6c, 0x90, 0x68, 0x68, 0x32, 0x0d, - 0x9d, 0x2c, 0xdf, 0x2c, 0x0d, 0xc9, 0xb7, 0x65, 0x9d, 0x3b, 0x14, 0x1a, 0x49, 0x0d, 0x6e, 0xd3, 0xf2, 0x39, 0xf5, - 0x94, 0x6c, 0x78, 0x6c, 0x2b, 0x79, 0x50, 0x61, 0x3a, 0x36, 0xb3, 0x79, 0xb4, 0x62, 0x3d, 0xaf, 0xd6, 0x39, 0x24, - 0x76, 0x5b, 0x56, 0x0e, 0x56, 0x64, 0x88, 0x4f, 0x5c, 0x8f, 0xd6, 0xf6, 0xa3, 0xef, 0x63, 0x44, 0x25, 0x39, 0x18, - 0x96, 0x2d, 0x95, 0xa7, 0x16, 0x27, 0x9e, 0xda, 0xfd, 0x60, 0xfa, 0xb2, 0x48, 0x85, 0x17, 0x4d, 0xe6, 0x8b, 0xd4, - 0xa4, 0x38, 0xcc, 0xc5, 0x48, 0xcc, 0x81, 0xa5, 0x7d, 0x84, 0x8c, 0xc1, 0x52, 0x7a, 0xa4, 0xb1, 0x8d, 0x91, 0xb7, - 0xe8, 0x50, 0x2e, 0x3a, 0xfb, 0x9f, 0x59, 0x7a, 0x0a, 0x43, 0xda, 0x2c, 0x8d, 0x6b, 0xb7, 0x54, 0x5e, 0xd9, 0x0f, - 0xae, 0xb2, 0x6d, 0x1b, 0x8b, 0xa0, 0x7e, 0xb2, 0xde, 0x92, 0x4c, 0x2a, 0x70, 0x30, 0x31, 0xd0, 0x43, 0x65, 0x37, - 0x5a, 0x6b, 0x7a, 0x3c, 0x4a, 0x5b, 0xa4, 0x89, 0xb0, 0x36, 0x32, 0xaa, 0xba, 0x8a, 0xb8, 0xbd, 0xd2, 0x97, 0x7c, - 0x05, 0x18, 0x6f, 0xbc, 0xb9, 0xb9, 0x46, 0xc3, 0x1d, 0x73, 0x12, 0xc5, 0x20, 0xde, 0xad, 0x8c, 0x27, 0x4d, 0xeb, - 0xf2, 0xdb, 0x72, 0xe2, 0x53, 0xbd, 0x6e, 0x31, 0x81, 0x0c, 0xce, 0x82, 0x78, 0x3d, 0x03, 0x58, 0x65, 0x57, 0x11, - 0xd5, 0x66, 0x41, 0x82, 0x60, 0x1a, 0x5c, 0xc7, 0x1a, 0xb7, 0x39, 0xca, 0x36, 0x09, 0x7a, 0xe6, 0x68, 0x2c, 0xff, - 0x9d, 0x59, 0xe0, 0xe5, 0x45, 0x81, 0xf6, 0xc4, 0x81, 0xef, 0xc6, 0x33, 0xf6, 0xfa, 0x9f, 0x8f, 0x8c, 0x01, 0x1c, - 0xd4, 0x98, 0x93, 0xd9, 0xe1, 0x55, 0xda, 0xaa, 0xf4, 0x2a, 0xa9, 0xb2, 0x3d, 0xc4, 0xab, 0xde, 0x8c, 0x24, 0x4a, - 0xa9, 0x0b, 0xb6, 0x6c, 0xe6, 0x41, 0xe2, 0x35, 0x47, 0x7f, 0xac, 0x25, 0x92, 0xbd, 0x82, 0x86, 0x91, 0x33, 0xf1, - 0x8b, 0x4f, 0x89, 0x79, 0xa7, 0x7d, 0xc5, 0xe4, 0x79, 0x83, 0x8f, 0xe0, 0x8b, 0x4a, 0x5d, 0x34, 0xde, 0x38, 0x84, - 0x3a, 0xd6, 0x30, 0x41, 0x0a, 0x30, 0x73, 0x80, 0x8b, 0x62, 0xe5, 0x93, 0x51, 0x52, 0xec, 0x9d, 0x16, 0xdb, 0xbc, - 0x16, 0x8a, 0x57, 0xfe, 0xa2, 0x94, 0xa6, 0xf6, 0xf2, 0xa9, 0x0d, 0xe5, 0xfb, 0x0d, 0x74, 0x1a, 0xeb, 0x8d, 0xe8, - 0xa3, 0x94, 0xf2, 0xec, 0x3a, 0xe1, 0x0c, 0x8f, 0xad, 0x8a, 0x59, 0x3c, 0x58, 0x35, 0xce, 0x3e, 0x84, 0x12, 0x1e, - 0x2d, 0x5b, 0x52, 0x76, 0xf3, 0x14, 0x86, 0xbf, 0xc3, 0x4a, 0xa1, 0xa8, 0x45, 0x80, 0xc0, 0xba, 0x8d, 0x7f, 0xdc, - 0x69, 0x3a, 0x6f, 0xa7, 0xb3, 0xc1, 0xc6, 0xe2, 0x68, 0xd8, 0x1e, 0xa9, 0x32, 0xf0, 0xcb, 0xc7, 0x33, 0x6b, 0x4d, - 0x0a, 0x17, 0x78, 0xa8, 0xc2, 0xf6, 0xaf, 0xa3, 0x3d, 0x69, 0xbe, 0x23, 0xe0, 0x8e, 0x41, 0x3a, 0x01, 0xdf, 0x79, - 0x08, 0x5d, 0x00, 0xed, 0x14, 0x62, 0x17, 0x21, 0x75, 0x09, 0x72, 0x97, 0xa1, 0xed, 0x5a, 0x78, 0x40, 0x4c, 0xf0, - 0xb0, 0x32, 0xc3, 0xa3, 0xca, 0x02, 0x8f, 0x2b, 0x7b, 0x78, 0x42, 0x1c, 0xe0, 0x69, 0x65, 0x85, 0x85, 0x8a, 0xea, - 0xb2, 0xba, 0xaa, 0xae, 0xab, 0xa5, 0x32, 0xf4, 0x2b, 0x96, 0x05, 0xc6, 0xbf, 0x20, 0x46, 0xb0, 0xf8, 0xa0, 0x31, - 0xe5, 0xf6, 0xc1, 0xc3, 0x47, 0x8f, 0x9f, 0x3c, 0x0d, 0x55, 0xa6, 0xf3, 0x75, 0xd6, 0xa4, 0xe6, 0x71, 0x6b, 0xa8, - 0x23, 0xef, 0x7c, 0x39, 0xf4, 0x2d, 0x03, 0xea, 0xec, 0x69, 0x1a, 0x86, 0x2f, 0x5d, 0x7e, 0x3d, 0xf1, 0x4b, 0xc5, - 0xeb, 0xc6, 0x37, 0x66, 0x7c, 0xef, 0x8b, 0x4f, 0x13, 0x5a, 0x3c, 0xbd, 0xe0, 0x82, 0x15, 0x3c, 0x98, 0x8f, 0xf3, - 0x2e, 0x0b, 0x11, 0x75, 0xaa, 0x87, 0xc2, 0x5a, 0xf1, 0xfc, 0x83, 0x0e, 0x63, 0x2f, 0x4e, 0x11, 0xd9, 0x37, 0xe7, - 0x86, 0x88, 0xa6, 0xc9, 0xbb, 0x88, 0xaa, 0x7f, 0xb0, 0x7b, 0x29, 0x1a, 0x5d, 0x52, 0xee, 0xd3, 0xdf, 0x9f, 0xad, - 0xc8, 0xbe, 0xa9, 0xe8, 0x41, 0x71, 0x70, 0x56, 0x2d, 0x4c, 0x66, 0x93, 0xa6, 0x4e, 0x36, 0xc0, 0xf7, 0x4c, 0x0a, - 0xaa, 0x92, 0xa1, 0xf6, 0xcf, 0x1e, 0xf8, 0x4f, 0x5a, 0x77, 0xe1, 0xa7, 0xb2, 0x7e, 0xbb, 0xff, 0x9a, 0xbe, 0x2d, - 0x4e, 0x58, 0x9c, 0x58, 0x94, 0x54, 0xd2, 0x99, 0xc9, 0xa2, 0x9e, 0xd7, 0x57, 0xce, 0xcd, 0x3c, 0xc9, 0x2c, 0x9f, - 0xf6, 0x56, 0xd0, 0xd9, 0x6d, 0x80, 0x98, 0x04, 0xea, 0x35, 0x3e, 0x65, 0xf5, 0x66, 0x3c, 0xfd, 0xc4, 0xb2, 0x29, - 0x85, 0x00, 0xa7, 0xc5, 0x17, 0xff, 0xdb, 0xf1, 0x2d, 0xf9, 0x6e, 0xa6, 0x97, 0xf9, 0xfd, 0x9a, 0x6c, 0xe6, 0x46, - 0x0a, 0xdc, 0xf1, 0xcb, 0xd9, 0xe8, 0xb9, 0xc6, 0x7f, 0x33, 0xc8, 0xdb, 0xe4, 0xe9, 0x52, 0xca, 0xd5, 0xc6, 0x0e, - 0x5d, 0x6e, 0xfe, 0xa9, 0x2a, 0x8f, 0x5d, 0x5b, 0xea, 0xf8, 0xe7, 0x5d, 0x1c, 0x8f, 0x5f, 0xf4, 0x3f, 0xde, 0xe7, - 0x1e, 0xef, 0xa2, 0x66, 0x51, 0xfe, 0x3d, 0x54, 0xa9, 0xe3, 0xf7, 0xa7, 0xf9, 0xd2, 0x51, 0x3e, 0x4e, 0x4d, 0xf3, - 0x41, 0x5e, 0x46, 0x2c, 0x8f, 0xd6, 0xf4, 0xf6, 0xcf, 0x9f, 0x12, 0xff, 0x6b, 0x73, 0x9d, 0xed, 0xe1, 0xfe, 0xa4, - 0x5e, 0x6c, 0x63, 0x31, 0x22, 0x66, 0x63, 0xc1, 0x0e, 0xa8, 0xde, 0x3f, 0x3d, 0x87, 0x94, 0x65, 0xfc, 0xa7, 0xc7, - 0x0b, 0x2c, 0x9d, 0xc0, 0x9a, 0xbf, 0xed, 0x98, 0x96, 0xbc, 0xcb, 0x96, 0x63, 0x0c, 0x7d, 0xc6, 0xff, 0xe6, 0x2e, - 0xee, 0xb0, 0x5a, 0x12, 0xa5, 0x18, 0x11, 0x16, 0xfa, 0xf9, 0x8d, 0x0f, 0x42, 0xf4, 0x61, 0xe5, 0xc1, 0x54, 0xaa, - 0x4d, 0xa6, 0x9c, 0x38, 0x81, 0x0f, 0xbe, 0x1d, 0x78, 0x5a, 0x81, 0x37, 0x7f, 0xdb, 0x37, 0x2d, 0xa3, 0x63, 0xf7, - 0x92, 0xbe, 0xbc, 0x9c, 0xb7, 0x4c, 0x4b, 0xbc, 0xd4, 0xc5, 0xed, 0x49, 0x13, 0x7a, 0x67, 0x41, 0x35, 0xc9, 0xc9, - 0xa7, 0xe5, 0x13, 0x0c, 0xfb, 0x45, 0x49, 0xac, 0x9a, 0x56, 0x43, 0x63, 0xd4, 0x9b, 0x26, 0x53, 0x37, 0xb8, 0xd6, - 0xa8, 0x57, 0x81, 0x7f, 0x66, 0x40, 0xee, 0x39, 0x13, 0x7b, 0xce, 0xa0, 0x97, 0x6f, 0x7e, 0x4c, 0x9a, 0xb7, 0xa6, - 0xde, 0x16, 0x5f, 0x2b, 0xa6, 0x52, 0xa2, 0xf0, 0x7d, 0x5d, 0xb8, 0x10, 0x1f, 0x0b, 0x97, 0xab, 0x07, 0xa6, 0xe9, - 0xd3, 0x29, 0xe4, 0xfb, 0x56, 0xe0, 0x9c, 0x91, 0xa3, 0xa0, 0x0d, 0x86, 0x3c, 0x62, 0xde, 0x0f, 0x21, 0xe5, 0x73, - 0x1f, 0xc6, 0xe8, 0xf7, 0xd8, 0x08, 0x28, 0x96, 0xa3, 0xec, 0x2e, 0xc4, 0x3c, 0xcf, 0x62, 0x76, 0xd0, 0xd5, 0x72, - 0xb3, 0xee, 0x98, 0x63, 0x26, 0x56, 0xfc, 0x8c, 0xeb, 0x58, 0xc1, 0x8c, 0x7c, 0xd0, 0xa2, 0x3b, 0xfa, 0x23, 0x2b, - 0xfb, 0x3a, 0x50, 0x77, 0x12, 0x5a, 0x3b, 0x4c, 0xdf, 0x66, 0x19, 0x0e, 0x28, 0x96, 0x08, 0x12, 0x85, 0xe6, 0xf7, - 0x2a, 0x31, 0xb1, 0x52, 0x9c, 0x1a, 0xcd, 0xf1, 0x86, 0xaa, 0xc4, 0x98, 0xa0, 0xe5, 0xed, 0xea, 0x8b, 0xcc, 0x74, - 0x09, 0x1b, 0x37, 0x58, 0xd2, 0x8a, 0xfd, 0xa2, 0xa4, 0x7e, 0xdf, 0x96, 0x85, 0x8a, 0x77, 0xd9, 0xd5, 0x51, 0x5d, - 0xda, 0xa1, 0xa3, 0x22, 0x66, 0xa0, 0xe8, 0xbb, 0x9d, 0x57, 0xce, 0x46, 0xf6, 0x81, 0xdb, 0x09, 0x9c, 0x05, 0x55, - 0x79, 0x9d, 0xea, 0x50, 0x0a, 0x97, 0x05, 0xe0, 0x16, 0x38, 0x3d, 0x3d, 0x09, 0xca, 0xb3, 0xae, 0xac, 0x44, 0x57, - 0x7a, 0x37, 0xe4, 0x3f, 0xd2, 0xe8, 0xc7, 0x39, 0xe2, 0xd9, 0xe7, 0xdd, 0xe9, 0x46, 0xa7, 0xf9, 0x5d, 0xfe, 0x26, - 0xe6, 0xef, 0x2a, 0xfa, 0xf2, 0x97, 0x2d, 0x6f, 0x5f, 0x07, 0xc2, 0x60, 0x69, 0x86, 0xf8, 0xda, 0x52, 0x79, 0x8d, - 0xb9, 0x0f, 0x31, 0x4d, 0xc2, 0xc6, 0xc8, 0xa3, 0x29, 0xe0, 0x3c, 0xdf, 0xbb, 0x51, 0x7a, 0x6d, 0x4b, 0x82, 0x50, - 0xe2, 0x3d, 0xdf, 0x4a, 0xbf, 0xe7, 0x71, 0x11, 0x0b, 0x32, 0xdb, 0xd0, 0xe9, 0xf9, 0xa8, 0x8e, 0x21, 0xe6, 0xc6, - 0xd3, 0x2e, 0xec, 0xa9, 0x5a, 0xab, 0x05, 0x49, 0xd2, 0xeb, 0x3c, 0xdf, 0xfd, 0xce, 0xa4, 0x8c, 0xe0, 0x51, 0x13, - 0x24, 0x7e, 0xee, 0xeb, 0x3f, 0xef, 0x4c, 0x0d, 0x7a, 0x0b, 0x78, 0x5a, 0x3a, 0xe8, 0xc9, 0x27, 0x8e, 0x5f, 0x0b, - 0xcb, 0x6d, 0xa3, 0x4b, 0xff, 0x7d, 0x9e, 0xad, 0x4b, 0x26, 0x4c, 0xba, 0x45, 0x32, 0x1f, 0x2b, 0x13, 0xe8, 0xa9, - 0x69, 0x9d, 0x90, 0xaa, 0xfe, 0x89, 0x15, 0xd4, 0x28, 0x70, 0xa0, 0x94, 0xba, 0x9a, 0xb0, 0x10, 0xdc, 0x09, 0x8f, - 0xcf, 0x4b, 0x01, 0xd1, 0xdc, 0xbd, 0x08, 0x92, 0x8b, 0xc4, 0x6b, 0x41, 0x25, 0x56, 0x65, 0xc5, 0x65, 0x55, 0x52, - 0x09, 0xa6, 0xf0, 0x03, 0x00, 0xcd, 0x7b, 0x05, 0x4c, 0x72, 0xa0, 0x47, 0x54, 0x3a, 0xf9, 0xa8, 0x11, 0x1f, 0xf0, - 0x64, 0x93, 0xfe, 0xa1, 0xa7, 0xb8, 0x24, 0x4e, 0x9c, 0xe9, 0x50, 0x82, 0xfb, 0x63, 0xd2, 0xfa, 0x53, 0xc5, 0x7c, - 0x8b, 0x89, 0x0e, 0xea, 0xf2, 0xf6, 0xfa, 0x3a, 0xab, 0x89, 0x84, 0x1a, 0x70, 0xc3, 0x9a, 0xd6, 0x94, 0xba, 0x6e, - 0x03, 0x4d, 0x1f, 0x40, 0xdf, 0xb3, 0xea, 0xf6, 0x47, 0x85, 0xe5, 0x40, 0x6e, 0x72, 0x5b, 0x8d, 0xa2, 0x8d, 0xff, - 0x38, 0xb8, 0xa9, 0xc6, 0x29, 0x70, 0x5d, 0xcd, 0x64, 0x99, 0x80, 0xab, 0x6a, 0x8a, 0x00, 0x2e, 0xab, 0xf9, 0x09, - 0xf5, 0xbd, 0xfa, 0x82, 0x8c, 0x5f, 0xea, 0xf0, 0xf5, 0x7e, 0xae, 0x20, 0xf4, 0xce, 0x4f, 0x59, 0x5b, 0x39, 0xf3, - 0x9c, 0xf7, 0xeb, 0x94, 0x83, 0xf7, 0x1b, 0xba, 0xee, 0xf0, 0xa9, 0xce, 0xbc, 0x0d, 0xd0, 0xfe, 0x29, 0x6f, 0xde, - 0xe9, 0x29, 0x62, 0xef, 0xe4, 0x6a, 0xee, 0xee, 0xff, 0x69, 0xe8, 0x79, 0x6f, 0x78, 0xaa, 0xb4, 0x95, 0xe3, 0x4a, - 0x8f, 0xde, 0xd1, 0xbf, 0x96, 0xda, 0x02, 0x87, 0xd5, 0x54, 0x0a, 0x1c, 0x54, 0x93, 0x2b, 0xb0, 0x5f, 0xcd, 0xab, - 0x5a, 0x3b, 0x00, 0x9b, 0xd5, 0x20, 0x03, 0x7b, 0xd5, 0x64, 0x0d, 0xd8, 0xaa, 0x46, 0xbb, 0x4f, 0x1e, 0xd8, 0xae, - 0x06, 0x6b, 0x60, 0x67, 0x64, 0x5e, 0xe7, 0xed, 0xfe, 0x33, 0xf1, 0xdc, 0xc0, 0x58, 0xaf, 0xdb, 0xcb, 0x3e, 0x33, - 0xef, 0x75, 0x0d, 0x4c, 0x97, 0x65, 0x5b, 0xac, 0x5a, 0x80, 0x0d, 0xfa, 0xef, 0x48, 0xd3, 0xd6, 0x0a, 0x7e, 0x23, - 0x90, 0xc0, 0xfc, 0xec, 0x2c, 0x60, 0x01, 0xa3, 0xff, 0xd0, 0xe3, 0xd8, 0xc0, 0x50, 0x4b, 0xac, 0x4f, 0xd6, 0x31, - 0x6d, 0xd3, 0xff, 0xc7, 0xc6, 0x66, 0x1b, 0x25, 0x48, 0xb7, 0xed, 0x35, 0xbe, 0xbc, 0x65, 0x47, 0x68, 0x3f, 0x52, - 0xd4, 0xa5, 0xb4, 0x0a, 0xf0, 0x57, 0x4b, 0x72, 0xf1, 0xe8, 0xb2, 0x3d, 0x13, 0xbe, 0x57, 0x67, 0xb1, 0xce, 0x94, - 0x90, 0x88, 0x1d, 0xe2, 0xea, 0xfe, 0xbf, 0x3c, 0xf5, 0x95, 0x42, 0x65, 0x11, 0x27, 0xc5, 0x47, 0x7c, 0x4c, 0xb6, - 0x0a, 0x4e, 0x11, 0xc7, 0x42, 0x8c, 0x43, 0x37, 0x4f, 0xce, 0x61, 0xd5, 0x24, 0x0a, 0xfb, 0x47, 0xc2, 0x6b, 0xd0, - 0xf0, 0x9b, 0x6c, 0x39, 0xc0, 0xce, 0x19, 0x86, 0xd0, 0x8c, 0xbe, 0xb3, 0x9c, 0x49, 0x95, 0x93, 0x57, 0x30, 0x8e, - 0xe8, 0x58, 0xfd, 0x8e, 0x94, 0x97, 0xf7, 0xa5, 0xca, 0x97, 0x43, 0x50, 0xb6, 0x1f, 0xe0, 0x1d, 0x14, 0xc5, 0xf4, - 0xb9, 0x0c, 0x16, 0xf8, 0x5e, 0x77, 0x0f, 0x0f, 0xf0, 0xc2, 0x26, 0xfc, 0x58, 0x2b, 0x4f, 0x78, 0x67, 0xab, 0x96, - 0xd1, 0x63, 0xe1, 0x60, 0x06, 0xeb, 0x20, 0xfe, 0xaf, 0x79, 0xdd, 0x32, 0x80, 0x12, 0x38, 0xe1, 0xe6, 0x4c, 0x7b, - 0x7d, 0x9d, 0x3e, 0xb0, 0x32, 0x69, 0x8a, 0x5d, 0xf4, 0x07, 0xe6, 0x2a, 0xca, 0x6b, 0xa8, 0xe2, 0x34, 0xbb, 0x91, - 0xba, 0xc0, 0xe3, 0x7b, 0xa5, 0x3b, 0x8f, 0x3b, 0x3c, 0x74, 0xed, 0x0a, 0x8a, 0x5c, 0x23, 0x06, 0xda, 0x5d, 0x00, - 0xdd, 0x2e, 0xc3, 0xc6, 0xbf, 0xea, 0x27, 0xa9, 0x08, 0x44, 0x34, 0xe3, 0x5e, 0xf8, 0x17, 0xb0, 0xad, 0x1b, 0xdd, - 0xa2, 0x94, 0x8e, 0x2e, 0x72, 0xeb, 0xad, 0xc4, 0x9d, 0xa9, 0x64, 0xa3, 0x41, 0x0d, 0x58, 0xce, 0xbd, 0xff, 0x3b, - 0xd5, 0x5c, 0x52, 0x7f, 0xc8, 0xc4, 0x41, 0xdf, 0x9a, 0xad, 0x90, 0x05, 0xff, 0x32, 0xc9, 0x7f, 0xc6, 0x3d, 0x3e, - 0xf1, 0xd5, 0xf3, 0x90, 0x36, 0xdd, 0xb4, 0x02, 0x76, 0xd7, 0x14, 0x59, 0x9f, 0xf2, 0x71, 0x11, 0x46, 0xce, 0x82, - 0x47, 0xf8, 0x03, 0x3a, 0x09, 0x71, 0xd9, 0xfb, 0xda, 0x8b, 0xbd, 0xe8, 0xb9, 0x4c, 0xfa, 0xc7, 0xac, 0x5e, 0xa4, - 0xe3, 0x3c, 0xd4, 0x66, 0xef, 0xa8, 0x5f, 0xa3, 0x0c, 0xd5, 0x9b, 0xb3, 0x9e, 0xc3, 0x36, 0x30, 0xea, 0x4e, 0x89, - 0x56, 0x11, 0xa5, 0x1b, 0x31, 0x68, 0x28, 0x39, 0x19, 0x5f, 0xe9, 0x6c, 0xbf, 0xbb, 0xbc, 0xe3, 0x56, 0x53, 0x0a, - 0x14, 0xfb, 0x46, 0xb9, 0xc6, 0x87, 0x9f, 0x25, 0x2b, 0x22, 0xcb, 0x7a, 0xc1, 0x78, 0xf6, 0x1a, 0xd6, 0x82, 0x69, - 0x8a, 0xb6, 0x50, 0x7b, 0x71, 0x71, 0xd1, 0x84, 0x6b, 0x9d, 0xdc, 0xb8, 0x3b, 0x1f, 0x16, 0x76, 0x41, 0x09, 0xb7, - 0xbd, 0xc2, 0xd3, 0xc1, 0xdc, 0x67, 0x46, 0x28, 0x89, 0x06, 0xe0, 0x04, 0x5c, 0x3a, 0x86, 0xeb, 0xc6, 0xf1, 0x5e, - 0x03, 0x78, 0x58, 0x2f, 0xe0, 0x0c, 0x50, 0xa9, 0x78, 0x45, 0x77, 0xab, 0x1e, 0xbf, 0x3b, 0x4b, 0xf3, 0xeb, 0x44, - 0x19, 0x14, 0x96, 0x83, 0x87, 0x96, 0x32, 0xcf, 0x34, 0xa8, 0x51, 0x83, 0x8d, 0x54, 0xac, 0x90, 0x17, 0xaf, 0x79, - 0xd0, 0xba, 0xea, 0x85, 0x3d, 0x41, 0x4a, 0x3f, 0x2f, 0x73, 0x0b, 0xaa, 0x81, 0x81, 0x84, 0x58, 0x36, 0x99, 0xd5, - 0x1f, 0x1d, 0x3c, 0x77, 0xaf, 0xa6, 0x9a, 0x6c, 0xb2, 0x05, 0x99, 0xf7, 0xeb, 0xef, 0xad, 0x24, 0x5b, 0x7e, 0x71, - 0x27, 0xdb, 0x5f, 0xef, 0x97, 0x42, 0xc9, 0xe8, 0xcd, 0x38, 0x3b, 0xd6, 0x83, 0x72, 0xd6, 0x81, 0x40, 0x44, 0x7e, - 0x5b, 0x1d, 0x0a, 0xc4, 0x62, 0x32, 0x82, 0x32, 0x85, 0x19, 0x61, 0xdf, 0xd0, 0x5d, 0x1e, 0xc6, 0xdb, 0xb7, 0x13, - 0xd8, 0x4c, 0x2c, 0x28, 0xc0, 0x58, 0xdc, 0x4e, 0x4e, 0x27, 0xd7, 0x50, 0x09, 0xe6, 0x31, 0xc7, 0x50, 0x09, 0xc9, - 0xbd, 0x72, 0x66, 0xfd, 0x58, 0x64, 0x15, 0x35, 0x06, 0xc9, 0x95, 0x55, 0xc0, 0xb2, 0xb7, 0x90, 0x90, 0x71, 0xc8, - 0x28, 0x72, 0x02, 0xfb, 0x45, 0x6a, 0x46, 0x41, 0x49, 0xfc, 0x6f, 0xe6, 0x9e, 0xb9, 0x99, 0x7b, 0xe5, 0xc3, 0xb2, - 0x34, 0x29, 0x49, 0x56, 0x69, 0x13, 0xf4, 0x13, 0x3a, 0x7e, 0x89, 0xd8, 0xe9, 0xb4, 0x21, 0x34, 0xb0, 0xc7, 0x38, - 0x32, 0x82, 0xa9, 0x30, 0xa1, 0x7a, 0x57, 0x6f, 0x40, 0x12, 0x78, 0x80, 0xd1, 0xce, 0x44, 0x4e, 0x68, 0xe0, 0xc3, - 0xd9, 0x68, 0xe3, 0xe6, 0xe1, 0x77, 0x4e, 0x54, 0xaa, 0x85, 0x9d, 0xe5, 0xdf, 0x5b, 0x70, 0xa5, 0xfd, 0x4d, 0x40, - 0x95, 0xc0, 0xc5, 0xe4, 0xdf, 0xbf, 0xfb, 0x71, 0xe8, 0xdf, 0x2a, 0xe7, 0x8f, 0x16, 0x8b, 0x6f, 0x23, 0x3b, 0x5c, - 0x82, 0xb5, 0xae, 0x23, 0xe7, 0x22, 0x3f, 0x33, 0x1b, 0xaa, 0xe1, 0xf7, 0x43, 0xdd, 0xee, 0xe4, 0x42, 0xa9, 0x5a, - 0x7c, 0x32, 0x72, 0xdc, 0x51, 0xc9, 0x13, 0x89, 0x86, 0x58, 0xd1, 0xf2, 0x2b, 0x26, 0x9e, 0xd1, 0xf8, 0x52, 0xf1, - 0x48, 0x16, 0xee, 0x1f, 0xb9, 0xf6, 0x92, 0xa9, 0x83, 0x6c, 0xc0, 0x64, 0x64, 0xae, 0xfc, 0x76, 0xdc, 0x68, 0x1f, - 0xe6, 0x0f, 0x7f, 0xaa, 0x4f, 0xc4, 0xde, 0xff, 0x9a, 0x51, 0x76, 0xbd, 0x2c, 0x24, 0x3f, 0x61, 0xe1, 0x09, 0x7f, - 0x1c, 0xa1, 0xe0, 0xe5, 0xf4, 0xf1, 0x82, 0x38, 0x8e, 0x9e, 0x2b, 0x76, 0x20, 0x4b, 0x25, 0x2a, 0xee, 0x22, 0x48, - 0x42, 0x14, 0xe1, 0x09, 0xa0, 0xe4, 0x7d, 0x5c, 0x89, 0x4a, 0xb3, 0x98, 0x98, 0x4f, 0x46, 0x2a, 0x00, 0x67, 0x07, - 0xd1, 0xd0, 0x4a, 0xa4, 0x27, 0xea, 0x24, 0xa2, 0x21, 0x47, 0x67, 0x83, 0xfe, 0x9d, 0x78, 0x16, 0x1b, 0xa2, 0x22, - 0x40, 0x4c, 0x41, 0xd4, 0xb1, 0x15, 0x6f, 0x94, 0x81, 0x2b, 0xea, 0xa1, 0x44, 0x82, 0xd6, 0xd4, 0x19, 0xa0, 0x29, - 0x21, 0x20, 0xc7, 0x5c, 0xee, 0xa1, 0x87, 0x19, 0xa6, 0x6d, 0xb7, 0xab, 0xa8, 0x30, 0xde, 0x1f, 0xce, 0xcb, 0xa8, - 0xd4, 0x76, 0x0a, 0x44, 0xa9, 0x7a, 0x1a, 0xc6, 0x38, 0x1d, 0x2b, 0x84, 0x77, 0x01, 0xc5, 0x25, 0xc9, 0x4a, 0x8c, - 0xfa, 0x6e, 0x34, 0x6d, 0xaa, 0x11, 0x12, 0x38, 0xe9, 0xdd, 0xf4, 0x3a, 0x40, 0x49, 0x0c, 0x26, 0x58, 0x1c, 0xc1, - 0x66, 0xf0, 0xc9, 0xb1, 0x46, 0x90, 0x94, 0x50, 0xa0, 0x54, 0x99, 0xf1, 0x47, 0xad, 0xa4, 0x20, 0xf1, 0x3e, 0xfc, - 0xfc, 0x53, 0x65, 0xf1, 0xc4, 0x0a, 0x1b, 0xef, 0x63, 0x7e, 0x5f, 0xc9, 0xab, 0x1e, 0x84, 0x89, 0x07, 0xc4, 0xb7, - 0x09, 0x1c, 0x61, 0x99, 0xca, 0x65, 0x83, 0x0c, 0x05, 0x28, 0x38, 0xd5, 0x9a, 0xb3, 0x38, 0x13, 0x9b, 0x46, 0xaa, - 0x30, 0xe8, 0x11, 0x4c, 0x91, 0xfc, 0x8b, 0xb8, 0x7f, 0xdf, 0x06, 0xc4, 0xf8, 0x14, 0x1f, 0xfa, 0xc9, 0xeb, 0x9d, - 0x85, 0x2a, 0x98, 0x10, 0xf5, 0xe2, 0x3f, 0x8f, 0x97, 0xc5, 0xae, 0x1f, 0x1f, 0x36, 0x15, 0x48, 0x20, 0xf6, 0x3c, - 0x0d, 0xce, 0x7f, 0xc6, 0x2c, 0x09, 0x03, 0x69, 0x71, 0x6f, 0x4a, 0xe0, 0x4d, 0x2f, 0x58, 0x9a, 0x4d, 0x80, 0x09, - 0x52, 0x9c, 0x8c, 0xb6, 0x16, 0xac, 0x53, 0x4f, 0x8d, 0xd2, 0x99, 0x13, 0x60, 0x52, 0xa9, 0x87, 0x90, 0x9e, 0x7a, - 0x81, 0xde, 0xb1, 0xa2, 0x1f, 0xe3, 0xad, 0x86, 0xb7, 0xec, 0x6a, 0x91, 0x0a, 0x8b, 0xd0, 0x19, 0x08, 0x2e, 0x0b, - 0x4e, 0xf1, 0xfb, 0x08, 0x75, 0x0d, 0xc3, 0x9a, 0xb1, 0xc9, 0x89, 0x1a, 0x2a, 0xc8, 0xb4, 0xee, 0xe9, 0x60, 0xef, - 0x65, 0xde, 0x24, 0xcc, 0x9c, 0x0f, 0x47, 0x94, 0x6c, 0x78, 0xe3, 0x1e, 0x4f, 0x3f, 0x20, 0xdd, 0x19, 0xa4, 0x4f, - 0x30, 0x3d, 0xa6, 0x50, 0x12, 0x41, 0x73, 0x5f, 0x5e, 0x92, 0xab, 0x47, 0x87, 0xbd, 0x52, 0x37, 0xdd, 0x2f, 0xf7, - 0x72, 0xd2, 0xb8, 0x85, 0xe9, 0x2a, 0xec, 0x76, 0xf7, 0xa3, 0x7e, 0x10, 0x38, 0xea, 0xa5, 0x9a, 0x66, 0xa0, 0x66, - 0x92, 0x62, 0x67, 0xf2, 0x56, 0xca, 0x37, 0x8e, 0xa5, 0x17, 0xd4, 0x79, 0x5a, 0x33, 0x6f, 0x72, 0x9f, 0x15, 0x8a, - 0xb2, 0xc2, 0xda, 0xa1, 0x8c, 0x61, 0xa8, 0x92, 0xe1, 0x71, 0x1a, 0xc1, 0xba, 0x28, 0x8a, 0xb6, 0xe8, 0x3a, 0x73, - 0x34, 0xa7, 0x5d, 0xa5, 0x4b, 0xb2, 0xc4, 0xf7, 0xe8, 0x47, 0x13, 0x95, 0xfd, 0x25, 0x7a, 0x91, 0x5a, 0x5a, 0x99, - 0xb6, 0x77, 0xd7, 0xd4, 0xf1, 0x3b, 0x1b, 0x85, 0x3f, 0xba, 0xc1, 0x67, 0xa6, 0xf3, 0xd1, 0x06, 0x37, 0x96, 0xee, - 0x08, 0x67, 0xb0, 0x6d, 0x63, 0x3f, 0xef, 0x03, 0x46, 0x85, 0x0f, 0xa8, 0xbe, 0x9e, 0xe6, 0x1d, 0xae, 0x1d, 0x71, - 0x53, 0x5b, 0xcf, 0xff, 0x66, 0x3a, 0x07, 0xe5, 0x07, 0x7c, 0xc0, 0xa6, 0xcf, 0xf6, 0x16, 0x2c, 0x95, 0xaf, 0x3b, - 0x01, 0x63, 0x49, 0xd5, 0x55, 0x98, 0xad, 0xd9, 0xd2, 0x60, 0x80, 0xe2, 0x22, 0x3f, 0x73, 0x2a, 0x88, 0x70, 0x89, - 0x4c, 0x70, 0x03, 0x45, 0x1f, 0x50, 0x42, 0x99, 0xb1, 0x08, 0x0f, 0xc6, 0x57, 0x98, 0x48, 0x8c, 0xf4, 0x27, 0x14, - 0xc6, 0xe3, 0x8e, 0x7f, 0x7e, 0xce, 0xcf, 0xbb, 0x66, 0xa7, 0xbd, 0xc2, 0xd8, 0xc4, 0xee, 0x59, 0xe8, 0xf8, 0x83, - 0x9c, 0xfd, 0xce, 0xfc, 0x45, 0x30, 0xad, 0xf3, 0x17, 0xc2, 0x7e, 0xe6, 0x44, 0x82, 0xbf, 0xf6, 0xd4, 0xf6, 0x06, - 0x62, 0x39, 0x11, 0xa5, 0x94, 0xfa, 0xc6, 0xbc, 0x55, 0x83, 0x05, 0x50, 0xc2, 0xec, 0xa7, 0x51, 0x7f, 0x6d, 0xe3, - 0x4f, 0x1d, 0x5a, 0x17, 0x4c, 0xbd, 0x16, 0x30, 0x64, 0x86, 0x11, 0x2d, 0x58, 0x13, 0x69, 0x64, 0xe6, 0x8f, 0x33, - 0xb9, 0x90, 0xf3, 0x3e, 0xf3, 0xe7, 0x19, 0x9f, 0x73, 0xea, 0xb7, 0xd5, 0xe6, 0x4a, 0xce, 0xd0, 0x5f, 0xc5, 0xb3, - 0xb7, 0x97, 0x18, 0xdd, 0xf5, 0x0e, 0x62, 0x24, 0xf6, 0x95, 0x3e, 0x9c, 0xfe, 0x73, 0x77, 0x8e, 0x5c, 0x06, 0x3c, - 0x9e, 0x8b, 0x15, 0x67, 0x7e, 0x80, 0xf2, 0x34, 0xb7, 0x36, 0x4e, 0x10, 0xa9, 0xc9, 0xd2, 0xb8, 0xf2, 0x7c, 0x2f, - 0xe3, 0x60, 0x41, 0x76, 0x6c, 0x87, 0x3d, 0xd3, 0xf7, 0xb4, 0xd7, 0xd7, 0x1e, 0xc8, 0x88, 0x3f, 0x97, 0xbe, 0x9f, - 0x40, 0x94, 0xab, 0xad, 0xa7, 0x2a, 0xaa, 0xe6, 0x5e, 0x1e, 0xac, 0xf3, 0x2b, 0x2f, 0xfd, 0xc9, 0xd3, 0x4e, 0xf7, - 0xe5, 0x6e, 0xdd, 0x80, 0x4c, 0x71, 0xa2, 0xf6, 0xc9, 0xc7, 0x0c, 0x11, 0xc5, 0xc5, 0x6f, 0x5c, 0x4f, 0xef, 0x30, - 0xe1, 0x7b, 0x5f, 0x20, 0xcb, 0x47, 0x2a, 0x71, 0xc1, 0x7a, 0x4e, 0x53, 0x63, 0xf9, 0x2e, 0xbb, 0xfd, 0xa2, 0x6d, - 0x83, 0x31, 0xe9, 0xd1, 0x8c, 0xcf, 0xfa, 0x6f, 0xac, 0x03, 0x00, 0x16, 0x7f, 0x97, 0xf8, 0x4a, 0xac, 0xe6, 0xb9, - 0xc9, 0x12, 0x5b, 0x71, 0xd5, 0x78, 0x5e, 0x27, 0x72, 0x90, 0xdd, 0x64, 0x87, 0x08, 0x4d, 0x11, 0x31, 0xed, 0xa5, - 0x7a, 0xbd, 0x41, 0xdf, 0x41, 0x84, 0x85, 0x8a, 0xea, 0x4c, 0x3f, 0x69, 0x0a, 0x33, 0x46, 0xa7, 0x23, 0x40, 0xe5, - 0x80, 0xa4, 0x2f, 0x0f, 0xbe, 0x7d, 0x25, 0x64, 0x05, 0xee, 0x72, 0xe7, 0xb2, 0x90, 0x02, 0xfb, 0xf0, 0xa1, 0xe9, - 0x6f, 0xcf, 0xf3, 0x3c, 0x57, 0x59, 0x3c, 0x8f, 0x7f, 0x92, 0x1c, 0x26, 0x26, 0xda, 0x1a, 0x45, 0x3c, 0xd1, 0x02, - 0x6f, 0x7f, 0xd1, 0x14, 0x09, 0x57, 0x95, 0xc2, 0x8a, 0x02, 0xbd, 0x6a, 0x74, 0x49, 0x50, 0xbc, 0x2b, 0x93, 0x40, - 0x67, 0x70, 0x5d, 0x32, 0x58, 0xb0, 0x3b, 0x15, 0xd2, 0x73, 0x6a, 0x2e, 0xd8, 0xce, 0x04, 0x78, 0x7d, 0x48, 0xe5, - 0xc6, 0x2c, 0x55, 0x48, 0x59, 0x54, 0xe7, 0x05, 0x78, 0xdb, 0xe3, 0xa0, 0xd5, 0x89, 0xc2, 0xde, 0x6b, 0x01, 0x68, - 0xc0, 0xf2, 0xb9, 0xcd, 0x03, 0x5b, 0x72, 0x80, 0xa6, 0x41, 0xd3, 0x31, 0x80, 0xec, 0xef, 0x7c, 0x95, 0xf4, 0xbf, - 0xbd, 0xe3, 0x4f, 0xeb, 0x16, 0x0c, 0x21, 0x63, 0x36, 0x4f, 0x9f, 0xb5, 0x68, 0x08, 0x14, 0x6f, 0xa3, 0x48, 0xc4, - 0x9f, 0x59, 0xab, 0x2a, 0x0d, 0x0c, 0xb9, 0x0e, 0x83, 0x5a, 0xa5, 0x9d, 0xcc, 0x99, 0x96, 0x59, 0xa9, 0x53, 0xb5, - 0xb0, 0x7b, 0x80, 0x0a, 0x3c, 0xa8, 0xde, 0x4b, 0x0d, 0x2a, 0x07, 0x18, 0x8a, 0xe2, 0xa2, 0x0c, 0x9d, 0x8a, 0x7b, - 0xfa, 0x3e, 0x4f, 0xb1, 0x09, 0xff, 0xf9, 0xb2, 0xf2, 0x8d, 0xe7, 0x20, 0x5e, 0x4a, 0xff, 0xb9, 0x53, 0x7e, 0x8c, - 0xbc, 0x86, 0xfa, 0xea, 0x2a, 0x0a, 0x73, 0x3c, 0x62, 0xbc, 0xb3, 0x31, 0x34, 0x02, 0xb1, 0x94, 0xe2, 0x09, 0xe1, - 0xc5, 0x36, 0x0a, 0x3e, 0x87, 0x0a, 0xb4, 0x19, 0x01, 0x58, 0xb8, 0xbb, 0x16, 0xfc, 0xcb, 0xd7, 0x90, 0xbf, 0x57, - 0x3c, 0xa2, 0xa9, 0x4c, 0x6e, 0x57, 0xa7, 0xec, 0x6b, 0xb8, 0xc2, 0xe8, 0xed, 0xfb, 0xbe, 0x5e, 0xe6, 0xeb, 0x4e, - 0xff, 0xe1, 0x13, 0x3e, 0x76, 0xa7, 0xfd, 0x65, 0xe7, 0x61, 0x9d, 0x91, 0x7f, 0x3c, 0x36, 0x79, 0x1b, 0xd6, 0xb4, - 0x1b, 0xec, 0x65, 0x8f, 0x89, 0xc3, 0x4c, 0x3c, 0x14, 0xe3, 0xdf, 0x16, 0x15, 0x38, 0xe6, 0x85, 0x06, 0x52, 0x6b, - 0x7f, 0x4e, 0x1f, 0xff, 0xaa, 0xb0, 0x43, 0xcc, 0x0e, 0xac, 0x04, 0x81, 0xee, 0x7b, 0x05, 0x6e, 0x29, 0x5d, 0x0a, - 0xb4, 0xf5, 0x68, 0x51, 0x2e, 0xae, 0xef, 0x01, 0x21, 0xb5, 0xb6, 0x59, 0xd5, 0x65, 0xa2, 0x71, 0x29, 0x8e, 0x57, - 0xa7, 0x3e, 0xfa, 0x03, 0x2d, 0xf6, 0xd9, 0xc5, 0x16, 0x9a, 0x5a, 0x88, 0x33, 0x71, 0x36, 0x2e, 0xb8, 0x19, 0x37, - 0xeb, 0xce, 0x81, 0x0a, 0x7d, 0x35, 0x18, 0xca, 0x92, 0xd0, 0xda, 0xc0, 0x10, 0x4c, 0x2b, 0x37, 0x58, 0x14, 0x25, - 0xdd, 0x51, 0x2f, 0x8e, 0xb6, 0x0d, 0x2e, 0xdc, 0x46, 0x8c, 0x72, 0xbc, 0x65, 0x7f, 0x81, 0x2a, 0x61, 0xfe, 0x92, - 0x59, 0xe4, 0x5d, 0x93, 0xce, 0x9f, 0x23, 0x3b, 0x10, 0xb3, 0xd4, 0x96, 0xb5, 0x5a, 0x85, 0x9b, 0x09, 0xf9, 0xa6, - 0x7b, 0x8a, 0x42, 0x81, 0x6d, 0x53, 0xb9, 0x8b, 0xff, 0x3d, 0xe2, 0x6a, 0xe7, 0x1c, 0xf9, 0x37, 0x9b, 0xe6, 0xb9, - 0x9e, 0x89, 0x03, 0xa2, 0x9c, 0xae, 0x82, 0x99, 0xc3, 0x70, 0xc7, 0x3d, 0xf5, 0xf3, 0x22, 0x3d, 0xcf, 0xa8, 0x63, - 0x25, 0xb7, 0xef, 0xd4, 0x2f, 0xfc, 0x52, 0x3c, 0xf6, 0x0b, 0x7d, 0x61, 0xcf, 0x95, 0x78, 0x4b, 0xb7, 0x4f, 0x4e, - 0xa2, 0xd5, 0xa8, 0x9c, 0x9a, 0x76, 0xe1, 0x25, 0xd4, 0x6c, 0x6f, 0x68, 0xaf, 0xcc, 0x2d, 0x7a, 0xd9, 0x1d, 0xee, - 0x57, 0x76, 0x8a, 0x22, 0x76, 0xa5, 0x60, 0x9f, 0x56, 0xd2, 0xe3, 0x4a, 0x9c, 0x73, 0xa1, 0xb3, 0x4e, 0x2d, 0xa0, - 0x28, 0xcb, 0x00, 0x2b, 0x2f, 0x15, 0xfa, 0x6d, 0xc5, 0x13, 0x94, 0x1c, 0xa6, 0x36, 0x1b, 0x47, 0xcf, 0x7a, 0x49, - 0x25, 0xf7, 0xd5, 0x06, 0xf7, 0x92, 0x91, 0xd6, 0xde, 0xe1, 0xf2, 0xce, 0x56, 0x1f, 0xf1, 0xb4, 0x87, 0xfb, 0x8f, - 0x59, 0x81, 0x7f, 0xab, 0xca, 0xfb, 0x86, 0x31, 0x14, 0x02, 0xb5, 0xce, 0xa5, 0xd4, 0xb4, 0x49, 0xb8, 0x64, 0x10, - 0xee, 0x1d, 0xac, 0x11, 0x51, 0x27, 0xb0, 0xbf, 0x46, 0xc9, 0x6a, 0x67, 0xc0, 0xe7, 0x49, 0x88, 0x98, 0x13, 0xe5, - 0xb1, 0x7f, 0x7e, 0xb7, 0xb2, 0xc9, 0x9f, 0x98, 0x43, 0x9d, 0xa1, 0x4a, 0x58, 0x87, 0xf8, 0x83, 0xb8, 0x79, 0xd5, - 0xeb, 0xdb, 0x25, 0xca, 0xdf, 0x4c, 0x4f, 0x2c, 0xcc, 0x0a, 0x2b, 0xf8, 0x4b, 0x29, 0x5f, 0xdf, 0xf1, 0x01, 0xd4, - 0x67, 0x93, 0x1f, 0xff, 0xbc, 0xa1, 0x7b, 0xd9, 0x95, 0xff, 0x72, 0x87, 0x40, 0x31, 0xed, 0xe2, 0x70, 0x8e, 0x4b, - 0x87, 0xc2, 0xec, 0x02, 0xc3, 0xe2, 0x45, 0x55, 0x1d, 0xf0, 0xe1, 0x39, 0xe2, 0xe3, 0xf3, 0x24, 0x2e, 0x88, 0x84, - 0xd2, 0xfc, 0x79, 0x50, 0x37, 0xa3, 0xe3, 0xc2, 0x86, 0x3e, 0x38, 0x9c, 0xd1, 0x00, 0x84, 0xac, 0xb1, 0xc9, 0xf6, - 0x63, 0x95, 0x52, 0x99, 0x59, 0x3a, 0x72, 0xc7, 0xc6, 0x76, 0xb5, 0xd4, 0xe3, 0xbb, 0x7e, 0x1f, 0xee, 0x64, 0xd3, - 0xb2, 0x7e, 0xca, 0x10, 0xfb, 0xa8, 0x0b, 0xc3, 0x05, 0xc8, 0xda, 0x0f, 0xb9, 0xf6, 0x62, 0x19, 0xdb, 0x80, 0x3e, - 0xc4, 0xfe, 0xa7, 0x5d, 0x9c, 0xd1, 0xad, 0xf0, 0xb1, 0x58, 0xb4, 0x3a, 0xdb, 0x90, 0x24, 0x07, 0x46, 0x73, 0x48, - 0x30, 0x6e, 0x44, 0x68, 0x1b, 0x94, 0xe0, 0xb1, 0x62, 0x0a, 0x37, 0xe2, 0xfe, 0x38, 0x58, 0x68, 0x55, 0xd1, 0x8d, - 0xb9, 0x8d, 0x6d, 0x14, 0x67, 0xf0, 0xf5, 0x80, 0x7f, 0x15, 0x65, 0x7c, 0x80, 0xbb, 0x41, 0xe4, 0xce, 0x9e, 0x97, - 0x92, 0x48, 0x2d, 0xa7, 0xdb, 0x56, 0x6c, 0xe7, 0x97, 0xd8, 0xed, 0x82, 0x3d, 0x5f, 0x16, 0xe6, 0xcc, 0xd6, 0x20, - 0x33, 0x8c, 0xbd, 0xb7, 0xc0, 0xbb, 0x6d, 0x29, 0x4c, 0x76, 0xe9, 0x1b, 0xa8, 0x84, 0x56, 0xe7, 0xa3, 0xc3, 0x78, - 0x53, 0x07, 0xae, 0xe2, 0x78, 0x1a, 0xdb, 0x56, 0x62, 0x34, 0x46, 0x1e, 0xc9, 0x30, 0x95, 0xe1, 0x10, 0x7f, 0x24, - 0xbb, 0x29, 0x37, 0xd3, 0xa5, 0xce, 0x2e, 0x0d, 0x10, 0x74, 0x85, 0x55, 0x0f, 0x85, 0x24, 0x11, 0x70, 0xcb, 0x15, - 0xc8, 0xc3, 0x70, 0x3f, 0xaf, 0xc6, 0xc8, 0xe1, 0x6c, 0x81, 0xd0, 0xf0, 0xb2, 0x51, 0x90, 0x39, 0xf1, 0xed, 0x21, - 0xf1, 0x72, 0x6a, 0x7a, 0xc7, 0x11, 0x0f, 0x86, 0xea, 0x36, 0x0f, 0x5e, 0x8e, 0xaa, 0x4e, 0x67, 0xe9, 0x91, 0x70, - 0x4b, 0xe7, 0x59, 0x49, 0xca, 0x51, 0x35, 0x05, 0x7a, 0x8e, 0x34, 0x1a, 0xca, 0x5b, 0x5b, 0x29, 0x81, 0x55, 0xd0, - 0x32, 0x39, 0xfd, 0xbc, 0x23, 0x12, 0x91, 0x70, 0x21, 0xa0, 0xf8, 0xcb, 0x28, 0xa4, 0xd1, 0x5b, 0xcd, 0xc7, 0x30, - 0xc8, 0x70, 0x9d, 0x57, 0x5c, 0x0a, 0xfe, 0xf8, 0xc5, 0x01, 0xf4, 0x50, 0x94, 0x0d, 0xdc, 0x2f, 0x16, 0xfe, 0xcc, - 0x2e, 0x46, 0xf4, 0x36, 0x9b, 0xa1, 0xba, 0xcd, 0xe8, 0x27, 0xd6, 0xe7, 0x95, 0x22, 0x76, 0xcf, 0x0e, 0xed, 0xc1, - 0x8e, 0x19, 0x59, 0x5d, 0x25, 0x1d, 0xcd, 0x04, 0x99, 0xf4, 0x32, 0xf2, 0x0c, 0x41, 0x84, 0x0e, 0xd8, 0x27, 0x87, - 0x36, 0x4a, 0x87, 0xf9, 0x0a, 0xc2, 0x3f, 0xc7, 0x7c, 0x0e, 0x89, 0x6e, 0x76, 0x09, 0x8e, 0x7a, 0x8d, 0x48, 0x3a, - 0x89, 0x50, 0x04, 0x5e, 0xc8, 0x7a, 0x22, 0x98, 0x78, 0x41, 0xef, 0x26, 0xb8, 0xe2, 0xc5, 0xd1, 0x4d, 0x07, 0xcb, - 0x61, 0x46, 0xfc, 0x1b, 0x13, 0x46, 0xee, 0x12, 0xe2, 0xdb, 0x03, 0x84, 0x3b, 0xd8, 0x29, 0x88, 0xea, 0xe5, 0x56, - 0x9b, 0x5e, 0x9c, 0xa2, 0x9e, 0xc7, 0xf9, 0x78, 0x36, 0xd6, 0x91, 0x17, 0x72, 0x38, 0x5b, 0xc4, 0x30, 0x40, 0x16, - 0xc2, 0x93, 0x9b, 0x76, 0x97, 0x10, 0x90, 0x9c, 0x4c, 0x65, 0x59, 0xde, 0xc4, 0x4d, 0x27, 0x60, 0x91, 0xe7, 0x76, - 0x69, 0x4a, 0xf1, 0xf4, 0x9f, 0xaa, 0xb1, 0x0d, 0x67, 0x8b, 0x8c, 0x63, 0xd0, 0x62, 0x95, 0xce, 0x20, 0x10, 0x17, - 0xe5, 0x46, 0x4b, 0x2f, 0x0e, 0x73, 0xb8, 0xf9, 0xf7, 0xb8, 0xbc, 0x8d, 0x09, 0x20, 0x1f, 0xbc, 0x49, 0x3a, 0x40, - 0xcf, 0xf2, 0x7c, 0xce, 0xd0, 0x4b, 0x6f, 0x73, 0x91, 0x4c, 0x84, 0xff, 0xd3, 0xc9, 0x47, 0xa2, 0x1c, 0xe9, 0x15, - 0x72, 0x9c, 0x50, 0x51, 0xb2, 0x9d, 0x10, 0xd5, 0xcb, 0xc3, 0x7f, 0x61, 0xd5, 0x11, 0x02, 0xe7, 0xdb, 0x84, 0x2f, - 0x5f, 0x6e, 0xf9, 0xc1, 0xd7, 0x97, 0xec, 0x44, 0x28, 0xe5, 0x1f, 0x18, 0x87, 0x98, 0x56, 0x32, 0xb1, 0x63, 0x40, - 0x64, 0x7a, 0x58, 0xc0, 0x72, 0xe0, 0x66, 0xe4, 0xf1, 0xe3, 0xd6, 0x38, 0xd3, 0x14, 0x9f, 0xab, 0xff, 0x9f, 0xd8, - 0x7a, 0x10, 0xd7, 0x6e, 0x2f, 0x8d, 0x48, 0x62, 0x1a, 0xa3, 0x01, 0xf3, 0x8a, 0x06, 0x68, 0x9c, 0x94, 0x01, 0xc3, - 0x5e, 0x59, 0xfa, 0x85, 0x1e, 0x63, 0x93, 0x47, 0xaa, 0x99, 0x98, 0x1f, 0x21, 0x64, 0xbb, 0x46, 0xc1, 0x44, 0x12, - 0x8c, 0xf6, 0x2d, 0x50, 0xd8, 0x81, 0x14, 0x53, 0xdd, 0x01, 0xf9, 0x9c, 0xcb, 0xc8, 0x6b, 0x20, 0x1b, 0x7d, 0xbe, - 0xb9, 0xd7, 0xaf, 0x13, 0xfb, 0xd2, 0xa3, 0x39, 0x84, 0x48, 0x23, 0xf2, 0xfb, 0xf0, 0xfe, 0x58, 0xaf, 0x99, 0x77, - 0xbd, 0xca, 0x14, 0x2f, 0xc1, 0xc8, 0x07, 0x37, 0x96, 0x90, 0x0f, 0x1a, 0xac, 0x02, 0x73, 0xf2, 0x35, 0xaa, 0xb5, - 0x43, 0xcc, 0xce, 0xf3, 0x26, 0x47, 0xde, 0x76, 0x75, 0x54, 0x51, 0x58, 0xad, 0xc0, 0xf9, 0x55, 0x03, 0xad, 0xc4, - 0x07, 0xf2, 0x2f, 0x43, 0xa2, 0x8a, 0x09, 0x61, 0x80, 0x1e, 0x19, 0xe7, 0x1f, 0x84, 0x28, 0xe8, 0x32, 0xa9, 0x5a, - 0x36, 0xfb, 0x97, 0x9a, 0xc3, 0x55, 0x60, 0x04, 0xec, 0x36, 0xa6, 0x31, 0x8d, 0xe7, 0xe3, 0x28, 0x66, 0xd6, 0xbc, - 0x2b, 0x89, 0xaf, 0x70, 0x2e, 0x08, 0x2a, 0xac, 0xe1, 0xbe, 0xcb, 0xff, 0xfd, 0x7c, 0xfc, 0x90, 0x97, 0x62, 0xe7, - 0xd7, 0xe5, 0x1a, 0xfa, 0x61, 0xff, 0x75, 0x29, 0x56, 0xbd, 0x49, 0x2d, 0x7a, 0x37, 0x9a, 0x36, 0x8e, 0xff, 0x7c, - 0x76, 0xb1, 0x91, 0x4e, 0xef, 0x78, 0xcb, 0x7b, 0xd0, 0x37, 0xa7, 0xe9, 0x69, 0x5c, 0xe0, 0xe7, 0x2c, 0x2f, 0x67, - 0xff, 0x95, 0xbb, 0x94, 0xc7, 0xf5, 0x7b, 0x76, 0xdd, 0xa1, 0x39, 0xad, 0xbd, 0xb1, 0xec, 0xdd, 0xb3, 0x2b, 0xfe, - 0x1e, 0x81, 0x2c, 0xbe, 0x08, 0xc9, 0xa4, 0x52, 0x09, 0x20, 0xd0, 0x5c, 0x0f, 0x7e, 0xf7, 0xc4, 0x28, 0xa5, 0x1e, - 0xef, 0x3f, 0x26, 0x5f, 0x95, 0x75, 0xb8, 0x3b, 0xb7, 0x40, 0xd6, 0x23, 0xfd, 0x3b, 0x4f, 0x37, 0xba, 0x5f, 0xd0, - 0xa8, 0x3a, 0x75, 0x90, 0x19, 0x8d, 0x33, 0x2d, 0x0d, 0xf9, 0xb7, 0x8d, 0xe6, 0x8c, 0xc2, 0xb7, 0x82, 0x46, 0x74, - 0x13, 0xe1, 0x1f, 0x57, 0x8d, 0x03, 0x4a, 0x0a, 0xf8, 0x61, 0x9b, 0xf6, 0x6d, 0xf7, 0x72, 0x2f, 0xa4, 0xa9, 0xf2, - 0xcb, 0x33, 0x16, 0x18, 0xb4, 0x0f, 0x74, 0x66, 0x47, 0xff, 0x7f, 0x0a, 0x68, 0xbd, 0x88, 0x51, 0xb2, 0x95, 0x3a, - 0x40, 0x5c, 0x6c, 0xe3, 0xe6, 0x0b, 0xbd, 0x71, 0x9a, 0x0b, 0x67, 0x1e, 0xf5, 0xe8, 0x24, 0xdd, 0x02, 0x18, 0xd5, - 0xfc, 0x7e, 0xc4, 0xab, 0x53, 0x57, 0x46, 0x7c, 0x54, 0xbc, 0xa3, 0xbb, 0x0b, 0xcc, 0xf6, 0xbf, 0xf2, 0x2e, 0x46, - 0x34, 0x7f, 0xf7, 0x11, 0xe8, 0x86, 0x1f, 0xb3, 0xd3, 0x37, 0x9f, 0xf9, 0xe3, 0x03, 0x3e, 0x0c, 0xed, 0x1e, 0xa3, - 0x79, 0x67, 0xdc, 0x9a, 0x27, 0x3c, 0x31, 0xc8, 0x0c, 0xe0, 0xb2, 0xcf, 0xde, 0x7b, 0x2c, 0xe3, 0xc0, 0x77, 0x20, - 0x56, 0x26, 0xf3, 0x16, 0x30, 0x29, 0x17, 0x23, 0xa4, 0x35, 0x32, 0xfa, 0x37, 0xe0, 0x45, 0xc9, 0xe8, 0x9f, 0xce, - 0x3d, 0x8a, 0x6e, 0x48, 0xf4, 0xc9, 0x93, 0x01, 0xcb, 0x3a, 0x28, 0x5a, 0x62, 0x52, 0x21, 0x3a, 0x84, 0x2c, 0x13, - 0xa0, 0xf4, 0x49, 0xa0, 0xa1, 0xf0, 0x77, 0x2d, 0x27, 0xbd, 0x9f, 0x7b, 0x66, 0x82, 0xa4, 0xc7, 0xe4, 0x28, 0x8d, - 0x4c, 0x18, 0xf9, 0x73, 0xcd, 0xcb, 0xeb, 0xeb, 0xa7, 0x76, 0x7b, 0xd0, 0x7c, 0x64, 0xbf, 0x95, 0xe6, 0xc4, 0xe4, - 0x6b, 0xad, 0x06, 0x2b, 0x79, 0x03, 0x28, 0x9b, 0x7d, 0x41, 0x2b, 0x60, 0xf1, 0x5b, 0x0d, 0x61, 0xe9, 0x99, 0x0c, - 0xb4, 0x06, 0x4e, 0xd2, 0x73, 0x36, 0xb8, 0x6e, 0x98, 0x1f, 0x91, 0x5e, 0xaf, 0x98, 0xa8, 0x32, 0xa7, 0x27, 0x7d, - 0xba, 0xb9, 0x1e, 0x7b, 0xb1, 0xd0, 0x87, 0xd4, 0x13, 0xfa, 0x93, 0x17, 0xe1, 0x6c, 0xf9, 0xb9, 0xec, 0x3f, 0x4d, - 0x20, 0x75, 0xd5, 0x18, 0x2d, 0x74, 0x7e, 0x3d, 0xbe, 0x9b, 0x35, 0x3e, 0x1a, 0xd9, 0xea, 0x6d, 0xbb, 0x73, 0x64, - 0xb9, 0x77, 0x8b, 0x59, 0x5f, 0x42, 0x3e, 0xa3, 0x58, 0x33, 0x99, 0x83, 0x9c, 0x23, 0xb4, 0xbf, 0xd6, 0x95, 0xe4, - 0xb8, 0xf6, 0x61, 0x4e, 0x41, 0x7a, 0x6c, 0x0d, 0xeb, 0x20, 0x6a, 0xbe, 0xad, 0x7d, 0x06, 0x2d, 0xbf, 0x9e, 0x7a, - 0x9d, 0x16, 0x4c, 0xf2, 0xa4, 0x73, 0x5f, 0xf7, 0x8f, 0x34, 0xe2, 0x5e, 0x7a, 0x59, 0x13, 0x45, 0xb7, 0x48, 0x40, - 0xd7, 0x2a, 0x2d, 0xf4, 0xb2, 0xe2, 0x3c, 0xad, 0xe8, 0x4f, 0x33, 0xe6, 0x51, 0xc9, 0xaa, 0x51, 0xa9, 0x9e, 0x5c, - 0x63, 0x9c, 0x29, 0xeb, 0x09, 0x20, 0x17, 0x45, 0x02, 0xc7, 0x59, 0x6f, 0xd7, 0xa7, 0x4b, 0x43, 0x07, 0xf1, 0xd1, - 0xdb, 0xb8, 0xe9, 0xbc, 0x83, 0x69, 0x2c, 0xdd, 0x9f, 0x48, 0x67, 0x19, 0xc3, 0x89, 0x2a, 0x4b, 0xf2, 0xb4, 0x1c, - 0x85, 0xba, 0xa3, 0xbb, 0x20, 0x29, 0x4b, 0xf6, 0x46, 0x3b, 0xfb, 0xe3, 0x7a, 0xf2, 0x28, 0xfb, 0x30, 0xec, 0xa1, - 0x0a, 0xdc, 0x43, 0xaa, 0xef, 0x72, 0xff, 0xba, 0xcc, 0x94, 0xa6, 0xc1, 0xfe, 0xc7, 0xd7, 0xa1, 0x03, 0x3f, 0x0e, - 0x6e, 0xc7, 0x11, 0x12, 0x28, 0xb7, 0x98, 0xa6, 0x0c, 0x5b, 0x4e, 0x30, 0xd9, 0xee, 0x0d, 0x37, 0xc5, 0xd5, 0x9e, - 0x4b, 0x14, 0x83, 0x25, 0xf7, 0xc0, 0xcb, 0x67, 0xb4, 0x7f, 0x62, 0xeb, 0x26, 0xe6, 0xa9, 0x6b, 0xe1, 0xa3, 0xd4, - 0xc2, 0x34, 0x74, 0x60, 0xb0, 0xc8, 0x59, 0x92, 0x8c, 0x04, 0xbb, 0xfc, 0xd2, 0x6a, 0x27, 0xcf, 0x72, 0x25, 0xd3, - 0xd7, 0x5e, 0x4f, 0x4c, 0x31, 0xd2, 0xb0, 0xa5, 0xed, 0x70, 0xd8, 0xc9, 0x79, 0x02, 0x22, 0x44, 0x54, 0xcf, 0x97, - 0xb8, 0xa6, 0xbe, 0x10, 0x67, 0xdd, 0xf9, 0x32, 0x56, 0xb4, 0xe7, 0x41, 0x01, 0x08, 0xad, 0x36, 0xad, 0x54, 0x17, - 0xdc, 0xd0, 0x23, 0x48, 0x77, 0xeb, 0xe5, 0x1d, 0x14, 0x55, 0xcd, 0xf4, 0x60, 0xd2, 0x8b, 0x1f, 0xe7, 0x5d, 0xe1, - 0x61, 0x16, 0x19, 0x2a, 0x80, 0x1b, 0xa3, 0xef, 0xe0, 0x72, 0x7d, 0xcf, 0x43, 0xb8, 0xb5, 0xe6, 0x4c, 0xcf, 0x4f, - 0x5b, 0x8f, 0x78, 0xf1, 0xe6, 0x61, 0x1c, 0xc2, 0x5d, 0x6c, 0x7d, 0xfa, 0x24, 0x5f, 0x3b, 0x6c, 0xe7, 0xd1, 0xa2, - 0xd0, 0xd6, 0xe5, 0xd4, 0xf6, 0xe2, 0xce, 0xe7, 0xf9, 0x27, 0xb5, 0x05, 0xca, 0x0a, 0xbc, 0xf6, 0xea, 0x3e, 0x22, - 0x14, 0x73, 0x07, 0xdf, 0xfe, 0x2f, 0x1d, 0xb5, 0xdd, 0x7c, 0xde, 0x0e, 0x67, 0x46, 0x0f, 0xcf, 0x48, 0x88, 0xba, - 0x3c, 0xd8, 0x24, 0xd7, 0xaf, 0xfe, 0xe9, 0x29, 0x7e, 0xa5, 0x9d, 0xe6, 0x5f, 0x73, 0xce, 0x0b, 0x63, 0x53, 0x3e, - 0xdb, 0x47, 0x9a, 0x30, 0xba, 0x46, 0x84, 0xcb, 0xef, 0xdb, 0xd0, 0x4a, 0x83, 0x8c, 0x48, 0x08, 0x79, 0xbd, 0x75, - 0x05, 0xb8, 0xef, 0x2f, 0xdb, 0x1d, 0xbc, 0xa5, 0x44, 0xe2, 0x8d, 0xea, 0x38, 0x6e, 0xcf, 0xc8, 0xc2, 0xf5, 0xfd, - 0x5b, 0x07, 0x82, 0x7d, 0xad, 0x7d, 0x25, 0xbf, 0xdc, 0x39, 0x7a, 0x01, 0x06, 0x94, 0x30, 0x84, 0x27, 0x51, 0xff, - 0x97, 0xd8, 0x88, 0xd4, 0x6d, 0xc6, 0x74, 0xc2, 0x84, 0xfd, 0x59, 0xd1, 0xaa, 0xad, 0xf4, 0x00, 0x28, 0xa6, 0x4e, - 0xae, 0x06, 0x51, 0x74, 0x87, 0x26, 0xe2, 0x8e, 0x39, 0x5a, 0xde, 0x13, 0x9a, 0xb5, 0x40, 0x15, 0x4e, 0x61, 0xcf, - 0xa3, 0x50, 0x9a, 0xe1, 0x19, 0xf4, 0x01, 0xf6, 0x52, 0x84, 0x9c, 0xb9, 0x24, 0x79, 0xe6, 0xc0, 0x6b, 0x13, 0x04, - 0x69, 0x74, 0xb7, 0x7a, 0x4a, 0xb1, 0x8b, 0x6c, 0xb7, 0x20, 0xe9, 0xcd, 0x22, 0x74, 0x2b, 0x56, 0x49, 0x8a, 0xbb, - 0x99, 0x8a, 0xad, 0x0e, 0x1e, 0x61, 0x8f, 0x48, 0xdf, 0x96, 0xfd, 0xbd, 0x75, 0xc0, 0x42, 0x17, 0x45, 0x4d, 0x4a, - 0xed, 0xbf, 0x29, 0x1d, 0x38, 0xa1, 0x86, 0x09, 0x05, 0x05, 0xfb, 0x6c, 0xdc, 0x62, 0xbc, 0x7b, 0x6b, 0x6d, 0x6f, - 0x21, 0xf0, 0x2a, 0x34, 0x37, 0xd5, 0x82, 0x5c, 0xe1, 0x0b, 0x64, 0xc9, 0xb5, 0x15, 0x42, 0xd7, 0x37, 0x2d, 0xbb, - 0xf0, 0xfc, 0xc2, 0xf4, 0xc7, 0x56, 0x29, 0xea, 0x52, 0x90, 0x4b, 0x38, 0xb5, 0xb2, 0x46, 0x57, 0x1f, 0xd8, 0x9a, - 0x8e, 0x51, 0xbb, 0x33, 0xce, 0x5e, 0x21, 0x90, 0xfc, 0x89, 0x4a, 0x9d, 0x53, 0x9a, 0x11, 0x18, 0x5e, 0x0f, 0x8a, - 0xd5, 0x2f, 0xb9, 0x16, 0x30, 0x0e, 0x0f, 0xf4, 0xc7, 0xa0, 0x48, 0x9e, 0x64, 0x62, 0x0e, 0x03, 0x4f, 0xe5, 0xb0, - 0x73, 0xcf, 0xe9, 0x4e, 0xe6, 0xf7, 0xbe, 0xb1, 0xb7, 0xc7, 0xae, 0xe3, 0x96, 0x31, 0x3f, 0x8c, 0x20, 0x6a, 0x25, - 0xc2, 0x48, 0x45, 0x1e, 0x31, 0x80, 0x12, 0x4e, 0xae, 0x1b, 0x70, 0xa8, 0xa9, 0x36, 0xdc, 0xa7, 0xe8, 0x08, 0xcc, - 0xa9, 0xcb, 0x34, 0xaa, 0x39, 0x55, 0x99, 0x20, 0x84, 0xcf, 0x8d, 0x5b, 0xe7, 0x78, 0x02, 0x33, 0xed, 0x80, 0xd5, - 0x26, 0xaf, 0x53, 0x1c, 0x84, 0xcc, 0xd4, 0x9d, 0x2d, 0x1a, 0x13, 0x49, 0x4d, 0xb5, 0x4b, 0xad, 0x05, 0xe3, 0x64, - 0xb3, 0x6b, 0xd4, 0x6e, 0x2b, 0x32, 0xb8, 0x88, 0x15, 0x0f, 0x64, 0x04, 0x38, 0xba, 0x96, 0x6b, 0x94, 0x27, 0x47, - 0x5a, 0x10, 0xe6, 0x26, 0x39, 0x8e, 0x98, 0xb6, 0x7f, 0xdc, 0x8d, 0xe8, 0x66, 0x9e, 0x99, 0x8a, 0xc3, 0x5f, 0xbd, - 0xe7, 0xb6, 0x5e, 0x59, 0x2a, 0xd6, 0xf3, 0x2c, 0x25, 0xeb, 0x95, 0xcf, 0x2c, 0xa5, 0x21, 0xb9, 0xb0, 0x16, 0xd8, - 0x6c, 0x9a, 0xa5, 0xd9, 0x72, 0x7a, 0xde, 0xb9, 0x45, 0x66, 0x5e, 0xf0, 0x08, 0x53, 0xde, 0xae, 0xbc, 0x44, 0x67, - 0x03, 0xf6, 0x3f, 0xfb, 0x7c, 0x09, 0x9a, 0x19, 0x2b, 0x34, 0xc7, 0xbb, 0xc2, 0x1c, 0x12, 0x59, 0x61, 0xd4, 0x8f, - 0x4b, 0xf9, 0xec, 0x5d, 0x70, 0xda, 0x6a, 0xe7, 0x46, 0x05, 0x85, 0xef, 0x4d, 0x52, 0x60, 0x22, 0x09, 0x6c, 0x72, - 0x34, 0xee, 0x83, 0xf3, 0xac, 0x9c, 0xe9, 0x97, 0x03, 0x04, 0xff, 0x89, 0x6d, 0xc6, 0x35, 0x27, 0x30, 0x77, 0x06, - 0x77, 0x4a, 0xa8, 0x6e, 0x88, 0xe1, 0xf5, 0xd9, 0x75, 0x4e, 0x56, 0x1c, 0x73, 0x4b, 0xb2, 0x10, 0xe0, 0xb5, 0x07, - 0xb7, 0xcf, 0x33, 0x6b, 0x71, 0xa7, 0xe2, 0x34, 0xd4, 0x66, 0x5f, 0xfa, 0xcc, 0xd7, 0x83, 0x5f, 0x8d, 0x1c, 0x65, - 0x5c, 0xe0, 0x66, 0xd7, 0x8b, 0x81, 0x21, 0x34, 0x9e, 0x05, 0xe8, 0x11, 0x4f, 0xe9, 0xbf, 0x80, 0x10, 0xbf, 0x1b, - 0xfc, 0x2a, 0x33, 0x83, 0xd5, 0xd7, 0x2a, 0x06, 0x89, 0x9e, 0x64, 0x42, 0x81, 0x91, 0x61, 0xe8, 0xba, 0x2a, 0x8b, - 0x84, 0x37, 0xbc, 0xd8, 0xcd, 0xee, 0xcd, 0x98, 0x3f, 0x60, 0xa8, 0x43, 0xf8, 0x25, 0xb1, 0x27, 0xe6, 0x39, 0x9c, - 0x6a, 0xe6, 0x65, 0x76, 0x56, 0x45, 0x63, 0xbd, 0x59, 0xe3, 0x89, 0x09, 0xd5, 0x87, 0x68, 0xdb, 0x37, 0xc5, 0xdc, - 0x6e, 0xf7, 0xd6, 0x87, 0xd3, 0x44, 0x8d, 0x98, 0x99, 0x9a, 0x8f, 0xfb, 0xc6, 0x0a, 0x69, 0x33, 0x52, 0x64, 0x12, - 0xaa, 0x0c, 0x56, 0xc2, 0xc8, 0x3d, 0xbd, 0x6d, 0x75, 0x74, 0x5a, 0x00, 0x4e, 0x34, 0xcb, 0xdb, 0x4a, 0x64, 0xa3, - 0xbd, 0xb6, 0x1b, 0x85, 0xa8, 0x17, 0x3d, 0x9e, 0x51, 0x28, 0x15, 0x37, 0x34, 0x70, 0x6e, 0x06, 0x02, 0x4b, 0x3f, - 0xc5, 0x4b, 0xd8, 0x8b, 0xae, 0x3d, 0x6b, 0xc2, 0xb5, 0x51, 0x7b, 0x87, 0xb4, 0xac, 0x54, 0x4b, 0xd9, 0x77, 0x8e, - 0x74, 0xe3, 0x85, 0xaa, 0x97, 0xb9, 0xd0, 0xb9, 0xda, 0x4f, 0x7c, 0x6c, 0x1b, 0x23, 0x4d, 0xed, 0x9a, 0xfe, 0x66, - 0xce, 0x36, 0xd7, 0x99, 0xac, 0x90, 0x1f, 0x2c, 0x43, 0xfe, 0x04, 0xe9, 0xb6, 0x91, 0x4d, 0xac, 0xc4, 0xfa, 0x85, - 0x1f, 0xf0, 0x0e, 0x3a, 0x67, 0x2d, 0x3b, 0xb0, 0x36, 0xdb, 0x2e, 0x58, 0x26, 0x3f, 0x58, 0xae, 0x5d, 0xe3, 0x37, - 0x7c, 0x08, 0x57, 0xb2, 0x3a, 0x97, 0x9d, 0xec, 0x3d, 0xfe, 0x45, 0xfd, 0xf2, 0xfb, 0x19, 0x3d, 0x8b, 0x0f, 0x96, - 0x35, 0xde, 0x4c, 0x9f, 0xb2, 0x32, 0xfb, 0xc5, 0xed, 0x5b, 0x8b, 0x8f, 0x37, 0x97, 0x36, 0x38, 0x8f, 0x61, 0x68, - 0xef, 0xc5, 0xdd, 0x83, 0xfa, 0xc3, 0x70, 0x56, 0x4e, 0xd0, 0x6a, 0x18, 0x19, 0xe0, 0xce, 0xd6, 0xf3, 0x05, 0xbd, - 0xc7, 0xc6, 0x4c, 0x1f, 0xee, 0xf9, 0xd0, 0xbb, 0xfc, 0xc7, 0xcb, 0x7e, 0x24, 0x9c, 0x3d, 0x3a, 0xbb, 0x40, 0xd0, - 0x5a, 0xd7, 0x56, 0x4a, 0xf5, 0x98, 0xd7, 0x2e, 0x8e, 0xd0, 0x92, 0x3d, 0x2f, 0x75, 0x34, 0xff, 0xd0, 0x2a, 0x87, - 0x0d, 0x1a, 0x63, 0xf5, 0xbe, 0xd5, 0x96, 0x46, 0x6f, 0x3f, 0x10, 0x16, 0xa6, 0xa1, 0x52, 0x81, 0x80, 0x4a, 0xff, - 0xcc, 0x26, 0x5c, 0x7b, 0x9b, 0xc9, 0x28, 0x7d, 0x8a, 0x30, 0x7b, 0xd4, 0x93, 0xc5, 0xfb, 0x8e, 0x9d, 0xac, 0xd5, - 0x1b, 0xca, 0x74, 0x58, 0x69, 0x33, 0x59, 0xa9, 0x11, 0x46, 0x0c, 0x33, 0x9b, 0xe1, 0x85, 0x2a, 0xa7, 0xc3, 0xae, - 0x04, 0x81, 0xa9, 0xda, 0x78, 0xe2, 0xdc, 0xc1, 0xf3, 0xdf, 0xb2, 0x3e, 0x40, 0x8c, 0xe9, 0xe1, 0xc4, 0xde, 0x81, - 0x2e, 0xb5, 0x8b, 0x27, 0xfc, 0xcb, 0xdf, 0x92, 0x43, 0xb0, 0x42, 0xb5, 0xf1, 0xfd, 0x00, 0xd6, 0xd7, 0x20, 0xe6, - 0x6d, 0x77, 0xe2, 0xe0, 0x8e, 0x5d, 0x59, 0x3e, 0xcd, 0xcb, 0x03, 0x88, 0x52, 0x36, 0x72, 0xb3, 0x61, 0xcd, 0xaf, - 0x66, 0x16, 0xbb, 0x36, 0x9e, 0x1f, 0xc8, 0xfa, 0xb0, 0xcd, 0x9f, 0x0b, 0x90, 0xa2, 0x28, 0xd0, 0x96, 0xbb, 0xda, - 0xa2, 0x8d, 0x80, 0x69, 0xd7, 0x3a, 0x6f, 0x6d, 0x21, 0xeb, 0xa4, 0x76, 0xca, 0xa0, 0x2b, 0x65, 0x8a, 0x9c, 0x9a, - 0x51, 0x23, 0x44, 0xc7, 0xf8, 0x41, 0x0e, 0xfd, 0x62, 0xf5, 0xdd, 0xf5, 0x3b, 0x5d, 0x80, 0xb8, 0xe2, 0x54, 0xe6, - 0x59, 0x49, 0xac, 0x0f, 0x37, 0x79, 0xcf, 0x1b, 0xf4, 0xbf, 0xd4, 0x95, 0xef, 0xcb, 0xda, 0x13, 0x24, 0x03, 0x41, - 0x3a, 0x0e, 0xfe, 0x18, 0xc0, 0xf0, 0xc7, 0x06, 0x46, 0x2f, 0x7a, 0x78, 0x1e, 0x54, 0xbf, 0x76, 0xc2, 0x77, 0x96, - 0x5f, 0xaa, 0xd0, 0xfb, 0x49, 0xf5, 0x0b, 0x58, 0x5f, 0x83, 0xa0, 0x8e, 0x44, 0xcd, 0xef, 0x69, 0x5b, 0xf7, 0x2b, - 0x8c, 0x78, 0x91, 0x0f, 0x15, 0xf9, 0xeb, 0xba, 0xfa, 0x3c, 0x87, 0x01, 0x39, 0xf6, 0x09, 0x06, 0x36, 0xfd, 0xb2, - 0x0f, 0x21, 0x78, 0x5f, 0x5f, 0xd5, 0x42, 0xe3, 0x97, 0x22, 0x4e, 0x50, 0xe1, 0x81, 0x2c, 0x74, 0x3c, 0xb5, 0x72, - 0x6b, 0x1d, 0x99, 0x68, 0x6c, 0x62, 0x14, 0x3a, 0x8b, 0x15, 0x6c, 0xcc, 0x27, 0xa3, 0xba, 0xf2, 0x86, 0x09, 0x86, - 0x5f, 0xad, 0x3f, 0x9d, 0xa5, 0x57, 0x5b, 0x85, 0xbd, 0xaa, 0xf0, 0x5f, 0x75, 0x13, 0xbe, 0xc9, 0x70, 0x58, 0x05, - 0x2f, 0x08, 0x15, 0xfc, 0x40, 0x27, 0x55, 0xa8, 0xa3, 0xd3, 0x10, 0xa1, 0x55, 0xb3, 0x82, 0x1c, 0x15, 0xda, 0xef, - 0xdb, 0xd4, 0xd6, 0x9b, 0xea, 0xec, 0xed, 0x58, 0xd5, 0x54, 0x98, 0x1f, 0x8f, 0x59, 0x4d, 0x33, 0x12, 0x95, 0x2c, - 0xbf, 0x83, 0xdd, 0x69, 0x0b, 0x6f, 0x9f, 0xc0, 0xfb, 0x9b, 0xfa, 0x31, 0xe3, 0xb3, 0x6c, 0xd2, 0x04, 0xba, 0x33, - 0xd7, 0x02, 0xb5, 0x4f, 0x4d, 0xdd, 0x91, 0xb9, 0x0e, 0xec, 0x5d, 0xcd, 0x97, 0xf8, 0x4c, 0x84, 0xbb, 0x5f, 0x93, - 0xa8, 0xcc, 0x69, 0x06, 0x6d, 0x2c, 0xa5, 0x89, 0xaa, 0xdb, 0x70, 0xca, 0xb0, 0xf7, 0x0c, 0xed, 0x02, 0x6a, 0xf4, - 0x44, 0x77, 0x62, 0x8c, 0x90, 0xc6, 0xfd, 0x22, 0xb4, 0x1f, 0xe9, 0x79, 0x2b, 0x90, 0x8e, 0xed, 0x18, 0xa6, 0x9b, - 0x06, 0xc8, 0x5a, 0xe8, 0xe3, 0x5f, 0x5f, 0xed, 0xc3, 0xd8, 0xe6, 0xfd, 0x06, 0x61, 0xa9, 0xde, 0x1e, 0x1d, 0x20, - 0xf9, 0x9e, 0x52, 0x58, 0x5c, 0xd1, 0x1a, 0xad, 0x86, 0x8d, 0x83, 0x5c, 0x61, 0x30, 0xca, 0x54, 0xe9, 0x3c, 0x62, - 0x38, 0x1a, 0xc2, 0x08, 0x85, 0x42, 0x5e, 0x7d, 0xc4, 0x9a, 0x79, 0xdc, 0x9e, 0x3d, 0x94, 0x56, 0x07, 0xbf, 0x7a, - 0xb2, 0x46, 0x7d, 0xe9, 0x5d, 0x6e, 0xc6, 0x52, 0x8b, 0x8f, 0x57, 0xbc, 0xd1, 0xeb, 0xcb, 0x84, 0x66, 0x6e, 0xd1, - 0xa0, 0x14, 0x1b, 0x12, 0xbb, 0x95, 0xdf, 0x13, 0xeb, 0xb1, 0x59, 0x21, 0x09, 0x99, 0x5f, 0x5e, 0x99, 0xca, 0x53, - 0x79, 0x7f, 0x65, 0x39, 0xc3, 0x51, 0x3c, 0x78, 0x07, 0x7e, 0xd1, 0xcb, 0x9f, 0xa4, 0xde, 0xaa, 0x6e, 0x4b, 0x1b, - 0x14, 0xb5, 0x73, 0xcb, 0x86, 0x73, 0xe1, 0x3a, 0x29, 0x54, 0xc1, 0x0d, 0x16, 0x49, 0x23, 0x6f, 0x1d, 0x2f, 0x3e, - 0xc5, 0x60, 0xca, 0xc2, 0x19, 0x94, 0xb5, 0xcc, 0x05, 0xd6, 0x68, 0x1f, 0x86, 0x67, 0x8b, 0xcc, 0x18, 0x33, 0x18, - 0xdb, 0x70, 0x6e, 0xf9, 0xac, 0xfb, 0xfa, 0x85, 0xe0, 0xfd, 0xc6, 0x48, 0x44, 0x2c, 0x1f, 0xa0, 0x0f, 0x06, 0xa4, - 0x7f, 0x59, 0x62, 0xe4, 0xc3, 0x73, 0x05, 0x7e, 0xd2, 0xb2, 0x70, 0x00, 0x36, 0x6b, 0xef, 0x30, 0x2e, 0x92, 0x79, - 0xab, 0xdb, 0x31, 0x3b, 0x04, 0x37, 0x6c, 0x8d, 0x22, 0x18, 0x15, 0xa3, 0x25, 0x18, 0xac, 0xa0, 0x21, 0xb8, 0x80, - 0xf3, 0x75, 0xc4, 0xaa, 0xc7, 0x29, 0x2e, 0x33, 0x75, 0x86, 0x7f, 0x76, 0x37, 0xcd, 0xb2, 0x1a, 0xc4, 0x07, 0xa1, - 0xc8, 0x16, 0xec, 0xc1, 0xc5, 0x63, 0xe1, 0xcf, 0x21, 0xdf, 0x45, 0x61, 0xe9, 0x1a, 0xff, 0xaf, 0x43, 0xaa, 0xf7, - 0x3d, 0xec, 0x9e, 0x60, 0x0f, 0x3a, 0xa9, 0x2d, 0x34, 0x7f, 0x85, 0x55, 0x15, 0x55, 0xf3, 0xcd, 0x08, 0x8f, 0x16, - 0x5c, 0xab, 0x23, 0xd0, 0x41, 0x20, 0xd4, 0x6a, 0x06, 0x03, 0xb4, 0xe3, 0x07, 0xf8, 0xd2, 0xf1, 0xf8, 0x25, 0x89, - 0x09, 0xcf, 0xef, 0x9b, 0x10, 0xc4, 0xe3, 0xe8, 0x71, 0xe7, 0xfa, 0x43, 0x95, 0x21, 0xb2, 0x48, 0xea, 0x7e, 0x84, - 0xb9, 0xfd, 0x34, 0x17, 0x2e, 0x16, 0x27, 0xe8, 0xb1, 0x5c, 0x71, 0xc7, 0x3d, 0xea, 0x6e, 0xda, 0x3d, 0x9f, 0xb2, - 0x27, 0x31, 0x96, 0x52, 0xc4, 0x1d, 0xad, 0xcd, 0xb8, 0x22, 0x45, 0xae, 0x36, 0x81, 0x5e, 0x8e, 0xf4, 0x1c, 0x8f, - 0x64, 0x29, 0x51, 0xc7, 0x12, 0x44, 0xad, 0xe2, 0x3b, 0x23, 0x05, 0xd5, 0x28, 0xef, 0x72, 0xf7, 0xad, 0xd3, 0xd4, - 0xdd, 0xcf, 0xee, 0xa7, 0xc1, 0xcb, 0x54, 0xe7, 0x8c, 0x77, 0x5e, 0xb4, 0x5a, 0xfb, 0x22, 0x46, 0xaf, 0x1f, 0x0b, - 0x32, 0x9c, 0xf6, 0x5d, 0x67, 0x01, 0x6a, 0x95, 0xe5, 0xbf, 0x41, 0x20, 0x53, 0x74, 0x97, 0x9e, 0x8e, 0x68, 0xae, - 0x74, 0xf9, 0x8e, 0x0e, 0x54, 0x26, 0x0a, 0x31, 0xd3, 0x68, 0xf6, 0x80, 0xce, 0x2d, 0xcf, 0x75, 0x19, 0xf5, 0x2e, - 0xa2, 0x0d, 0x0a, 0xb5, 0xcf, 0xd1, 0x5d, 0x2f, 0x3a, 0x87, 0xeb, 0x94, 0xdb, 0x47, 0xcb, 0x45, 0xe5, 0xb3, 0xf1, - 0x70, 0x61, 0x97, 0x48, 0x22, 0x1f, 0x78, 0x09, 0x31, 0x74, 0xdf, 0xce, 0x30, 0x83, 0xb3, 0xda, 0xbb, 0x5d, 0xaa, - 0x1b, 0x3e, 0x84, 0x1e, 0xc5, 0xc2, 0xb5, 0x59, 0xce, 0xff, 0x97, 0xde, 0x45, 0xf5, 0xb7, 0x3a, 0x25, 0xee, 0x17, - 0xfe, 0x5d, 0x24, 0x8a, 0x84, 0x1e, 0xd2, 0x90, 0xde, 0x9f, 0x95, 0x1d, 0x98, 0x0f, 0xed, 0xa1, 0x32, 0x35, 0x79, - 0x9e, 0x05, 0xa0, 0xf5, 0xaa, 0x50, 0x46, 0x0e, 0x46, 0x4f, 0xce, 0x3b, 0xa4, 0x10, 0x86, 0x90, 0xc3, 0x20, 0x11, - 0x73, 0x1d, 0x70, 0x73, 0xd5, 0xed, 0x2c, 0x45, 0x85, 0xee, 0x1a, 0x96, 0x12, 0xd0, 0x11, 0x1d, 0x92, 0xcc, 0x9c, - 0xd0, 0x10, 0x14, 0x28, 0xf2, 0x1e, 0x31, 0x18, 0x4d, 0xe0, 0x3f, 0x98, 0x7d, 0x14, 0xd2, 0x08, 0x08, 0xe3, 0x14, - 0xc5, 0x7b, 0x20, 0x0e, 0x94, 0xd6, 0x3d, 0x98, 0x56, 0xe1, 0xaa, 0x57, 0xda, 0xca, 0x18, 0xbe, 0xc6, 0xb9, 0x33, - 0xc8, 0x05, 0x9e, 0xea, 0x5e, 0xcc, 0x80, 0x28, 0x40, 0x29, 0x68, 0xc1, 0x49, 0x90, 0x7c, 0xa8, 0x15, 0x48, 0xc0, - 0x21, 0xae, 0x41, 0xa9, 0xb1, 0xe0, 0xd5, 0x78, 0xa3, 0x10, 0x96, 0x62, 0x24, 0x02, 0x21, 0xd9, 0x30, 0xac, 0x98, - 0x0a, 0xb4, 0xfb, 0xc5, 0xbe, 0xf7, 0xc2, 0xe3, 0x43, 0x7d, 0x23, 0xe6, 0x02, 0x09, 0xa3, 0xb3, 0x93, 0x7b, 0x81, - 0x24, 0x7f, 0xb5, 0xa7, 0x2b, 0xb3, 0xbc, 0xf0, 0x8d, 0x85, 0x73, 0xb5, 0x12, 0x10, 0xf6, 0x6f, 0x8c, 0x03, 0x01, - 0x30, 0x97, 0xce, 0x6a, 0x2d, 0x91, 0x95, 0x0b, 0x69, 0xd6, 0x63, 0x29, 0xd6, 0xdd, 0x3c, 0x54, 0x80, 0x29, 0xb5, - 0xb8, 0x20, 0x95, 0x15, 0xde, 0x68, 0x0e, 0xa6, 0xf0, 0xa6, 0x83, 0xae, 0xcd, 0x67, 0xff, 0x43, 0xee, 0x1e, 0x1e, - 0x87, 0x57, 0xaa, 0x5b, 0x82, 0x51, 0x67, 0x92, 0xc1, 0x89, 0x4c, 0xa5, 0x9e, 0x06, 0xb1, 0x93, 0xbe, 0x13, 0x20, - 0x90, 0xd0, 0x38, 0x25, 0x9d, 0x8d, 0x74, 0xe8, 0x03, 0xf7, 0x43, 0x59, 0x50, 0x7c, 0x1d, 0x75, 0x7c, 0x11, 0x45, - 0x58, 0x64, 0xa5, 0x67, 0x97, 0x57, 0x37, 0x8d, 0xce, 0xcc, 0x4b, 0xcb, 0x9c, 0xc6, 0x4f, 0x60, 0xc9, 0x0a, 0x51, - 0xf2, 0x92, 0xb4, 0xb0, 0x9c, 0xe0, 0x7a, 0xa0, 0xe9, 0xb0, 0x20, 0x73, 0xe3, 0xb8, 0xfe, 0x51, 0x31, 0x8e, 0xa9, - 0xc3, 0x9e, 0xd2, 0x9b, 0x0a, 0x3c, 0x75, 0x64, 0x15, 0x3a, 0x10, 0x9e, 0x61, 0xbc, 0xa6, 0x81, 0x37, 0xfb, 0xf5, - 0xfc, 0xdf, 0x01, 0x8d, 0xe3, 0xc3, 0x25, 0x6d, 0xb8, 0x0e, 0xab, 0x70, 0x21, 0x8e, 0xc9, 0x0f, 0x26, 0x93, 0xb8, - 0x26, 0x71, 0xe0, 0xf7, 0x61, 0x89, 0x54, 0x88, 0x0c, 0xea, 0x58, 0xb9, 0x1d, 0xfb, 0x0b, 0x40, 0x8f, 0x87, 0x4c, - 0xe7, 0x81, 0x2f, 0x58, 0xe0, 0x38, 0xa8, 0x66, 0x37, 0x87, 0x8a, 0x05, 0xc0, 0x85, 0x59, 0x29, 0x5c, 0x8c, 0xd6, - 0x28, 0xc5, 0xec, 0x19, 0x1f, 0xda, 0x55, 0x03, 0x96, 0x99, 0x77, 0x66, 0xe5, 0xdc, 0x5a, 0x48, 0xc1, 0xed, 0xfa, - 0x46, 0x4c, 0x70, 0xbb, 0x46, 0xb2, 0x2d, 0xea, 0xd7, 0xe0, 0x58, 0x5c, 0x5c, 0xd7, 0xf8, 0xac, 0xcc, 0xdc, 0x49, - 0xfb, 0xc4, 0x75, 0x94, 0x56, 0x20, 0x89, 0xe7, 0x79, 0x18, 0x89, 0x05, 0xd3, 0xe7, 0x84, 0xa8, 0xc4, 0xb0, 0xf4, - 0xb1, 0xec, 0x0c, 0x83, 0xc7, 0x1c, 0x1d, 0x79, 0x66, 0xe7, 0x1c, 0xfe, 0xc7, 0x05, 0x60, 0x59, 0x7c, 0x2a, 0xe3, - 0x5f, 0x1c, 0x8f, 0xb2, 0x27, 0xf2, 0xfe, 0x4a, 0xe2, 0x4e, 0xc5, 0x1c, 0x48, 0x23, 0x5b, 0xc6, 0xd2, 0x16, 0xc8, - 0x45, 0xc6, 0x33, 0xec, 0xfc, 0xd4, 0xfa, 0x98, 0xfd, 0xd8, 0xc7, 0xaa, 0xe1, 0xd7, 0x81, 0x6e, 0x93, 0x12, 0xf4, - 0xad, 0x94, 0xe9, 0xec, 0xbd, 0x99, 0xd2, 0xdc, 0x89, 0xab, 0x7a, 0x65, 0x6b, 0x1b, 0x6a, 0x9b, 0xc4, 0xf5, 0x5b, - 0xf3, 0x18, 0x98, 0xb6, 0x4e, 0x5c, 0x19, 0x0a, 0x6d, 0xb2, 0x3c, 0xd3, 0x20, 0x55, 0x31, 0x74, 0xf7, 0x8a, 0x0f, - 0x9d, 0xee, 0x70, 0x36, 0x5f, 0x9a, 0xf4, 0x30, 0x9e, 0xc5, 0xb5, 0x5c, 0x92, 0xc1, 0x07, 0x85, 0xc3, 0x21, 0x49, - 0xd1, 0x22, 0x97, 0x21, 0x80, 0xdc, 0xed, 0xe0, 0x6e, 0xb2, 0xdd, 0x94, 0x77, 0xcc, 0x5e, 0x9a, 0xa3, 0xcf, 0xdb, - 0x72, 0x31, 0xa1, 0x46, 0x4c, 0xd5, 0x79, 0x6b, 0xbb, 0x6e, 0x0a, 0x4a, 0x39, 0x0a, 0xa4, 0x53, 0x16, 0xa2, 0x82, - 0x9f, 0x98, 0xef, 0xff, 0xa0, 0x28, 0x37, 0x04, 0xdc, 0xf2, 0x3a, 0x7e, 0xdc, 0x69, 0x2d, 0x63, 0x58, 0x8e, 0x8c, - 0x0b, 0xd3, 0xbf, 0xa4, 0x59, 0xcd, 0x96, 0x65, 0xe2, 0x75, 0x9d, 0x3d, 0x28, 0x2e, 0xe1, 0x5c, 0xad, 0x65, 0xe1, - 0x3a, 0xd2, 0xd0, 0x84, 0xfe, 0x10, 0x0a, 0xdb, 0xa6, 0x32, 0x70, 0xa2, 0x94, 0x21, 0x3f, 0x97, 0x86, 0x29, 0x18, - 0x7e, 0x13, 0x60, 0x9d, 0x66, 0x18, 0x85, 0xb4, 0x00, 0xaa, 0x0f, 0x47, 0x93, 0x6e, 0x08, 0x3b, 0x07, 0x1d, 0x47, - 0xe9, 0xec, 0xc0, 0x7a, 0x40, 0xce, 0xf3, 0xd9, 0x5e, 0xef, 0xcd, 0xfa, 0xda, 0xf8, 0x07, 0x04, 0x3e, 0xf3, 0x2d, - 0xfa, 0xda, 0x06, 0xe2, 0x7c, 0x39, 0x23, 0xc6, 0xb6, 0x0c, 0xd8, 0x52, 0xe5, 0x10, 0xb6, 0x54, 0x0c, 0x13, 0x33, - 0x75, 0x62, 0x8a, 0x17, 0x65, 0xdd, 0x79, 0x3e, 0x04, 0x2c, 0x50, 0x7e, 0xc0, 0x91, 0x25, 0xc7, 0x74, 0x14, 0x29, - 0x3a, 0x0d, 0x14, 0x2c, 0x50, 0x7e, 0x7d, 0x5b, 0xfe, 0x61, 0x08, 0xb0, 0x1c, 0x69, 0x95, 0x81, 0x64, 0x6a, 0x63, - 0x39, 0xa9, 0xc5, 0xa9, 0x38, 0x8b, 0xca, 0x30, 0xfa, 0xdd, 0xb8, 0x78, 0xe9, 0xbd, 0x56, 0x6f, 0xe1, 0x29, 0x57, - 0xb0, 0x46, 0x13, 0xd3, 0x13, 0xb1, 0xbf, 0xe0, 0x7c, 0x30, 0xc8, 0x6f, 0x78, 0x77, 0x08, 0xa9, 0x8d, 0x62, 0x8f, - 0xda, 0x0f, 0x4c, 0x46, 0xa5, 0xd5, 0x25, 0x2f, 0xea, 0x45, 0xb6, 0x65, 0x17, 0xbb, 0x72, 0x8f, 0x81, 0xcb, 0x8b, - 0x11, 0xe8, 0xf1, 0xf6, 0x1a, 0x1c, 0x00, 0x1f, 0x2d, 0x8a, 0xab, 0x61, 0x5b, 0xa4, 0x40, 0xd8, 0xd6, 0x7b, 0xde, - 0xea, 0x53, 0x2b, 0xc8, 0x63, 0x10, 0x5a, 0x97, 0x13, 0xde, 0xb9, 0x75, 0xca, 0x90, 0x16, 0x31, 0xce, 0xb3, 0xa8, - 0xd0, 0x87, 0x49, 0x55, 0xc9, 0x86, 0x7f, 0xc0, 0x60, 0xe4, 0x16, 0x53, 0xe1, 0xdf, 0xe2, 0xaf, 0xcc, 0x0d, 0xf7, - 0x6a, 0x98, 0xce, 0xa9, 0x36, 0xef, 0xba, 0xed, 0xf0, 0xc3, 0xf0, 0xdd, 0x12, 0x7a, 0x54, 0x60, 0x9c, 0xe6, 0x89, - 0xd9, 0x1a, 0x7e, 0xa5, 0x80, 0x6f, 0x1f, 0xca, 0xb4, 0x0d, 0x37, 0xd3, 0xaa, 0xbd, 0xe9, 0xb6, 0x1b, 0x40, 0xe6, - 0xac, 0x66, 0xf9, 0xe6, 0x83, 0x3b, 0x09, 0x69, 0x11, 0xfe, 0x58, 0x26, 0xea, 0x11, 0xb6, 0x74, 0xe8, 0x04, 0x3c, - 0xd3, 0xd3, 0xaa, 0xc6, 0xf3, 0x75, 0x56, 0x22, 0x7f, 0xb4, 0x37, 0xfe, 0xe4, 0x83, 0xb7, 0xbe, 0x83, 0x1a, 0x79, - 0xa2, 0x47, 0x84, 0x0b, 0xd5, 0x25, 0xb4, 0xad, 0x1a, 0xb2, 0x28, 0x96, 0xdc, 0x06, 0xde, 0x13, 0x53, 0x84, 0xc3, - 0x4f, 0xed, 0xe9, 0x52, 0xd4, 0xfe, 0x98, 0x19, 0xfc, 0x07, 0x80, 0x44, 0xe5, 0xf2, 0xbf, 0xc3, 0xe3, 0x1d, 0x85, - 0x88, 0x78, 0x0b, 0xc9, 0x82, 0x05, 0x18, 0x79, 0xa8, 0xcc, 0x48, 0x4a, 0xca, 0xb5, 0x12, 0x80, 0xef, 0xc3, 0xd0, - 0x56, 0x5d, 0x83, 0x1c, 0x6c, 0xf0, 0xb7, 0x0c, 0xe2, 0x61, 0xd7, 0x23, 0xad, 0xf1, 0xf2, 0xf8, 0xd2, 0xa7, 0x9a, - 0xd0, 0xe2, 0xdb, 0x48, 0x59, 0xbc, 0x5c, 0x3d, 0x10, 0x1d, 0x49, 0x0c, 0x71, 0x23, 0x27, 0xc9, 0x9b, 0xc4, 0xfb, - 0x69, 0x63, 0x44, 0x72, 0x62, 0x9d, 0xbd, 0x20, 0xe5, 0x17, 0x62, 0xf3, 0xdd, 0xb8, 0x73, 0xb8, 0x73, 0xbd, 0xaf, - 0x94, 0x45, 0x5d, 0x8b, 0x7a, 0x68, 0x76, 0x1d, 0xfd, 0xd9, 0x94, 0x30, 0xa4, 0x43, 0xa2, 0x41, 0x21, 0x2d, 0x2a, - 0x0b, 0xa4, 0x81, 0x9e, 0x44, 0xf6, 0x71, 0x58, 0xcc, 0xde, 0xbd, 0x4a, 0x7d, 0x92, 0x48, 0x49, 0x6c, 0x0f, 0x58, - 0x9a, 0x4c, 0xbc, 0xb9, 0x30, 0xfb, 0xbf, 0xb2, 0xf3, 0xf2, 0x21, 0xd2, 0x98, 0xaa, 0x63, 0x64, 0xa1, 0x06, 0x4a, - 0x59, 0x0b, 0xa7, 0x2d, 0xbe, 0x14, 0x45, 0x5b, 0x85, 0x9e, 0x6a, 0x1e, 0x78, 0x5a, 0x58, 0x13, 0xc5, 0x16, 0xf4, - 0x74, 0x98, 0x96, 0x25, 0xb5, 0x09, 0x4f, 0x5f, 0x7a, 0x9e, 0xe5, 0x39, 0xdb, 0x5d, 0x9a, 0x7d, 0xeb, 0xa0, 0x5e, - 0x53, 0xcb, 0xf6, 0x53, 0x95, 0x69, 0xd0, 0x12, 0x04, 0xf5, 0x10, 0xe4, 0x56, 0x61, 0xe2, 0xc6, 0x38, 0x4f, 0x77, - 0xed, 0xd6, 0x9d, 0xf9, 0xa7, 0x5d, 0x10, 0x17, 0x12, 0x18, 0x34, 0x12, 0xad, 0x26, 0xf4, 0x63, 0xc3, 0x52, 0x18, - 0x72, 0xb6, 0x64, 0x96, 0xf3, 0x6a, 0x20, 0x3f, 0xd3, 0x56, 0x70, 0x40, 0xc2, 0xe8, 0x1c, 0x63, 0x66, 0xf0, 0x39, - 0x12, 0xc3, 0x57, 0x6d, 0xd2, 0x73, 0x24, 0xf7, 0x34, 0xc1, 0x54, 0x00, 0xf3, 0x4a, 0xc1, 0x74, 0xd6, 0x37, 0x8b, - 0x0a, 0x56, 0xfc, 0xf0, 0xe3, 0x2f, 0xa8, 0xde, 0x07, 0x05, 0x9d, 0x04, 0x57, 0xea, 0xb6, 0x9d, 0xf1, 0x6d, 0xf7, - 0x41, 0x01, 0x5e, 0xa8, 0x21, 0xf3, 0x12, 0xff, 0x57, 0x2f, 0xd6, 0xd4, 0x2f, 0xf2, 0xd9, 0x61, 0xa2, 0xef, 0x32, - 0x69, 0xe6, 0xf7, 0xa5, 0x01, 0x65, 0x7e, 0xc9, 0xe3, 0x8a, 0x69, 0xde, 0x23, 0xfe, 0xd3, 0x98, 0xdb, 0xc2, 0x84, - 0x76, 0x98, 0x3e, 0x4a, 0xd4, 0xdc, 0x3e, 0x13, 0x54, 0xfb, 0x86, 0x97, 0xea, 0x31, 0x17, 0xac, 0x63, 0x72, 0x4b, - 0x89, 0xf5, 0x95, 0xc0, 0x83, 0x2c, 0x92, 0x89, 0x7b, 0xa9, 0xf6, 0x8e, 0xf2, 0x7c, 0xa7, 0xf6, 0x34, 0x39, 0x61, - 0x5d, 0x5c, 0x5d, 0xc9, 0xd7, 0x31, 0xc2, 0x6e, 0xbd, 0x59, 0x5e, 0xab, 0x62, 0xcc, 0x28, 0xd9, 0xd4, 0x6e, 0xef, - 0x62, 0x31, 0xe3, 0x26, 0x0c, 0x45, 0xb6, 0x28, 0x97, 0x8f, 0x5c, 0x3c, 0xe4, 0xfb, 0x94, 0x5f, 0xfd, 0x67, 0x0b, - 0x71, 0xf3, 0xf9, 0xf9, 0x1b, 0x23, 0x2c, 0x08, 0x03, 0xdb, 0xad, 0x22, 0x3e, 0x9d, 0x09, 0x14, 0xc6, 0xc6, 0x04, - 0x9b, 0xd7, 0xba, 0x09, 0xbc, 0x48, 0x94, 0x91, 0x34, 0xcc, 0xcf, 0xf2, 0x10, 0xa8, 0x62, 0xe8, 0x49, 0x6b, 0x25, - 0x8a, 0xd6, 0xf7, 0x63, 0x9f, 0x01, 0x21, 0x55, 0xb2, 0xac, 0x88, 0x2b, 0x57, 0x28, 0x04, 0x22, 0x09, 0x07, 0x47, - 0x60, 0x9b, 0x26, 0x84, 0x4f, 0x0f, 0xe9, 0xa5, 0x2e, 0x73, 0xc9, 0xc5, 0x35, 0x38, 0x0a, 0x60, 0x69, 0x32, 0xe2, - 0xd7, 0xbb, 0x55, 0x5e, 0xfa, 0xa5, 0x9d, 0x6e, 0xfe, 0x9e, 0x03, 0x8e, 0x0b, 0xdd, 0x17, 0x05, 0x68, 0x0d, 0x58, - 0x56, 0x28, 0x6f, 0x1f, 0x83, 0x8b, 0xd2, 0x61, 0xf4, 0x72, 0x5c, 0x2d, 0xa2, 0xba, 0x42, 0x59, 0xbb, 0x5d, 0x11, - 0x95, 0xb7, 0xf3, 0xd7, 0x34, 0xa9, 0x45, 0x04, 0x71, 0xde, 0x47, 0x34, 0xcb, 0x44, 0x98, 0x5d, 0xdc, 0x75, 0xa8, - 0xc7, 0x90, 0xf4, 0xa1, 0x15, 0x17, 0x11, 0xf8, 0xb4, 0x02, 0x69, 0x63, 0x6e, 0x0f, 0xe9, 0xb7, 0xb6, 0xa3, 0x00, - 0xe8, 0x85, 0xb0, 0x90, 0xb9, 0x91, 0x14, 0x3c, 0x7b, 0x0f, 0x54, 0x92, 0xf4, 0xb9, 0x1a, 0xb3, 0xae, 0xc7, 0x17, - 0xaf, 0x95, 0xbe, 0x05, 0x79, 0x6f, 0x8a, 0xe0, 0xe9, 0xaa, 0xbd, 0x74, 0x21, 0xa0, 0xbd, 0xce, 0x74, 0xb6, 0x34, - 0x7b, 0x1f, 0xbc, 0x17, 0x1d, 0x78, 0x0d, 0xf5, 0x66, 0x89, 0x3c, 0x66, 0xf0, 0x65, 0x93, 0x90, 0xe4, 0xb5, 0x91, - 0x0a, 0xa2, 0xa0, 0x07, 0xae, 0x51, 0x91, 0x8c, 0x92, 0x8b, 0x6e, 0xfb, 0xb3, 0x19, 0xa4, 0x5c, 0x5e, 0x7d, 0xcd, - 0xdb, 0x9d, 0x83, 0x28, 0xa5, 0xf9, 0xeb, 0x85, 0x4f, 0xbb, 0x67, 0x74, 0xe5, 0x35, 0x81, 0x56, 0x33, 0x7a, 0x4b, - 0x8d, 0x6a, 0xa4, 0xa9, 0x48, 0x05, 0xb1, 0x77, 0x59, 0x83, 0xb5, 0xf1, 0x78, 0x30, 0x95, 0x1a, 0xbc, 0xcf, 0xf4, - 0xa4, 0x75, 0xfa, 0xf6, 0x69, 0x39, 0x84, 0x57, 0xdf, 0x6d, 0x37, 0x2a, 0xf5, 0x5c, 0x7c, 0x2f, 0xdb, 0x45, 0xa6, - 0x75, 0x9c, 0xe8, 0x64, 0xd2, 0x57, 0x69, 0xb7, 0x27, 0x55, 0x3d, 0x73, 0xe1, 0x00, 0xfd, 0xbb, 0x51, 0xa6, 0x94, - 0xa5, 0xf1, 0xda, 0x25, 0xac, 0xa7, 0xde, 0x08, 0xbb, 0x2e, 0x0a, 0xc0, 0x3f, 0x67, 0x1c, 0x68, 0xa2, 0x63, 0xc5, - 0x3a, 0xbe, 0x2e, 0x75, 0x3c, 0x94, 0xfc, 0x80, 0x6d, 0x66, 0x92, 0x77, 0x28, 0x6e, 0xde, 0x10, 0xd8, 0x9f, 0x29, - 0xd6, 0x76, 0xab, 0xc4, 0x19, 0xab, 0xd8, 0x2b, 0xb1, 0xd7, 0x1e, 0x6f, 0x98, 0x40, 0x99, 0xad, 0x2b, 0x4c, 0x99, - 0x33, 0xbf, 0xc9, 0x67, 0x2f, 0xaf, 0x6f, 0x9e, 0xfd, 0x65, 0xe7, 0x35, 0xc3, 0xb0, 0x92, 0x3d, 0x27, 0xcb, 0x21, - 0xac, 0xca, 0xf8, 0x99, 0x9e, 0x6a, 0x1e, 0xdf, 0x51, 0x6f, 0xdc, 0x9b, 0xa4, 0x07, 0xe3, 0xdb, 0x13, 0x92, 0x87, - 0x82, 0x09, 0x18, 0xe9, 0xcf, 0x47, 0xa3, 0x00, 0x9d, 0x5c, 0x76, 0x0d, 0x2e, 0x12, 0x93, 0x3f, 0xa6, 0x5e, 0xc6, - 0x22, 0x05, 0xa9, 0x3a, 0xc2, 0x5d, 0x83, 0x48, 0x8a, 0x2a, 0xe8, 0xc8, 0x8b, 0x02, 0x4c, 0x5a, 0x70, 0x60, 0x01, - 0x0a, 0x3a, 0x5f, 0x79, 0x89, 0x25, 0x7e, 0x88, 0xfe, 0xf1, 0x1f, 0x56, 0x27, 0xbd, 0x68, 0xfe, 0x31, 0xbd, 0xf0, - 0xff, 0x4f, 0xe7, 0xb3, 0x75, 0x8e, 0x0d, 0x08, 0xd6, 0xff, 0x05, 0x62, 0x17, 0x9a, 0xa0, 0x04, 0xe9, 0x01, 0x84, - 0x43, 0xfe, 0xf4, 0x4e, 0x33, 0x9c, 0xb0, 0x7c, 0xaa, 0x26, 0xc3, 0x78, 0x2a, 0xce, 0xc9, 0x84, 0xc6, 0x71, 0x1a, - 0x4d, 0x29, 0xc0, 0x33, 0x6e, 0xcc, 0x5e, 0xc3, 0xd0, 0x7b, 0xdd, 0xa3, 0x40, 0xe8, 0x24, 0xdd, 0xcc, 0x58, 0x8a, - 0x49, 0xb4, 0xac, 0x57, 0x6d, 0x9c, 0x1c, 0xf6, 0xe0, 0x0c, 0xb4, 0xcc, 0x32, 0x27, 0x5a, 0x0f, 0x16, 0x42, 0x73, - 0x6e, 0x6c, 0x30, 0xdd, 0x5b, 0x28, 0xdd, 0x11, 0x41, 0x63, 0xf7, 0x98, 0xca, 0x56, 0x44, 0x25, 0xa5, 0x88, 0x78, - 0x63, 0x20, 0x4c, 0x2f, 0x7e, 0x9d, 0x9a, 0x53, 0xdf, 0xf6, 0x51, 0xbe, 0x32, 0xa9, 0x94, 0xb5, 0x5c, 0x48, 0x83, - 0xf1, 0xea, 0xcb, 0x48, 0x08, 0xcd, 0x44, 0x48, 0x91, 0x17, 0xb2, 0x27, 0x60, 0x13, 0x23, 0xbe, 0xc7, 0xa5, 0x84, - 0x32, 0x39, 0x28, 0x55, 0x50, 0x3c, 0x3e, 0xd4, 0x8f, 0x18, 0x10, 0xba, 0x1a, 0x23, 0x89, 0xe5, 0x58, 0xe8, 0x27, - 0xd2, 0x17, 0x34, 0x71, 0x08, 0x5c, 0xe3, 0x07, 0xd1, 0x67, 0x4f, 0xc6, 0xcb, 0x5e, 0x81, 0xcd, 0x39, 0xb0, 0x89, - 0xdc, 0x1c, 0xb4, 0xee, 0x3f, 0x65, 0x02, 0x4d, 0x14, 0x59, 0xeb, 0x39, 0x4b, 0xf6, 0x2c, 0xc5, 0x3c, 0x54, 0x4b, - 0x96, 0x40, 0xa3, 0xa8, 0x88, 0xd0, 0x5f, 0x0e, 0xf6, 0x50, 0xcf, 0x2a, 0x23, 0xb6, 0x30, 0xaf, 0x4e, 0xa8, 0xb2, - 0xd5, 0x51, 0xd2, 0x2b, 0x0b, 0xf5, 0x70, 0x37, 0x80, 0xf1, 0xb5, 0x9f, 0x7b, 0xe3, 0xce, 0xfa, 0x84, 0x6d, 0xcf, - 0x37, 0xb9, 0x38, 0xfe, 0x7a, 0xe6, 0xe5, 0xe1, 0xe0, 0xb9, 0x67, 0xb1, 0xd9, 0x50, 0x09, 0x81, 0x48, 0x26, 0x82, - 0x4a, 0xf5, 0xdb, 0x6c, 0x38, 0x20, 0xb0, 0x83, 0x62, 0x2b, 0xe1, 0xa5, 0x52, 0x51, 0xee, 0xad, 0xd5, 0x58, 0x0e, - 0x1c, 0x8e, 0x21, 0x8d, 0x63, 0x97, 0x98, 0x82, 0x38, 0xd6, 0x67, 0xe9, 0x21, 0xf0, 0x6b, 0x6b, 0x34, 0x4f, 0xec, - 0x90, 0x21, 0xd3, 0xa4, 0x4a, 0x45, 0xfa, 0x39, 0x00, 0x6e, 0x23, 0xa7, 0x17, 0xe9, 0x1f, 0x50, 0x02, 0xfc, 0x2a, - 0x16, 0x7b, 0xa3, 0x32, 0x16, 0xc9, 0xaa, 0xac, 0x56, 0x0a, 0xa7, 0xe3, 0x03, 0xf0, 0xd2, 0xbf, 0x2c, 0x58, 0xa0, - 0x6a, 0xa4, 0x67, 0x0b, 0x77, 0x08, 0xd4, 0x4a, 0xdf, 0x8d, 0x10, 0xb5, 0xee, 0xd7, 0xf5, 0xa8, 0xba, 0xb9, 0x58, - 0x8c, 0x31, 0xb6, 0xdc, 0x9b, 0x4f, 0x2a, 0xb7, 0x6d, 0x31, 0xfa, 0x36, 0x77, 0x60, 0x6b, 0xd2, 0x3d, 0xfd, 0xb0, - 0x3d, 0xe5, 0xe1, 0x25, 0x7e, 0xf3, 0x6a, 0x22, 0x33, 0x79, 0x7c, 0x26, 0x74, 0xef, 0xa9, 0x42, 0xcd, 0x8d, 0x85, - 0x3c, 0xf5, 0xbb, 0xf7, 0x34, 0xe1, 0x87, 0xfa, 0xaf, 0xd4, 0x78, 0x52, 0x93, 0x93, 0xbf, 0x99, 0xc5, 0x64, 0x13, - 0x86, 0xfd, 0xb0, 0x81, 0x20, 0x10, 0x50, 0x25, 0x80, 0xed, 0x51, 0x94, 0xeb, 0x6f, 0x4a, 0xaa, 0x5b, 0xc0, 0x85, - 0xa2, 0x53, 0x51, 0xf7, 0x29, 0xe1, 0x59, 0xcf, 0xb6, 0x5b, 0x28, 0x22, 0x2e, 0xaa, 0x33, 0x71, 0xa2, 0xcd, 0xcf, - 0xd9, 0xfe, 0x51, 0x75, 0x40, 0x87, 0x2c, 0x3e, 0xea, 0x9a, 0x50, 0x10, 0x37, 0xbc, 0x50, 0x86, 0x09, 0x92, 0x08, - 0x65, 0xd3, 0x6c, 0xf7, 0x65, 0xba, 0xc6, 0xad, 0xfd, 0xb9, 0xd0, 0xf7, 0x76, 0x20, 0x12, 0x17, 0x33, 0xd1, 0xac, - 0x08, 0x57, 0xf2, 0xe0, 0x54, 0xd1, 0xe6, 0x2c, 0xc8, 0xd6, 0xec, 0xad, 0x9e, 0x93, 0x39, 0xe0, 0x28, 0xd2, 0x72, - 0xcc, 0x8f, 0x3c, 0x17, 0xcf, 0xd5, 0x67, 0x8b, 0xe4, 0x7e, 0xc9, 0xc6, 0xb6, 0xdd, 0x4b, 0xd2, 0x93, 0x1c, 0x60, - 0x22, 0x04, 0xdf, 0x94, 0x90, 0x0e, 0xc0, 0xfa, 0xda, 0xb2, 0x04, 0x99, 0xa9, 0x02, 0x22, 0x93, 0xe9, 0xfc, 0xda, - 0x93, 0x83, 0xa0, 0x33, 0x15, 0x70, 0xc0, 0x10, 0x7e, 0x06, 0x06, 0x1a, 0xdf, 0x73, 0x90, 0xa4, 0x44, 0x43, 0x32, - 0xc5, 0x3d, 0x20, 0x52, 0xc0, 0xcd, 0x37, 0x4f, 0xb6, 0x68, 0x5d, 0x19, 0xa7, 0x90, 0x5e, 0xd2, 0x7a, 0x30, 0x25, - 0x31, 0x9e, 0x49, 0xb8, 0xc5, 0xf1, 0xcf, 0x96, 0xc0, 0xeb, 0x44, 0x5e, 0xa5, 0x64, 0x4b, 0xce, 0x09, 0x0c, 0x2e, - 0x47, 0x2b, 0x36, 0xb0, 0xb8, 0x7e, 0x0a, 0x6b, 0x8c, 0x27, 0x59, 0x1e, 0x8b, 0x9b, 0xbf, 0xef, 0xbf, 0xf5, 0xc8, - 0xee, 0xb3, 0xdd, 0x9d, 0x4f, 0x4d, 0x6b, 0x05, 0x83, 0x18, 0x53, 0xb2, 0x01, 0x6a, 0x05, 0xcf, 0x6c, 0xc8, 0xd8, - 0x95, 0xe6, 0x59, 0xe0, 0xa8, 0x67, 0xbb, 0x6f, 0xcd, 0xee, 0x58, 0xbe, 0x41, 0xfc, 0x44, 0x43, 0xd8, 0x78, 0xa7, - 0x02, 0x9b, 0xa8, 0xe5, 0xf7, 0xe8, 0xb7, 0xbb, 0xb3, 0x1d, 0xda, 0xab, 0x78, 0x37, 0x8d, 0x67, 0x19, 0x53, 0x59, - 0x96, 0xbf, 0x8f, 0x9e, 0xf9, 0xd1, 0xd1, 0x12, 0x86, 0xfa, 0xc8, 0x39, 0x4e, 0xd3, 0x38, 0xe8, 0x08, 0xf5, 0x84, - 0x1d, 0x7f, 0x87, 0x88, 0xff, 0xef, 0x0c, 0xff, 0x77, 0xf0, 0xbf, 0xe7, 0xf8, 0x17, 0xa1, 0xae, 0xf3, 0xd5, 0xe6, - 0xaf, 0x28, 0xff, 0x8d, 0x8f, 0x64, 0x9d, 0xbf, 0xda, 0xdb, 0x7d, 0x61, 0x5a, 0x6f, 0x92, 0x47, 0x6b, 0x13, 0x05, - 0xf4, 0xd5, 0xe3, 0xce, 0xe9, 0x04, 0x51, 0xe6, 0xd4, 0xf9, 0x88, 0xf2, 0xfb, 0x6e, 0x70, 0xe1, 0x17, 0xe3, 0x06, - 0x96, 0xad, 0x2f, 0xff, 0xf4, 0xaf, 0x90, 0xcb, 0x7d, 0x7e, 0xce, 0xb5, 0xad, 0x68, 0x94, 0xfd, 0x7d, 0x68, 0xdc, - 0x11, 0x41, 0x0d, 0xe7, 0xfe, 0xf3, 0x44, 0xf3, 0x4f, 0x06, 0x97, 0x22, 0x47, 0x6b, 0x85, 0xc5, 0x67, 0xfe, 0x4c, - 0x2b, 0x7b, 0xdf, 0xb8, 0x75, 0x65, 0x1b, 0x5a, 0x8c, 0x36, 0xdd, 0x00, 0x83, 0x49, 0x05, 0xad, 0x95, 0xb5, 0xfb, - 0xfc, 0x97, 0x89, 0x31, 0x24, 0x3c, 0xfa, 0xd9, 0xaf, 0x81, 0xfa, 0x8d, 0x6b, 0xb3, 0x06, 0x12, 0x5b, 0xa6, 0xe7, - 0x2f, 0x05, 0xb3, 0x09, 0x0d, 0x0f, 0xf5, 0x74, 0xa9, 0xf7, 0xea, 0xb3, 0x48, 0x34, 0xc6, 0xed, 0x50, 0x5c, 0x5f, - 0x01, 0x0a, 0xa4, 0x38, 0x4f, 0x97, 0xff, 0xc7, 0x5f, 0x88, 0xa2, 0xeb, 0x12, 0x3a, 0x2a, 0xe7, 0xa4, 0xe6, 0xa7, - 0xd0, 0x4b, 0x54, 0x7a, 0xa9, 0x12, 0x75, 0x54, 0x4c, 0x3c, 0x0b, 0x82, 0xeb, 0x8a, 0x9c, 0x9b, 0x5c, 0x0d, 0xe7, - 0x64, 0xdc, 0xe7, 0xff, 0x38, 0xcb, 0x49, 0x82, 0x87, 0xc0, 0x8c, 0xef, 0xec, 0x0a, 0x61, 0x49, 0x5e, 0xc3, 0xc1, - 0xec, 0xc1, 0xb0, 0x7c, 0x52, 0x03, 0x0d, 0x86, 0xf9, 0xf3, 0x05, 0x24, 0xe0, 0x1b, 0xdf, 0x0c, 0x52, 0xa3, 0xe2, - 0xa9, 0x3d, 0xad, 0x43, 0x95, 0x36, 0x06, 0x86, 0x21, 0x11, 0xc1, 0x21, 0xe7, 0x00, 0xf1, 0x41, 0xa7, 0xb3, 0xa3, - 0x8d, 0x63, 0x26, 0xa7, 0xe4, 0xa0, 0x1f, 0xe7, 0x52, 0x15, 0xca, 0xa1, 0x96, 0xd4, 0x51, 0xd1, 0x90, 0xe3, 0x32, - 0x3a, 0xd0, 0xa2, 0xdb, 0x2b, 0xc8, 0xfa, 0x32, 0x17, 0xef, 0xb2, 0xd9, 0xea, 0x91, 0x08, 0x33, 0x89, 0x18, 0xa9, - 0xe0, 0xb4, 0x64, 0x55, 0x0c, 0xfd, 0xa2, 0x25, 0xa6, 0x36, 0xe2, 0xe9, 0xde, 0x5a, 0x3a, 0x75, 0x1e, 0x79, 0x70, - 0x65, 0x90, 0xbd, 0xe2, 0x35, 0xe0, 0xde, 0x24, 0x1b, 0x66, 0x19, 0x2b, 0xb8, 0xb0, 0xbe, 0xf6, 0x69, 0xf5, 0xc1, - 0xbe, 0x43, 0xc1, 0x79, 0xf6, 0x5e, 0x0c, 0xba, 0x19, 0xec, 0x82, 0xea, 0x8d, 0xfd, 0x3c, 0x0d, 0xf8, 0xdd, 0x03, - 0xa5, 0xa0, 0xc0, 0x31, 0xa8, 0xa7, 0x3b, 0x7f, 0x43, 0x04, 0x7e, 0x60, 0x37, 0x60, 0x9d, 0xb3, 0x6d, 0xc1, 0x95, - 0x84, 0x6a, 0x20, 0xd5, 0x65, 0x2c, 0x0b, 0x6c, 0xa7, 0x87, 0x2d, 0xae, 0x7b, 0x8e, 0x06, 0xa5, 0xba, 0x37, 0x13, - 0x94, 0x89, 0xa3, 0x65, 0xae, 0x03, 0x5b, 0x04, 0x30, 0xb2, 0x15, 0xe1, 0x36, 0x20, 0xaf, 0x2e, 0x66, 0x34, 0xf4, - 0x87, 0xb8, 0x9a, 0x04, 0x34, 0xa0, 0x91, 0x8b, 0x8e, 0x17, 0x5d, 0x2f, 0x9d, 0xd2, 0xdb, 0xe3, 0x09, 0x72, 0xb0, - 0x73, 0x16, 0x86, 0xf2, 0x7a, 0x92, 0xf3, 0xa5, 0xe7, 0xbf, 0x9e, 0x10, 0xed, 0xf5, 0x31, 0x86, 0xb3, 0x85, 0x94, - 0xc4, 0x06, 0x32, 0xb4, 0x3a, 0x8e, 0xe8, 0xe0, 0x21, 0x0f, 0xe8, 0x49, 0x69, 0x96, 0xd7, 0x4d, 0x88, 0x8a, 0xb9, - 0x08, 0x95, 0xc8, 0xcb, 0xe2, 0x9a, 0xad, 0x2b, 0x10, 0x98, 0xd9, 0x2e, 0xf8, 0x82, 0xa3, 0xd1, 0xca, 0xf0, 0x82, - 0x30, 0x27, 0x8c, 0x90, 0xc5, 0xfa, 0x27, 0xc0, 0x1b, 0xbe, 0x06, 0x45, 0xfe, 0x64, 0x5c, 0x12, 0x9e, 0x59, 0xe6, - 0x0e, 0xe1, 0x84, 0x9f, 0x2a, 0x61, 0x75, 0x17, 0x15, 0xfa, 0xc7, 0xf3, 0x52, 0x50, 0x1c, 0x35, 0x06, 0x0c, 0xfb, - 0x22, 0x04, 0x91, 0x8a, 0xd2, 0xec, 0x3b, 0x88, 0x54, 0xf7, 0x03, 0xb1, 0x1e, 0x8e, 0x68, 0xc3, 0x23, 0x11, 0xc8, - 0x1f, 0x49, 0xb5, 0xd0, 0xc6, 0x47, 0x22, 0x24, 0xbd, 0x7d, 0xf3, 0xc1, 0x79, 0xb9, 0x29, 0xaa, 0x2c, 0xa3, 0xd0, - 0x27, 0x3a, 0xa8, 0x9c, 0xea, 0xf1, 0x7a, 0x96, 0xda, 0x0a, 0xae, 0x5b, 0x2b, 0x81, 0x8a, 0x8d, 0xe9, 0x32, 0xf7, - 0x91, 0xed, 0xb5, 0x4b, 0x29, 0xf1, 0xe7, 0x79, 0xc6, 0x1a, 0x8e, 0x04, 0xec, 0xa6, 0x17, 0x86, 0xb1, 0x93, 0x1d, - 0x43, 0x47, 0x92, 0xf4, 0x82, 0xd2, 0x74, 0x8c, 0x91, 0x9b, 0xd7, 0xdd, 0x60, 0xbb, 0x8a, 0xc1, 0x57, 0x03, 0xc3, - 0x39, 0x34, 0x69, 0x38, 0x29, 0x38, 0xd7, 0xad, 0xfb, 0xeb, 0x78, 0xe9, 0x3c, 0x2f, 0xfb, 0x18, 0x96, 0x47, 0x3c, - 0xd7, 0xf6, 0x5f, 0x2f, 0xe4, 0xf9, 0x7a, 0x09, 0xcb, 0xd6, 0x5f, 0x93, 0xe4, 0x08, 0xd9, 0xcf, 0x2a, 0xa2, 0xe6, - 0x17, 0x8c, 0x43, 0xe4, 0x4f, 0x71, 0x4c, 0x15, 0xcd, 0x5c, 0x75, 0x7a, 0x54, 0x7a, 0x83, 0x5e, 0x3c, 0x3c, 0x20, - 0x38, 0x09, 0x90, 0x27, 0x4e, 0x42, 0x18, 0x30, 0xe4, 0x14, 0xd6, 0x7c, 0x05, 0x3c, 0xe6, 0x71, 0xc3, 0x53, 0x9a, - 0xab, 0x3f, 0x01, 0x34, 0xa0, 0x02, 0xea, 0xc3, 0x0e, 0x5f, 0x2c, 0xc1, 0x86, 0x46, 0x08, 0x21, 0x9f, 0x10, 0xfc, - 0x2e, 0xff, 0x4a, 0x50, 0x16, 0x3b, 0x05, 0x32, 0x5a, 0xa7, 0xda, 0x15, 0x93, 0x95, 0x36, 0xa8, 0xff, 0x96, 0x76, - 0x53, 0x5e, 0x10, 0x39, 0x88, 0x50, 0x01, 0x06, 0xb2, 0xda, 0x50, 0xe0, 0xd5, 0x65, 0x9b, 0x69, 0x06, 0x63, 0xf5, - 0x51, 0xd3, 0x66, 0xef, 0xf6, 0x4a, 0x4f, 0x65, 0x40, 0xd4, 0x06, 0x86, 0x77, 0xfd, 0xcf, 0xe1, 0x69, 0x19, 0xcf, - 0x8b, 0x41, 0x35, 0x4e, 0x87, 0xc8, 0x70, 0x9d, 0x3a, 0x56, 0xad, 0xfc, 0xcf, 0x94, 0x12, 0xe3, 0x53, 0x51, 0xe1, - 0xc0, 0xba, 0x6a, 0xa8, 0x5a, 0x10, 0xa6, 0x8d, 0xd2, 0x3f, 0x28, 0xca, 0xa0, 0x05, 0x58, 0x1a, 0xb5, 0xc6, 0x01, - 0x9b, 0x9a, 0x5e, 0x9e, 0x1b, 0xd3, 0x89, 0x57, 0xfb, 0x9b, 0xc0, 0x84, 0xbb, 0xfa, 0x31, 0x87, 0xba, 0xb6, 0x78, - 0xae, 0xef, 0xbb, 0x8f, 0xa7, 0x3c, 0x24, 0x7a, 0x26, 0x16, 0x9a, 0xb2, 0xb9, 0xce, 0xe7, 0x1d, 0x14, 0x1b, 0x5e, - 0xd8, 0xe7, 0x2b, 0x24, 0xc8, 0x01, 0x87, 0x4d, 0x71, 0x35, 0x0e, 0xcf, 0x38, 0x27, 0x2a, 0x7d, 0x2e, 0xf6, 0x83, - 0x03, 0x38, 0x3c, 0xe7, 0x4a, 0x59, 0xcb, 0xe7, 0x6f, 0xd3, 0x19, 0x4f, 0xfa, 0x12, 0x9c, 0x94, 0x32, 0xf1, 0x8a, - 0xc5, 0x9f, 0xf1, 0xf1, 0x11, 0x67, 0x78, 0x7f, 0xa3, 0xf3, 0x69, 0x7f, 0x9b, 0xae, 0x8d, 0x79, 0x0f, 0xf2, 0x46, - 0x05, 0xcf, 0x5d, 0x58, 0xc6, 0x36, 0x51, 0x40, 0xf0, 0x37, 0x3a, 0xa7, 0x69, 0x1e, 0x42, 0x3c, 0xac, 0xa2, 0x22, - 0xbf, 0xa3, 0x48, 0x96, 0x68, 0xe1, 0x6d, 0x4e, 0x8b, 0xf4, 0x6a, 0x7a, 0xde, 0xe6, 0x4b, 0x99, 0x0e, 0xed, 0xc6, - 0xd4, 0x49, 0xb4, 0xaa, 0x30, 0x21, 0x88, 0xc0, 0x9d, 0x2e, 0x91, 0x60, 0xb7, 0x26, 0xb3, 0x27, 0xfc, 0x6b, 0x4c, - 0xe3, 0x20, 0x6e, 0xd9, 0x73, 0x8b, 0x7d, 0x4f, 0xcd, 0x1c, 0xe8, 0x85, 0x9c, 0xa1, 0x38, 0x64, 0x0c, 0xfa, 0x42, - 0xae, 0x1f, 0x8f, 0xd0, 0x49, 0x05, 0xf8, 0x55, 0xc9, 0xaf, 0x7a, 0xbc, 0x96, 0x0a, 0x95, 0xea, 0x6c, 0x22, 0x47, - 0xfb, 0xb3, 0xb4, 0xf1, 0x18, 0xfb, 0xb1, 0x3c, 0x7d, 0xed, 0xe9, 0x04, 0x50, 0xb1, 0x25, 0xb7, 0xb2, 0x6f, 0xc8, - 0x43, 0x4b, 0xbf, 0x79, 0x9c, 0x61, 0xe6, 0x08, 0xd0, 0xd9, 0xaa, 0xe0, 0xa9, 0x8b, 0xd9, 0xdb, 0x5b, 0x6e, 0x2a, - 0xb6, 0xdf, 0xf0, 0x19, 0x60, 0x27, 0x89, 0x40, 0x34, 0xaa, 0xf6, 0x59, 0xa7, 0x31, 0x79, 0x2a, 0x9e, 0x42, 0x23, - 0x60, 0x29, 0x22, 0xd7, 0x94, 0x68, 0x5d, 0xcb, 0xd0, 0x45, 0x72, 0x7c, 0xbb, 0x1c, 0x82, 0x04, 0x77, 0x68, 0xd6, - 0x48, 0xe8, 0xa9, 0xba, 0x62, 0xae, 0x54, 0x95, 0x50, 0x77, 0x3a, 0x4c, 0x79, 0x6e, 0xac, 0xf0, 0x28, 0x73, 0xd1, - 0xb9, 0x8e, 0x55, 0x25, 0xc2, 0x22, 0x2e, 0xce, 0x02, 0x2f, 0x02, 0xfc, 0x08, 0x0c, 0xe2, 0x52, 0x95, 0x65, 0xa6, - 0x08, 0x49, 0x93, 0x6d, 0x81, 0xb1, 0xe2, 0xd0, 0x6f, 0xa2, 0x9a, 0x07, 0x5f, 0xff, 0x34, 0xa1, 0x28, 0xec, 0x04, - 0x44, 0xa3, 0x41, 0xb7, 0x16, 0xd0, 0xcc, 0xfc, 0x9b, 0x72, 0x78, 0x0c, 0x9d, 0x27, 0xf1, 0x86, 0x25, 0xc9, 0xa2, - 0x3f, 0xff, 0x97, 0x06, 0x61, 0xd2, 0x3b, 0x90, 0x52, 0x95, 0x40, 0xbb, 0x61, 0x6e, 0x3e, 0x89, 0x0c, 0xd9, 0xa2, - 0x5c, 0xec, 0x71, 0x94, 0x73, 0x1b, 0xd2, 0x0a, 0x66, 0x5e, 0x42, 0xc9, 0x3b, 0x5a, 0xaf, 0xbc, 0x0e, 0xab, 0x6e, - 0x79, 0x8d, 0x97, 0x38, 0xd1, 0x2f, 0x58, 0x74, 0x4d, 0xae, 0xc1, 0x6d, 0x42, 0x03, 0x32, 0x2b, 0x13, 0xd5, 0x1b, - 0x82, 0xa7, 0x90, 0xb2, 0x31, 0xe0, 0xe2, 0xdc, 0xe0, 0xd1, 0xf9, 0x9c, 0x90, 0xb2, 0x90, 0x0b, 0xcd, 0x00, 0x27, - 0x31, 0x92, 0xd1, 0xa6, 0x2e, 0xe5, 0x42, 0x9d, 0x7e, 0xe0, 0xad, 0x83, 0x1e, 0xd6, 0x66, 0x67, 0x2b, 0xf6, 0xb2, - 0x5e, 0x31, 0xb2, 0xa6, 0xea, 0x72, 0x52, 0x6e, 0x4a, 0x68, 0x68, 0x3f, 0xc6, 0x7b, 0x19, 0x87, 0x1c, 0x66, 0xd5, - 0x18, 0x89, 0x3d, 0x23, 0x39, 0xb3, 0x49, 0x92, 0x65, 0xd6, 0x65, 0x2d, 0xfd, 0x6c, 0xae, 0xfd, 0x8f, 0x6c, 0xe1, - 0x11, 0xcb, 0x38, 0xbf, 0xd2, 0x43, 0x49, 0xb8, 0xb2, 0x4e, 0x93, 0xe7, 0xe9, 0x07, 0xe1, 0xba, 0xdb, 0xd4, 0x8c, - 0x86, 0xf7, 0x80, 0x84, 0x2c, 0x6d, 0xd9, 0xaa, 0x36, 0x36, 0x36, 0xf8, 0xb9, 0xcf, 0x03, 0x0b, 0xf1, 0xa0, 0x21, - 0x19, 0xfd, 0x7c, 0x9b, 0xc7, 0x86, 0x85, 0xce, 0xe8, 0xa0, 0x94, 0x5e, 0x4b, 0x71, 0xee, 0x05, 0x4a, 0x2c, 0xf8, - 0x72, 0xaf, 0xa4, 0xc9, 0x99, 0x07, 0x7e, 0x10, 0x67, 0x46, 0x17, 0x99, 0x77, 0xbe, 0x46, 0xdd, 0x4a, 0xef, 0x95, - 0x96, 0xf4, 0x83, 0x5f, 0xfd, 0x6c, 0x95, 0x5e, 0xa7, 0x5f, 0x22, 0x73, 0xe6, 0x2b, 0x5c, 0xa2, 0x5d, 0x81, 0x1d, - 0xc6, 0x49, 0x5d, 0xf3, 0xeb, 0x0e, 0xd0, 0xfb, 0xc2, 0x9b, 0x81, 0x46, 0x89, 0xc0, 0x0b, 0x4d, 0x2e, 0x71, 0x25, - 0xbe, 0x10, 0xde, 0x14, 0x7b, 0x98, 0xc1, 0x44, 0x6c, 0x14, 0x41, 0x3c, 0x00, 0xff, 0x1c, 0xe2, 0x97, 0x31, 0xbf, - 0xd7, 0x41, 0x6d, 0xb4, 0x20, 0x34, 0x45, 0xe6, 0x26, 0x7b, 0x11, 0x5b, 0x90, 0xc3, 0x2b, 0x81, 0x04, 0xb9, 0x66, - 0x8d, 0x1d, 0xb0, 0x65, 0x5b, 0xa1, 0x6c, 0xde, 0x70, 0x6c, 0xb0, 0x3b, 0x7b, 0x69, 0x6d, 0x12, 0x84, 0xb8, 0x44, - 0xd4, 0x9a, 0x1e, 0x19, 0xe5, 0xab, 0x96, 0x82, 0x4a, 0xf4, 0xf3, 0xf4, 0xbf, 0xb1, 0x49, 0x6f, 0x17, 0x04, 0xfd, - 0x52, 0x64, 0xda, 0x2f, 0x34, 0x0c, 0x4a, 0x8b, 0xbc, 0xff, 0x43, 0x09, 0x7c, 0x1d, 0xac, 0xe9, 0x3a, 0xd5, 0x6e, - 0x7d, 0xf1, 0x4f, 0x50, 0x3d, 0x4f, 0xc6, 0x1d, 0x46, 0x2e, 0x0c, 0x68, 0x7e, 0x6f, 0x24, 0x9d, 0xb7, 0xfe, 0xcf, - 0x12, 0x02, 0x67, 0x90, 0x25, 0x14, 0xae, 0x11, 0xf9, 0x26, 0xff, 0xc0, 0x30, 0xed, 0x15, 0xc1, 0xce, 0x8e, 0xbc, - 0x37, 0xb6, 0x93, 0x75, 0x88, 0x36, 0xf4, 0x0d, 0x98, 0xb2, 0x7e, 0xa3, 0x59, 0x36, 0x40, 0x6d, 0x3b, 0xe3, 0xb6, - 0x2b, 0x69, 0x16, 0x92, 0xe1, 0x99, 0xc5, 0x20, 0xd5, 0x46, 0x7e, 0xe6, 0x95, 0x85, 0x33, 0x73, 0x77, 0xa1, 0x91, - 0x9f, 0x3d, 0x9d, 0xe1, 0xf0, 0xc6, 0x46, 0x7a, 0xe1, 0x09, 0x4b, 0x73, 0x43, 0x07, 0x00, 0x20, 0x5c, 0x2a, 0x3a, - 0xde, 0xb1, 0x54, 0xd9, 0x4a, 0xd4, 0x57, 0x82, 0xe6, 0xe0, 0x38, 0x1b, 0x5d, 0xc8, 0x27, 0x4c, 0x18, 0xe9, 0x79, - 0x0e, 0x11, 0x8f, 0x63, 0xe5, 0x32, 0x18, 0x32, 0x07, 0x3f, 0x91, 0x60, 0x47, 0xb3, 0xc3, 0x59, 0x0e, 0x57, 0xa9, - 0x5f, 0x27, 0xa8, 0x86, 0xd4, 0x58, 0x65, 0x6c, 0xc3, 0xfa, 0xff, 0x0a, 0x27, 0x2e, 0xeb, 0x79, 0x42, 0x2f, 0x3a, - 0x2c, 0xdc, 0xc2, 0x47, 0x2e, 0xf9, 0x87, 0x38, 0x38, 0xc4, 0xd9, 0x18, 0xe7, 0x19, 0x48, 0x9e, 0x36, 0x50, 0x18, - 0x79, 0x39, 0xbe, 0x54, 0x72, 0xb6, 0x1b, 0xaa, 0x4a, 0x4a, 0x97, 0x74, 0xab, 0xbd, 0x67, 0xb2, 0x1f, 0x91, 0x13, - 0x27, 0x45, 0x24, 0x99, 0x4c, 0x2a, 0xa8, 0x53, 0x1a, 0x6c, 0xfa, 0x15, 0xc0, 0x08, 0xe6, 0x5a, 0xd7, 0x34, 0x45, - 0x69, 0x99, 0x70, 0xf8, 0x1c, 0x2d, 0xd6, 0x99, 0x43, 0xd1, 0xc1, 0x60, 0x50, 0x48, 0x33, 0xb4, 0x0b, 0x3c, 0xc8, - 0xa8, 0xcc, 0x1e, 0x7f, 0x9e, 0x9f, 0x14, 0x4d, 0xd8, 0x64, 0xc5, 0x2a, 0x51, 0xa4, 0x96, 0xb1, 0xa4, 0x54, 0x75, - 0xfe, 0xed, 0x3e, 0x1a, 0x1a, 0x1d, 0x58, 0x03, 0x3a, 0x0a, 0x25, 0x9c, 0xf7, 0x50, 0x3c, 0xe9, 0x4e, 0xcd, 0xde, - 0x7e, 0xeb, 0x77, 0x57, 0x1f, 0xe2, 0x54, 0xdf, 0x25, 0x9d, 0xdc, 0x37, 0x9e, 0xb0, 0x9d, 0xb1, 0xd7, 0xba, 0xbe, - 0x63, 0x0b, 0x60, 0x97, 0x19, 0xcf, 0xc6, 0xf0, 0xbe, 0x8e, 0xbd, 0xf8, 0x82, 0x5c, 0x3d, 0x7f, 0xd6, 0x6a, 0xf9, - 0x8c, 0x5b, 0x0a, 0xa7, 0x1c, 0xa1, 0x85, 0x45, 0x40, 0x76, 0xc0, 0x28, 0x20, 0x2f, 0xa1, 0xcb, 0x18, 0xc2, 0x83, - 0x5f, 0x1b, 0x14, 0xad, 0x0e, 0x38, 0x13, 0xe8, 0x51, 0x3f, 0xe8, 0xc1, 0xdf, 0x74, 0x12, 0x6f, 0x4c, 0xbc, 0xb7, - 0x68, 0x4a, 0xbf, 0x5a, 0x69, 0x53, 0xa6, 0x3c, 0xbb, 0xe4, 0xc3, 0xd8, 0x0e, 0x55, 0x5b, 0xbb, 0x0d, 0x35, 0xc4, - 0x67, 0xb8, 0x9f, 0xb8, 0xa2, 0x78, 0x6c, 0x06, 0xea, 0x6f, 0xfc, 0xc8, 0x3b, 0x6d, 0x3f, 0x92, 0xfb, 0x10, 0x87, - 0x1e, 0xcb, 0xbe, 0x2a, 0xfb, 0x00, 0xae, 0xd9, 0xd9, 0x58, 0x89, 0x9f, 0x15, 0x61, 0xb8, 0x1c, 0x82, 0xf1, 0x67, - 0xb5, 0x0d, 0x3d, 0xa9, 0xa7, 0xac, 0x79, 0xfc, 0x3a, 0x32, 0xbf, 0xc9, 0xc2, 0xa4, 0x5e, 0xc8, 0x9a, 0x1e, 0xa7, - 0x33, 0x43, 0xe7, 0x4c, 0xe4, 0xee, 0xd9, 0xaf, 0xd1, 0xda, 0xc8, 0x48, 0xed, 0x6c, 0xd1, 0xc7, 0xdf, 0x35, 0x55, - 0x36, 0x9a, 0x34, 0xe3, 0xb1, 0x73, 0xad, 0x4b, 0x97, 0xba, 0x8c, 0x4c, 0xf7, 0x67, 0xbd, 0x58, 0x50, 0xa5, 0x55, - 0x66, 0xde, 0x27, 0x52, 0xc3, 0xb8, 0xd6, 0x6a, 0xe2, 0x9d, 0x5e, 0x7c, 0xcb, 0x8e, 0xdd, 0x74, 0x96, 0x64, 0x30, - 0x98, 0x8e, 0xdc, 0x0c, 0x1d, 0xc9, 0x08, 0x53, 0xbf, 0x7c, 0x32, 0x30, 0xeb, 0x3a, 0x7f, 0x6f, 0x5f, 0x33, 0x8e, - 0xe2, 0x5b, 0x8f, 0x15, 0xfd, 0xb6, 0x4e, 0xb7, 0xef, 0x09, 0x70, 0x6d, 0x53, 0xfb, 0xd4, 0x5b, 0xa5, 0x9c, 0x8d, - 0xe4, 0x58, 0x4e, 0xe2, 0x09, 0x14, 0x3c, 0x00, 0x49, 0x04, 0x4c, 0x9a, 0xf9, 0xc5, 0x52, 0xc9, 0x84, 0x8d, 0xba, - 0xdb, 0xef, 0x15, 0x17, 0x18, 0xb5, 0x7f, 0x94, 0xb9, 0x9a, 0x5e, 0x8e, 0x14, 0x25, 0xd3, 0x81, 0x81, 0xa6, 0x30, - 0x96, 0x3e, 0xff, 0x62, 0x5b, 0x89, 0x05, 0x95, 0x45, 0xb1, 0xb0, 0x66, 0x74, 0x00, 0x22, 0xe8, 0x63, 0x2a, 0xa8, - 0x1a, 0x7e, 0x43, 0xca, 0xe7, 0xf4, 0xeb, 0xd9, 0xf9, 0x88, 0x6d, 0xba, 0x29, 0x7d, 0x8c, 0x07, 0xab, 0xec, 0x75, - 0x7e, 0x1f, 0x9f, 0xfa, 0x42, 0x2f, 0x5e, 0x49, 0xde, 0x59, 0xf7, 0x4f, 0xf3, 0xcf, 0xe7, 0x6c, 0xfe, 0xf9, 0xac, - 0x19, 0x60, 0x98, 0xd9, 0x18, 0xf7, 0x2a, 0x7a, 0xb5, 0xc0, 0x35, 0x4e, 0x65, 0x78, 0x1a, 0xcb, 0x7f, 0x74, 0x16, - 0xae, 0x32, 0xc0, 0x99, 0x6c, 0xa0, 0x4d, 0x65, 0x10, 0x7c, 0x73, 0xc3, 0x82, 0xa6, 0x2b, 0x27, 0x26, 0x9a, 0x99, - 0x48, 0x5c, 0x82, 0x9c, 0xc4, 0x1c, 0xec, 0xc9, 0xa5, 0xea, 0x3a, 0x25, 0x33, 0x7e, 0x66, 0xda, 0x82, 0x9e, 0xa0, - 0xf0, 0x53, 0x90, 0xa9, 0x00, 0x41, 0x74, 0xbe, 0x4f, 0xf3, 0x38, 0x4a, 0x7f, 0xe7, 0x98, 0xa7, 0xfc, 0xff, 0x89, - 0x2c, 0x11, 0xa9, 0x6b, 0x19, 0xc2, 0x01, 0xbf, 0x46, 0x34, 0x75, 0xc6, 0xef, 0xc5, 0xa0, 0x7e, 0x91, 0xde, 0xaa, - 0x11, 0x38, 0x17, 0xa0, 0xae, 0x46, 0x98, 0x06, 0xf2, 0x8e, 0xf6, 0x1a, 0xf9, 0xc5, 0xa9, 0xd2, 0x2d, 0x7b, 0x5a, - 0xf2, 0x8a, 0x2c, 0x54, 0xfe, 0xc9, 0x98, 0x62, 0x56, 0x15, 0x1d, 0x47, 0x9a, 0xf7, 0x0d, 0x36, 0xc9, 0x17, 0x4e, - 0x12, 0x7a, 0xca, 0xa9, 0xb1, 0x30, 0x6e, 0x15, 0x5e, 0xda, 0x85, 0xd1, 0x07, 0x64, 0x0e, 0x34, 0x17, 0x65, 0x3f, - 0x0e, 0x71, 0x44, 0x7c, 0xa0, 0x9f, 0xf1, 0x2d, 0x64, 0xdc, 0xf6, 0x1c, 0x27, 0xc4, 0xa9, 0xfa, 0x62, 0x28, 0x5e, - 0xc6, 0x26, 0x15, 0x92, 0x1b, 0xb2, 0x18, 0xca, 0xaa, 0x85, 0xe7, 0x67, 0xf0, 0x7c, 0xe1, 0xf9, 0x69, 0x9e, 0x1b, - 0xbc, 0x25, 0x53, 0x51, 0x46, 0x62, 0xed, 0x0a, 0x3b, 0x6a, 0xf9, 0x4d, 0x5e, 0x61, 0xef, 0x73, 0x00, 0x12, 0xd5, - 0x5e, 0x15, 0x0b, 0x72, 0x01, 0x1b, 0xd0, 0xa0, 0x3a, 0x0c, 0xf2, 0x12, 0x5f, 0x0a, 0x20, 0x00, 0xa3, 0x07, 0xef, - 0xd9, 0x71, 0x3a, 0x6d, 0x0c, 0xe3, 0x2e, 0xc7, 0x04, 0x4a, 0xae, 0xd9, 0x54, 0x12, 0xf2, 0x26, 0x27, 0x9c, 0x7d, - 0x01, 0x2a, 0x84, 0x31, 0x80, 0x3c, 0x35, 0xb6, 0x58, 0xbd, 0x7e, 0x2b, 0xd5, 0xe7, 0x04, 0xa3, 0xb0, 0x97, 0x98, - 0x7d, 0xa1, 0x1b, 0x11, 0x11, 0x39, 0x8b, 0x39, 0xb9, 0xf4, 0xda, 0x55, 0xea, 0xfb, 0xd1, 0xd9, 0x27, 0x04, 0xf4, - 0xed, 0xe4, 0x5b, 0x73, 0x13, 0x0e, 0xc7, 0x97, 0x2c, 0x33, 0xd1, 0x7f, 0xae, 0x16, 0xc8, 0xb3, 0x32, 0xe7, 0x3d, - 0x57, 0xc7, 0xae, 0x89, 0x34, 0xc5, 0xa6, 0xa0, 0x53, 0x70, 0x4a, 0x10, 0x41, 0x5b, 0x70, 0x92, 0xe4, 0x90, 0x88, - 0xca, 0x40, 0x7f, 0x36, 0x15, 0x4b, 0x30, 0x5b, 0xc3, 0x52, 0x9d, 0xd7, 0x5a, 0xec, 0x9a, 0x1f, 0xb2, 0x27, 0x99, - 0x65, 0xc3, 0x45, 0xea, 0x4c, 0x27, 0xc5, 0x75, 0x64, 0x8d, 0xc2, 0xd4, 0xb8, 0x8b, 0xc5, 0xab, 0x84, 0x2b, 0xa9, - 0xdc, 0xa4, 0x4a, 0xbc, 0xd9, 0xa8, 0x19, 0x8f, 0xc6, 0xe5, 0x8f, 0x6d, 0x76, 0xf4, 0x46, 0x4a, 0xc0, 0xe3, 0x21, - 0xd5, 0xde, 0xa6, 0x33, 0x6e, 0x43, 0xfe, 0xed, 0xea, 0x69, 0x0b, 0x58, 0xfc, 0x4f, 0x7a, 0x18, 0xe2, 0xff, 0x8d, - 0x0a, 0x8a, 0xc8, 0x36, 0x42, 0x78, 0x14, 0x02, 0x84, 0xfb, 0xc2, 0xc9, 0x0b, 0x62, 0x51, 0xc4, 0xa3, 0xb0, 0xf7, - 0x3a, 0x6b, 0xe5, 0x12, 0x16, 0x7f, 0x1f, 0x74, 0xff, 0x8e, 0x42, 0xe7, 0x5c, 0xaf, 0xf1, 0xa7, 0xe2, 0xc7, 0x1f, - 0x39, 0x76, 0x74, 0x28, 0xc2, 0x4d, 0x0c, 0xad, 0x04, 0xcf, 0xb6, 0x44, 0x75, 0xac, 0x0a, 0x46, 0x84, 0x30, 0x07, - 0xa9, 0x6a, 0xc1, 0x04, 0xbb, 0xf0, 0x13, 0x2c, 0x3e, 0x10, 0x4e, 0xff, 0x1b, 0x1a, 0xb7, 0x09, 0x61, 0x36, 0x58, - 0xe5, 0xa8, 0x93, 0x27, 0xf1, 0x6a, 0x9f, 0xf9, 0x23, 0xe3, 0xd6, 0x57, 0x5f, 0xa4, 0xd5, 0x48, 0xf6, 0x14, 0x33, - 0x38, 0xbc, 0x76, 0x4b, 0x99, 0x19, 0x9f, 0x23, 0x26, 0x55, 0xf3, 0xe4, 0x45, 0x41, 0xa9, 0xa1, 0xae, 0x59, 0x9e, - 0x0b, 0xf3, 0x9c, 0xe1, 0xb9, 0xc0, 0x60, 0x2c, 0xeb, 0xb2, 0xc6, 0x19, 0x9f, 0xc6, 0x56, 0x0c, 0x8c, 0x3b, 0x1e, - 0x0e, 0x7a, 0x8e, 0x39, 0x54, 0x80, 0x5e, 0x04, 0x6e, 0x22, 0x3e, 0xe9, 0x25, 0x70, 0xea, 0xd4, 0x86, 0xf3, 0xcc, - 0x5c, 0x8e, 0x98, 0xf2, 0x0c, 0xa7, 0x98, 0xd2, 0xef, 0xdc, 0x26, 0xd2, 0x6e, 0x7d, 0xad, 0xc9, 0x47, 0xc1, 0xad, - 0x7a, 0x3a, 0xac, 0x23, 0x1a, 0x97, 0x16, 0xc1, 0x93, 0x45, 0x79, 0xd8, 0xbf, 0xf1, 0x9c, 0x5f, 0x89, 0xb4, 0x93, - 0x52, 0x25, 0x3a, 0x9f, 0x79, 0x0e, 0x42, 0x48, 0x93, 0x96, 0xd0, 0xf6, 0xb9, 0x20, 0x25, 0xc6, 0xb5, 0x53, 0x8a, - 0xd8, 0xcd, 0xc1, 0xfe, 0xd7, 0x53, 0xb9, 0x48, 0xab, 0xeb, 0x87, 0x77, 0x8b, 0x85, 0xd8, 0x4e, 0xc8, 0x79, 0x2e, - 0x64, 0xef, 0xfd, 0x6b, 0x12, 0x5e, 0x27, 0xe5, 0x97, 0xfd, 0x20, 0x71, 0xef, 0x5c, 0x6a, 0xfe, 0x5f, 0x38, 0x6c, - 0x7a, 0xf4, 0xca, 0xc1, 0x6c, 0x33, 0x01, 0x93, 0xa3, 0x52, 0x55, 0xdf, 0x8f, 0x80, 0xd4, 0xf0, 0x30, 0x40, 0x59, - 0xc5, 0xfe, 0x41, 0xbd, 0x25, 0x00, 0xb8, 0xd4, 0xb3, 0xfd, 0xb0, 0xb4, 0xbf, 0xfb, 0x61, 0xab, 0x13, 0x2e, 0x57, - 0x4a, 0xfe, 0x1b, 0x18, 0x42, 0x97, 0x86, 0xe4, 0xb0, 0x75, 0xd6, 0x03, 0x21, 0x77, 0x9e, 0x73, 0x8a, 0x51, 0x5c, - 0x4b, 0xbe, 0x60, 0xfb, 0x91, 0x94, 0xd7, 0xc9, 0x7c, 0x7e, 0xb1, 0xb5, 0x82, 0xcf, 0x3a, 0xa9, 0xbf, 0xa8, 0x44, - 0xff, 0x05, 0xec, 0x3b, 0x88, 0x0f, 0xcf, 0x13, 0xab, 0xca, 0x0f, 0x1d, 0x87, 0x23, 0xac, 0xb0, 0x95, 0x2a, 0x0d, - 0xcd, 0x23, 0xd3, 0xc7, 0x94, 0x9c, 0x9a, 0x80, 0x71, 0x48, 0x72, 0xb6, 0x56, 0x91, 0x4b, 0x92, 0x6a, 0x16, 0xee, - 0xeb, 0x06, 0x9f, 0x84, 0xe9, 0x92, 0x56, 0xe1, 0x1e, 0xa0, 0x7f, 0x0c, 0x7a, 0xfe, 0xcf, 0x4a, 0x71, 0xc0, 0xb0, - 0xea, 0x85, 0x4c, 0xfc, 0x64, 0x00, 0xd7, 0x66, 0x9e, 0xcc, 0x71, 0xe3, 0x6d, 0xd4, 0x47, 0x6d, 0x4b, 0xc0, 0x4f, - 0xa2, 0x27, 0x8f, 0x61, 0x4c, 0x16, 0x86, 0x02, 0x77, 0x2e, 0xf5, 0x73, 0xb0, 0x70, 0xc3, 0x31, 0xfa, 0x86, 0xe0, - 0x5b, 0xfe, 0xfc, 0x0e, 0xd4, 0x71, 0x8c, 0x86, 0x2e, 0x8d, 0xa4, 0xf4, 0x3b, 0x14, 0x44, 0x1a, 0x48, 0xa0, 0x79, - 0xee, 0x6b, 0x28, 0x9c, 0x4e, 0x22, 0x3b, 0xc9, 0x11, 0xd6, 0xce, 0x82, 0x59, 0xab, 0x9c, 0xbe, 0x9e, 0xed, 0x3b, - 0x0c, 0xd0, 0xb5, 0xcb, 0xe9, 0x7d, 0xae, 0xbf, 0x95, 0xa9, 0x9f, 0x2b, 0x84, 0x96, 0x65, 0x9b, 0x9d, 0x43, 0x20, - 0x94, 0x6b, 0xf5, 0x34, 0x44, 0xc1, 0x10, 0xef, 0x74, 0xac, 0x6e, 0xd0, 0x47, 0x2a, 0x5a, 0xe7, 0xab, 0x0d, 0xda, - 0x7c, 0xab, 0xf0, 0xd2, 0xf5, 0xca, 0xf6, 0x34, 0x24, 0xc5, 0x34, 0x62, 0x38, 0xf8, 0x66, 0x42, 0x67, 0xf2, 0x3e, - 0x6e, 0x90, 0x4d, 0x9c, 0x21, 0x79, 0xb8, 0x8e, 0x58, 0xba, 0x4a, 0x58, 0x54, 0xb6, 0xf0, 0x74, 0x4a, 0x3b, 0x5c, - 0xdd, 0x15, 0xe1, 0xe6, 0x4c, 0x06, 0x6a, 0xb9, 0x8e, 0xbe, 0x89, 0xc4, 0x20, 0x07, 0x6c, 0xb9, 0x4e, 0x1f, 0x1d, - 0x55, 0xaa, 0x90, 0xe9, 0x76, 0xde, 0xbc, 0x86, 0x05, 0x87, 0x2d, 0x63, 0x29, 0x0d, 0x30, 0xf0, 0x5d, 0x7b, 0x79, - 0x5f, 0x6d, 0x76, 0x2a, 0x3c, 0xe6, 0xbc, 0x4b, 0x69, 0x91, 0x57, 0xa9, 0x7f, 0x6e, 0xe3, 0xb5, 0xf9, 0xd5, 0xdc, - 0x46, 0x17, 0xbc, 0x39, 0x27, 0x3a, 0xad, 0x6f, 0x31, 0x5e, 0x76, 0x88, 0x68, 0xe7, 0x3e, 0x97, 0x79, 0xba, 0x85, - 0xd4, 0x62, 0xcc, 0x3a, 0xc7, 0x4c, 0x4f, 0x4b, 0x8b, 0xf2, 0x11, 0x63, 0x50, 0xc9, 0x9d, 0xf2, 0xcd, 0xc8, 0x53, - 0x8d, 0xc2, 0x10, 0xab, 0x6d, 0xe8, 0x19, 0xb4, 0xd2, 0x4f, 0x34, 0xfb, 0xf6, 0x76, 0xb3, 0x06, 0xa8, 0xc4, 0x62, - 0x1f, 0x99, 0x65, 0xe1, 0xe3, 0x72, 0x07, 0xa1, 0x83, 0x34, 0xb7, 0x93, 0xc3, 0x38, 0x3d, 0x46, 0x2b, 0xf4, 0xbb, - 0xf6, 0x14, 0xb3, 0x80, 0x48, 0x52, 0x2f, 0x90, 0xce, 0x01, 0x77, 0x49, 0xfd, 0x59, 0x9c, 0x19, 0x61, 0x3e, 0x7a, - 0x29, 0xa1, 0xdc, 0x25, 0x94, 0x1b, 0xaf, 0xf3, 0x24, 0x00, 0x3e, 0x89, 0xb6, 0xd7, 0x56, 0x9c, 0xe6, 0x9c, 0xd7, - 0xb6, 0x08, 0xc3, 0xae, 0xeb, 0xc5, 0x48, 0x72, 0x33, 0x7b, 0x61, 0xcc, 0x18, 0x04, 0x22, 0xa8, 0xe8, 0xe6, 0x80, - 0xc9, 0x98, 0x3a, 0xc2, 0x41, 0xe7, 0x4f, 0xb2, 0xa9, 0xa6, 0xb4, 0x98, 0xda, 0xd1, 0xff, 0xce, 0x1e, 0xfc, 0xf0, - 0x68, 0x7a, 0xfe, 0xeb, 0xdb, 0xfc, 0x1a, 0x34, 0x41, 0x0f, 0xe1, 0x7e, 0x77, 0x35, 0x53, 0xa1, 0xa0, 0xb0, 0xb2, - 0x7d, 0x69, 0x01, 0x50, 0x63, 0x2a, 0x4a, 0x57, 0xd7, 0xd6, 0xfd, 0x49, 0xc6, 0xa7, 0x35, 0x6d, 0x7c, 0x1d, 0xa8, - 0xf2, 0xb5, 0xa1, 0x6c, 0xdd, 0xe1, 0xf3, 0x50, 0xcd, 0x78, 0x0a, 0xda, 0x5c, 0x18, 0xfc, 0x0a, 0x39, 0x6e, 0x42, - 0x27, 0x43, 0x95, 0x4d, 0xb3, 0x13, 0x6f, 0x59, 0x25, 0x6b, 0x29, 0x39, 0x91, 0x12, 0xf6, 0xaa, 0xf2, 0x47, 0xdd, - 0x93, 0xd4, 0x96, 0x6a, 0x70, 0x83, 0x13, 0x94, 0x36, 0x3a, 0xfa, 0x2a, 0x6e, 0xe4, 0xa7, 0x1b, 0x40, 0x44, 0x4d, - 0x4f, 0x87, 0xf6, 0x76, 0xfe, 0x79, 0x6f, 0x8c, 0xb0, 0x49, 0x05, 0x3f, 0xca, 0xa8, 0xa9, 0x1a, 0x9e, 0xfb, 0x53, - 0xaf, 0x6c, 0x87, 0x67, 0xba, 0x9b, 0x2c, 0x6a, 0x8b, 0x3e, 0xe3, 0x02, 0xac, 0x9a, 0x68, 0xaa, 0x71, 0xa1, 0x08, - 0x63, 0x1a, 0x6f, 0xce, 0xda, 0x89, 0xb5, 0xea, 0xd5, 0xed, 0xac, 0x97, 0x1e, 0x6d, 0xb3, 0x05, 0x6a, 0xbc, 0x68, - 0xda, 0xf2, 0x2a, 0x7c, 0xb7, 0xa4, 0x9b, 0x95, 0x23, 0xc8, 0xdc, 0x04, 0xe8, 0x67, 0x3f, 0x64, 0x26, 0x7a, 0x07, - 0xe1, 0x4e, 0x2a, 0xd9, 0x93, 0x8a, 0xcd, 0x78, 0xbe, 0xbd, 0x72, 0x7e, 0x5a, 0x92, 0x6d, 0xc4, 0xda, 0x8d, 0x2a, - 0xe3, 0xb1, 0x35, 0xc8, 0xdb, 0x35, 0xd3, 0x59, 0xa2, 0xbf, 0x86, 0x7b, 0xfd, 0xf9, 0x76, 0x0d, 0x4d, 0xb7, 0xd5, - 0xdc, 0x79, 0xe9, 0xe4, 0x28, 0x29, 0x79, 0xf1, 0x03, 0x7b, 0x6c, 0xe1, 0x7c, 0xb0, 0xf1, 0x90, 0x60, 0x99, 0x8a, - 0x55, 0x9f, 0x55, 0xac, 0xf2, 0x2c, 0x11, 0x85, 0xd9, 0xd3, 0x2e, 0x80, 0xe3, 0x0f, 0x2b, 0xb9, 0x8a, 0x1f, 0x66, - 0x5a, 0xb5, 0x7c, 0x58, 0x08, 0xd5, 0xf4, 0x24, 0x10, 0x19, 0x98, 0x35, 0xf2, 0x70, 0x61, 0xea, 0x98, 0x4c, 0x4a, - 0x49, 0x20, 0x57, 0x58, 0x4d, 0xab, 0x6a, 0xd5, 0x87, 0x1f, 0x6e, 0xb8, 0x41, 0x5d, 0xf9, 0x67, 0x33, 0xae, 0xff, - 0xaf, 0x99, 0x89, 0xe5, 0xa0, 0xb1, 0xd0, 0xfa, 0x27, 0x2d, 0xcc, 0xfd, 0x08, 0xdd, 0x35, 0xc3, 0x95, 0xe9, 0x4b, - 0x14, 0xf3, 0x2b, 0x46, 0x66, 0x5b, 0xe4, 0xb5, 0x32, 0xd8, 0xc5, 0xde, 0x98, 0x49, 0x5b, 0x27, 0xc6, 0x34, 0x36, - 0x24, 0x56, 0x67, 0x51, 0x23, 0x43, 0x6a, 0x6b, 0xf3, 0x9d, 0xbe, 0xa2, 0xf9, 0x25, 0x2f, 0xf1, 0x97, 0x69, 0xc8, - 0xc2, 0x7c, 0xab, 0xe8, 0x8d, 0xf4, 0x1a, 0x7a, 0x09, 0xfe, 0x62, 0xe1, 0xde, 0x04, 0x78, 0x8e, 0x22, 0x2a, 0x46, - 0x63, 0xf0, 0x4d, 0x16, 0x07, 0x7a, 0xbf, 0xc2, 0xad, 0x20, 0xc3, 0x94, 0x15, 0xff, 0x5f, 0x03, 0xad, 0xf4, 0x2f, - 0x20, 0xf6, 0x0d, 0xbe, 0xc0, 0xca, 0x5b, 0x41, 0xb9, 0x87, 0xe9, 0xfe, 0x02, 0xdf, 0x0a, 0x71, 0x30, 0x68, 0x44, - 0x4d, 0x58, 0xeb, 0x3d, 0xc5, 0x38, 0x31, 0x3d, 0xf8, 0x67, 0x7a, 0x68, 0x25, 0x08, 0x87, 0x31, 0x86, 0x0e, 0x52, - 0x80, 0x60, 0xa5, 0x7c, 0xf5, 0xf5, 0xa1, 0x55, 0xa1, 0x64, 0xe4, 0xab, 0x66, 0x83, 0x4f, 0xb1, 0x9d, 0xa7, 0xd8, - 0x3e, 0x2b, 0x5f, 0x5a, 0x6b, 0x4d, 0x67, 0xab, 0x46, 0x7a, 0xbe, 0x0a, 0x2e, 0x30, 0xec, 0x60, 0xe0, 0xbc, 0x0a, - 0xbe, 0x22, 0x41, 0x93, 0x40, 0x58, 0x40, 0x83, 0xe7, 0x36, 0x94, 0x93, 0x0f, 0x09, 0x94, 0xba, 0x04, 0xbb, 0xcd, - 0x8f, 0x48, 0x6e, 0x03, 0x21, 0xb5, 0x18, 0x67, 0x0b, 0x7b, 0x70, 0x26, 0xcd, 0xfa, 0xb9, 0xb1, 0x1e, 0x5a, 0x89, - 0x92, 0x36, 0x5d, 0x9e, 0x0d, 0xae, 0x19, 0x29, 0xac, 0x93, 0xff, 0x2f, 0x39, 0x6e, 0xf3, 0xbd, 0x2a, 0x08, 0xae, - 0xc4, 0x49, 0x5f, 0xeb, 0x29, 0x28, 0x77, 0x3a, 0x74, 0xe0, 0x8e, 0x20, 0x4b, 0xd9, 0xf3, 0xcc, 0xd3, 0xe7, 0x76, - 0x81, 0x9d, 0x98, 0xed, 0x9e, 0x95, 0xce, 0x70, 0xc2, 0xf1, 0xfb, 0x05, 0xef, 0x2e, 0xfd, 0x39, 0xa4, 0xdc, 0xdf, - 0x57, 0x96, 0x8c, 0x77, 0xc7, 0xdd, 0xb3, 0x3f, 0xc7, 0x1b, 0xb7, 0xfc, 0x29, 0xf7, 0xc3, 0x6d, 0x24, 0xdd, 0xf0, - 0xaa, 0xf9, 0x17, 0x8f, 0x6c, 0xe5, 0x56, 0x42, 0xd0, 0x3a, 0x9d, 0xe3, 0x9b, 0xaf, 0x41, 0xa7, 0x12, 0xb6, 0xf8, - 0xf1, 0x5d, 0xf2, 0x5b, 0x63, 0x2f, 0x46, 0x61, 0x70, 0x68, 0xa6, 0x76, 0x95, 0x9c, 0xb7, 0xe0, 0xb6, 0x4c, 0xed, - 0x56, 0x81, 0x34, 0x7a, 0xe7, 0xb9, 0xfa, 0x3a, 0xfd, 0x54, 0x35, 0xff, 0x31, 0x73, 0x13, 0xf6, 0xb1, 0x42, 0x91, - 0xf8, 0x67, 0x6a, 0x74, 0x83, 0x91, 0xca, 0x03, 0x75, 0x81, 0x55, 0x4b, 0x82, 0xca, 0xdb, 0x11, 0x3f, 0xe5, 0xd7, - 0xdc, 0xe5, 0x48, 0x6c, 0x84, 0xfd, 0x69, 0x6c, 0x3e, 0x53, 0x9f, 0xed, 0x6c, 0x99, 0x65, 0xb7, 0x8f, 0x99, 0x87, - 0x47, 0xf9, 0x5b, 0xaa, 0x5b, 0xe4, 0x7b, 0xb3, 0x2c, 0x2f, 0xca, 0xec, 0xd4, 0x3e, 0x87, 0x4f, 0xb4, 0xd1, 0x24, - 0x7f, 0xe3, 0x71, 0x2f, 0xb1, 0x21, 0xd9, 0x34, 0x2f, 0x33, 0x87, 0xd8, 0xc3, 0xf3, 0x1b, 0x3f, 0x65, 0x53, 0x99, - 0x28, 0x38, 0x6d, 0x87, 0xa6, 0x03, 0xf7, 0x0d, 0xd4, 0x41, 0x15, 0x20, 0xa7, 0x2b, 0xb0, 0xd1, 0x80, 0x59, 0x6d, - 0xd4, 0x97, 0xa5, 0xb2, 0x8a, 0xb3, 0xae, 0xdb, 0x75, 0xfa, 0xc5, 0x46, 0x12, 0xb8, 0xb1, 0x47, 0x91, 0x3c, 0x9d, - 0x95, 0xae, 0xf1, 0x57, 0x79, 0xc9, 0x1c, 0xa5, 0x0f, 0xf8, 0xfc, 0x5b, 0xf0, 0x48, 0xfe, 0x52, 0x74, 0x8d, 0xab, - 0xdc, 0x66, 0x34, 0x6a, 0xcc, 0xc9, 0x78, 0x43, 0xd8, 0x58, 0x14, 0x55, 0x31, 0x35, 0xa7, 0xfc, 0x9c, 0x19, 0x8d, - 0xe4, 0x05, 0x09, 0xf2, 0xb9, 0xb7, 0xfb, 0x99, 0x5c, 0xf0, 0x2b, 0xd7, 0xa1, 0x57, 0x56, 0xa3, 0xe2, 0x4b, 0xe7, - 0xb8, 0xb7, 0xee, 0x50, 0x80, 0x0c, 0xd8, 0x43, 0x86, 0x9c, 0xf2, 0x56, 0xd3, 0xbe, 0x5e, 0x7e, 0xff, 0x82, 0x91, - 0x14, 0xab, 0xb2, 0xef, 0xc6, 0x26, 0x4c, 0x22, 0x3b, 0xc2, 0x5e, 0x35, 0xb2, 0x9b, 0x63, 0x11, 0x21, 0x21, 0x19, - 0xf7, 0x4c, 0xc8, 0xe8, 0xad, 0x5e, 0x26, 0x80, 0x73, 0xe2, 0x49, 0xe1, 0x4c, 0x4e, 0x9c, 0xc8, 0xb2, 0xb4, 0x15, - 0x5b, 0x62, 0xe1, 0x08, 0x5f, 0x6b, 0xca, 0x6c, 0xdb, 0x18, 0x2b, 0x68, 0x70, 0xcc, 0x01, 0xa2, 0x33, 0x9f, 0xf1, - 0x86, 0x09, 0xe7, 0xfc, 0xa9, 0xf2, 0xbe, 0x59, 0xa6, 0x41, 0x5f, 0x15, 0x2c, 0x6c, 0xb7, 0x9e, 0x20, 0xca, 0x34, - 0x03, 0x03, 0xf2, 0x6d, 0x85, 0x6a, 0xfe, 0x95, 0x97, 0x28, 0xf8, 0xee, 0x32, 0xf2, 0xf3, 0xe7, 0x70, 0x1b, 0x21, - 0x1a, 0x04, 0x8d, 0xed, 0x85, 0x51, 0xb2, 0xb3, 0xac, 0x86, 0x5c, 0x84, 0x93, 0xe0, 0x83, 0xdd, 0x53, 0xb8, 0x3e, - 0x87, 0xa4, 0x97, 0xe0, 0x29, 0x30, 0x4f, 0x68, 0xa4, 0xb4, 0xdf, 0xf7, 0x2e, 0x08, 0x84, 0xe6, 0x2d, 0x8f, 0x70, - 0x40, 0x32, 0x62, 0x36, 0x16, 0x1e, 0x37, 0x0b, 0x97, 0x56, 0xc1, 0xa9, 0xcb, 0x6a, 0xd4, 0x15, 0xc9, 0x25, 0x85, - 0x34, 0x63, 0xf0, 0x60, 0x74, 0x24, 0x24, 0x96, 0x85, 0x60, 0xcc, 0x86, 0xbf, 0xf5, 0x02, 0xeb, 0xa7, 0x39, 0x00, - 0xf6, 0x04, 0xf1, 0xd8, 0x84, 0xe9, 0x0d, 0x44, 0x78, 0xb6, 0x2d, 0x0f, 0x98, 0xf7, 0x05, 0x1a, 0xcf, 0xf9, 0x98, - 0x52, 0xe6, 0x7c, 0x01, 0x2e, 0x33, 0xf1, 0x62, 0x20, 0x87, 0x80, 0x7b, 0x88, 0x87, 0xfc, 0xe0, 0xb1, 0x97, 0x60, - 0x67, 0x64, 0xa5, 0xbb, 0x5d, 0xbb, 0x71, 0xcc, 0x2d, 0x45, 0x0e, 0xbc, 0xd8, 0x3f, 0x38, 0xac, 0x6b, 0xb1, 0x4a, - 0xbf, 0x69, 0xa0, 0xd2, 0xbf, 0xfa, 0xf7, 0xcd, 0xe7, 0xc8, 0xb7, 0xcf, 0xb5, 0xd4, 0xa2, 0xf8, 0x81, 0x77, 0x56, - 0x93, 0x82, 0x76, 0xff, 0xab, 0x26, 0x23, 0x5f, 0x52, 0x5a, 0x2d, 0x8b, 0x4f, 0xb5, 0x8b, 0x9e, 0xa2, 0x5a, 0x36, - 0x79, 0x6e, 0x0f, 0x49, 0x8e, 0x5e, 0x6b, 0x19, 0x56, 0xb5, 0x3d, 0xea, 0x83, 0xbd, 0xd7, 0x7b, 0x41, 0x1e, 0x52, - 0x0f, 0x89, 0xaa, 0x37, 0x5e, 0x40, 0xc5, 0x7f, 0xf5, 0x95, 0xea, 0x99, 0x5f, 0xd0, 0x90, 0x37, 0xca, 0x31, 0xbe, - 0x6c, 0x9c, 0xb6, 0xae, 0x1e, 0xc5, 0x14, 0xac, 0x14, 0x65, 0x65, 0x88, 0x1b, 0x59, 0xa1, 0x6b, 0x44, 0x5a, 0x03, - 0x7f, 0x63, 0xa3, 0x14, 0x5b, 0x4d, 0xb5, 0x91, 0xf4, 0xdf, 0x99, 0xfe, 0x0f, 0x89, 0xec, 0xff, 0x14, 0xc7, 0xfe, - 0x8f, 0x73, 0x84, 0x16, 0xf6, 0x8d, 0x65, 0x44, 0xc0, 0x15, 0x4d, 0x8a, 0xe3, 0x2b, 0x83, 0xb3, 0x44, 0x50, 0xa3, - 0x89, 0xc8, 0x6e, 0x3c, 0x51, 0x99, 0x11, 0x79, 0x6e, 0x15, 0x3c, 0x4b, 0x7b, 0x74, 0x4f, 0xee, 0xef, 0x9c, 0x40, - 0x94, 0x63, 0x52, 0x6d, 0x6f, 0xc6, 0x9c, 0xc3, 0x10, 0x17, 0x93, 0x8b, 0x0a, 0x60, 0x04, 0x37, 0x84, 0x6c, 0x2c, - 0x81, 0x8e, 0x92, 0xec, 0x47, 0x23, 0xe6, 0x00, 0x34, 0xc0, 0x7e, 0xd9, 0x20, 0xb0, 0x1c, 0xcc, 0x30, 0x43, 0x30, - 0x3a, 0xaf, 0x0e, 0xf0, 0x39, 0x66, 0x7b, 0xc7, 0x26, 0xf8, 0xb0, 0x32, 0x07, 0x3b, 0xd0, 0x20, 0x1c, 0x30, 0x8f, - 0x66, 0x79, 0x26, 0x28, 0x9a, 0xe0, 0x23, 0xea, 0x2c, 0xfb, 0x5c, 0xfc, 0x92, 0xa5, 0xad, 0xd7, 0x25, 0x87, 0x2e, - 0x16, 0x9f, 0x66, 0xd6, 0x50, 0xfa, 0x13, 0xf8, 0x5f, 0x83, 0x3b, 0xb0, 0xc7, 0x1d, 0x24, 0xc2, 0x56, 0x14, 0x4e, - 0xa5, 0xea, 0x9f, 0x5d, 0x06, 0x84, 0x92, 0x9e, 0x66, 0x48, 0xb9, 0x40, 0xeb, 0x1a, 0xe2, 0x1a, 0x6c, 0x0c, 0x86, - 0xed, 0x8c, 0x67, 0x9a, 0xdb, 0xd6, 0x33, 0xe3, 0xa4, 0x5e, 0xa3, 0x4d, 0x46, 0x87, 0x64, 0x5e, 0x44, 0x99, 0xbb, - 0xc8, 0x2f, 0x47, 0x4a, 0xad, 0xb9, 0x51, 0xac, 0x31, 0xe5, 0xa5, 0xd3, 0xec, 0xc3, 0x39, 0x82, 0xd3, 0x45, 0x50, - 0xf5, 0xc3, 0x1e, 0x9c, 0xb5, 0x31, 0xf8, 0x91, 0x62, 0x51, 0x20, 0xbd, 0x5d, 0x11, 0x52, 0xf3, 0x93, 0x1d, 0xab, - 0x59, 0x4c, 0x4b, 0x6f, 0x17, 0x1e, 0xce, 0xb7, 0xc5, 0x14, 0x64, 0x1c, 0x08, 0xf9, 0x23, 0x18, 0xd8, 0x14, 0x77, - 0x66, 0xa2, 0x2d, 0x82, 0xe0, 0x04, 0xa1, 0x8c, 0x48, 0x87, 0x6f, 0x45, 0x29, 0x2a, 0x02, 0x91, 0xfb, 0xd1, 0x7b, - 0x4c, 0xcb, 0x6a, 0x28, 0xad, 0x81, 0x3d, 0x2a, 0x2a, 0x06, 0xca, 0xeb, 0x4c, 0x68, 0x38, 0x45, 0x8b, 0x90, 0xa9, - 0x78, 0xb3, 0x00, 0x2b, 0x37, 0xf8, 0xa4, 0xc5, 0x4a, 0xcf, 0x58, 0xf6, 0x07, 0xf9, 0x4a, 0x59, 0xd1, 0x30, 0x81, - 0xee, 0x31, 0x55, 0xdb, 0x7f, 0xe3, 0xa2, 0x8d, 0xb9, 0x01, 0x7e, 0x06, 0x8c, 0xe2, 0x7a, 0x85, 0x09, 0xab, 0x15, - 0xdc, 0x01, 0x2c, 0xbd, 0x71, 0x6f, 0xd5, 0x4c, 0xc9, 0xa7, 0x5c, 0x49, 0x29, 0x13, 0xac, 0x77, 0x2a, 0x4b, 0x70, - 0xf2, 0x21, 0x04, 0x43, 0x7c, 0xf7, 0x65, 0xe6, 0xd7, 0x6b, 0x9e, 0xda, 0x84, 0x27, 0xd1, 0x9e, 0xf6, 0xd1, 0x37, - 0x6e, 0xc3, 0xab, 0x16, 0x43, 0x5c, 0x9d, 0x65, 0xe7, 0xe4, 0x4b, 0x64, 0x4d, 0xe5, 0x80, 0x1f, 0xb1, 0x1a, 0xea, - 0x12, 0xf8, 0x8c, 0x98, 0x37, 0x0c, 0xe6, 0x6f, 0x74, 0x8a, 0xc5, 0xbc, 0xa9, 0x22, 0xc5, 0xe1, 0xfe, 0x08, 0x8b, - 0x8c, 0x4b, 0x94, 0x23, 0x7d, 0xc8, 0xbf, 0x83, 0xc1, 0xa8, 0xd2, 0xcd, 0x8a, 0xfa, 0x9a, 0xf1, 0xbc, 0xed, 0x63, - 0x6b, 0x12, 0xb6, 0x00, 0x6b, 0x91, 0xf1, 0x04, 0xe8, 0xbe, 0x56, 0x6f, 0x0b, 0xd9, 0x9a, 0x68, 0x89, 0x86, 0xa6, - 0x50, 0xd4, 0x0d, 0x04, 0x13, 0x93, 0xd2, 0xee, 0xc0, 0x48, 0x82, 0xf1, 0xac, 0xa9, 0xfc, 0x82, 0xfc, 0xb8, 0x5e, - 0xc4, 0xd6, 0x98, 0x0b, 0x1d, 0x33, 0xa2, 0x49, 0x4d, 0x9a, 0x09, 0x88, 0x05, 0xf0, 0x72, 0x16, 0x3d, 0xac, 0xf3, - 0x84, 0x53, 0x73, 0xef, 0xd4, 0x11, 0x33, 0x30, 0x40, 0xa7, 0x10, 0xa9, 0x14, 0x3f, 0xbc, 0x0f, 0x52, 0x0a, 0x04, - 0xa0, 0xec, 0x98, 0x0d, 0x2d, 0x29, 0xa8, 0x4f, 0x6b, 0x97, 0x99, 0x5a, 0xdb, 0x1a, 0xfe, 0x94, 0xd9, 0xac, 0x45, - 0x55, 0xcc, 0x1f, 0x2e, 0xf3, 0x8b, 0x98, 0x71, 0xd1, 0xf0, 0x09, 0x43, 0xd5, 0x61, 0x05, 0x7a, 0x8f, 0x9b, 0xbc, - 0x5e, 0x99, 0xf0, 0x7e, 0x5e, 0xef, 0x9b, 0xfb, 0x22, 0x16, 0x5e, 0x14, 0x38, 0xf7, 0xa5, 0x82, 0x97, 0x86, 0x13, - 0xb8, 0xc4, 0x43, 0x99, 0xf9, 0x54, 0xb6, 0x95, 0x99, 0xa2, 0x04, 0x25, 0xb5, 0x88, 0x5c, 0x92, 0x0b, 0x82, 0x94, - 0x8a, 0x97, 0x81, 0x50, 0xdb, 0xb7, 0x0b, 0x90, 0xbd, 0xaf, 0x2d, 0xe3, 0xb5, 0x64, 0xe7, 0x22, 0x94, 0xcd, 0x66, - 0x5c, 0xdb, 0xcb, 0x69, 0xb7, 0xbf, 0x95, 0x41, 0x35, 0x00, 0x25, 0xb3, 0xe1, 0x32, 0xe2, 0x9b, 0x9b, 0x1d, 0x0a, - 0x08, 0xed, 0xfc, 0x6d, 0x57, 0xca, 0x2c, 0xcc, 0x41, 0xd7, 0xec, 0xa8, 0xf8, 0x17, 0xe5, 0xdd, 0x59, 0xed, 0x66, - 0x7c, 0xc9, 0x3b, 0x18, 0xdf, 0x14, 0xca, 0x01, 0x2e, 0x79, 0x21, 0xeb, 0x07, 0x6e, 0x94, 0xc8, 0x12, 0xc2, 0x32, - 0xbb, 0xa8, 0x15, 0x95, 0xc9, 0x98, 0x36, 0xbd, 0xad, 0xc8, 0x66, 0x23, 0x63, 0x5d, 0x96, 0x68, 0x79, 0x6e, 0xc5, - 0xc9, 0xab, 0x87, 0x3f, 0x52, 0xe5, 0xf4, 0x7d, 0x89, 0xfa, 0xd4, 0x07, 0x77, 0x6f, 0x2b, 0xb3, 0x84, 0x4f, 0x4d, - 0x13, 0x45, 0x70, 0x07, 0xcc, 0x55, 0xb6, 0x22, 0x6a, 0x61, 0x48, 0xfd, 0x17, 0x5e, 0xfa, 0xc6, 0x13, 0xaa, 0xb6, - 0x73, 0xd5, 0xab, 0x39, 0x24, 0x66, 0x8c, 0xe7, 0x68, 0xf5, 0x01, 0xb2, 0x81, 0xae, 0xcd, 0xbe, 0x02, 0x02, 0xaf, - 0x99, 0xfc, 0xf2, 0xdb, 0x61, 0xcc, 0xd9, 0x6d, 0x5e, 0x68, 0x64, 0x2b, 0x67, 0xd9, 0xc1, 0x8d, 0xb4, 0x55, 0x7b, - 0x3c, 0xd3, 0x4d, 0x5c, 0x69, 0x99, 0x8c, 0x31, 0xbf, 0xad, 0xea, 0xab, 0x05, 0xdf, 0x99, 0xdb, 0xce, 0x4a, 0xa6, - 0x91, 0xab, 0xd5, 0x57, 0x78, 0x5e, 0x34, 0x41, 0x27, 0x2e, 0x31, 0x53, 0xab, 0xea, 0xb7, 0xaa, 0x1c, 0x15, 0x15, - 0xcb, 0xb9, 0xd5, 0xa6, 0xca, 0x6b, 0xb7, 0xa8, 0xdf, 0x9f, 0x8d, 0x09, 0x41, 0x65, 0x8a, 0xcc, 0x58, 0xa2, 0x8b, - 0x78, 0xa1, 0x9f, 0xd3, 0xba, 0xa8, 0xe8, 0xf1, 0xca, 0xaa, 0x86, 0x18, 0x02, 0xb7, 0xda, 0xc9, 0x20, 0x65, 0x16, - 0x89, 0x79, 0x16, 0x31, 0xdb, 0xeb, 0xd1, 0xb6, 0x39, 0x1b, 0x06, 0x6e, 0xd2, 0x39, 0x81, 0x73, 0x12, 0xfe, 0xa6, - 0x32, 0xdb, 0xb0, 0xa2, 0x6e, 0x79, 0x8d, 0xae, 0xa2, 0x6e, 0xcd, 0x79, 0x3d, 0x55, 0xc5, 0x0f, 0xd4, 0x71, 0xb5, - 0x5e, 0xdd, 0x88, 0x0e, 0x41, 0x81, 0x0b, 0xeb, 0xe7, 0x00, 0xfe, 0x6f, 0xab, 0x18, 0x1e, 0xec, 0xaa, 0x0f, 0xd5, - 0xaa, 0x69, 0x63, 0xdf, 0x38, 0x20, 0xb0, 0x58, 0x15, 0x5c, 0xa0, 0x33, 0xac, 0x50, 0x2f, 0x33, 0x6d, 0x30, 0x4c, - 0x8c, 0x53, 0x4b, 0xcf, 0xa5, 0x13, 0x11, 0x7d, 0x1a, 0x5e, 0xcc, 0x34, 0x77, 0x68, 0xb3, 0x55, 0x6e, 0x11, 0x6a, - 0x47, 0xb0, 0x08, 0x26, 0xd3, 0x65, 0x1c, 0x8c, 0xfc, 0x36, 0xb4, 0xd1, 0x00, 0x13, 0x97, 0x36, 0x65, 0x21, 0xc4, - 0xca, 0x02, 0xaa, 0xf7, 0x5d, 0xb0, 0x40, 0x30, 0xb3, 0x27, 0xa4, 0x5f, 0x41, 0x54, 0xf9, 0x69, 0x7c, 0x3e, 0xa9, - 0xa4, 0xb6, 0x9d, 0xaf, 0x73, 0x0d, 0x1c, 0xaa, 0xa6, 0xa2, 0x2a, 0x57, 0xb6, 0x21, 0x72, 0x18, 0xe4, 0x3a, 0x52, - 0x42, 0x2d, 0x91, 0x2f, 0x33, 0x4a, 0x27, 0x13, 0x46, 0xcb, 0xb5, 0x29, 0x56, 0x81, 0xb4, 0xd6, 0x85, 0x77, 0xf9, - 0x1f, 0x86, 0xb2, 0x0d, 0x7a, 0x21, 0x4a, 0xe4, 0xa2, 0x67, 0xf1, 0xe7, 0x6b, 0x9d, 0x03, 0xa4, 0xfa, 0x5f, 0xad, - 0x5c, 0x2a, 0x63, 0x03, 0xa7, 0xd8, 0x95, 0x91, 0xf8, 0x20, 0xad, 0x25, 0xfa, 0x3e, 0xd7, 0x31, 0xea, 0xba, 0x07, - 0x8d, 0xeb, 0x55, 0xb1, 0x18, 0xfa, 0xc5, 0x7b, 0x12, 0xdd, 0xc2, 0x97, 0x05, 0x25, 0x1d, 0xf4, 0xd3, 0x5d, 0xdd, - 0x5f, 0xa7, 0x84, 0x44, 0x65, 0x31, 0x31, 0x84, 0x55, 0x7e, 0x66, 0x38, 0x6c, 0x15, 0xd1, 0xac, 0xb6, 0xeb, 0xec, - 0xfb, 0x03, 0x05, 0x13, 0x88, 0xa0, 0xb0, 0xf8, 0xff, 0x73, 0x1f, 0x14, 0x68, 0xe0, 0xa4, 0xce, 0x8d, 0x4a, 0x4e, - 0xfb, 0xb9, 0x76, 0x7d, 0xa8, 0xbc, 0x04, 0x40, 0x56, 0x8f, 0x37, 0xb2, 0xbe, 0xe5, 0x77, 0x2b, 0x8b, 0x17, 0x30, - 0x96, 0x3f, 0x85, 0x4d, 0x56, 0x23, 0xda, 0x8c, 0xae, 0xd5, 0x6c, 0x94, 0x4f, 0x99, 0x18, 0x8e, 0x36, 0xd0, 0xf5, - 0xbb, 0x6d, 0x6f, 0x50, 0x5d, 0x58, 0x4b, 0xec, 0x3e, 0x60, 0x5d, 0xa9, 0x80, 0xfd, 0xac, 0xa9, 0xa5, 0x3b, 0x15, - 0xfc, 0xfc, 0x56, 0x4f, 0xa5, 0x59, 0xd8, 0x3a, 0x02, 0x7a, 0xf6, 0xd5, 0x15, 0x07, 0xc0, 0x07, 0x74, 0x0d, 0x0b, - 0x3d, 0x76, 0x2c, 0xf5, 0x99, 0x45, 0x94, 0x7d, 0xe6, 0x1e, 0x5f, 0xdf, 0x0c, 0x85, 0x87, 0x9d, 0xdb, 0x6f, 0xbd, - 0x2a, 0xc6, 0xf1, 0xc2, 0xba, 0xba, 0xc8, 0x05, 0xc5, 0x13, 0x92, 0x9c, 0x5f, 0xce, 0x61, 0xa8, 0x5a, 0x49, 0xc4, - 0x5c, 0x85, 0x04, 0x41, 0xed, 0xbb, 0x22, 0xe0, 0x31, 0x39, 0x5e, 0x81, 0xbb, 0x07, 0xa6, 0xa8, 0x9b, 0x1d, 0x42, - 0xe8, 0x96, 0xb4, 0xba, 0x5b, 0x91, 0x00, 0xd0, 0x2e, 0xd8, 0xfe, 0xc6, 0x79, 0x89, 0x2d, 0x68, 0x19, 0xad, 0x17, - 0x21, 0x0c, 0x44, 0x22, 0x85, 0x31, 0x72, 0x7a, 0x38, 0x5b, 0xd7, 0xa0, 0x18, 0xfa, 0x53, 0x17, 0x38, 0xf5, 0xe3, - 0xd9, 0xb6, 0x4b, 0x85, 0x64, 0x22, 0xe8, 0xd0, 0x14, 0x58, 0x96, 0x9d, 0x38, 0xed, 0x91, 0xe4, 0xfd, 0x7d, 0x9e, - 0x92, 0x15, 0x15, 0x3f, 0x94, 0xc3, 0xcf, 0x27, 0x24, 0x38, 0xd4, 0x13, 0x30, 0x83, 0x0e, 0x78, 0xa6, 0xf7, 0xa9, - 0x13, 0x23, 0x95, 0xf5, 0x0e, 0x38, 0x8a, 0x88, 0x32, 0xd3, 0x25, 0xbb, 0xc7, 0xed, 0xf1, 0x14, 0x70, 0x23, 0x63, - 0xda, 0x65, 0x8e, 0x61, 0x26, 0x30, 0x8e, 0xf9, 0x6a, 0x7c, 0x3e, 0xa2, 0x1f, 0xc7, 0x7d, 0x44, 0xc9, 0x45, 0xa5, - 0x86, 0xc2, 0x36, 0x66, 0x8b, 0x5a, 0xf4, 0xd4, 0xbe, 0x91, 0x48, 0x47, 0xaf, 0x60, 0x2c, 0x17, 0x98, 0x06, 0x2b, - 0x9d, 0xf3, 0x8a, 0x82, 0x15, 0x4d, 0x80, 0xb8, 0x0a, 0xe3, 0x94, 0x51, 0x6b, 0xcc, 0x52, 0x18, 0x5c, 0x51, 0x93, - 0x11, 0xc1, 0xaf, 0x26, 0xf4, 0xec, 0x63, 0x31, 0xdc, 0x93, 0x97, 0xc3, 0xa1, 0x1e, 0xad, 0xa7, 0x75, 0xf7, 0x06, - 0x16, 0x63, 0xc1, 0xb2, 0xc0, 0x9c, 0x9d, 0x0e, 0x3a, 0xaf, 0xd8, 0xd6, 0xd4, 0x3a, 0x5d, 0xad, 0x1f, 0x5a, 0xd9, - 0x28, 0x96, 0xd3, 0x24, 0x92, 0x38, 0x6f, 0xa6, 0x51, 0x8c, 0x3f, 0x34, 0x5c, 0xea, 0x86, 0xfa, 0xc4, 0x6f, 0xcd, - 0x5f, 0x4b, 0xa5, 0xbf, 0xce, 0x3f, 0x8a, 0x85, 0x1d, 0x9b, 0xd8, 0x6f, 0xb4, 0x60, 0x65, 0xd1, 0xd8, 0x40, 0xa8, - 0xea, 0x0b, 0x9e, 0x25, 0x2b, 0x95, 0x27, 0xdf, 0x8d, 0xd8, 0xd2, 0x02, 0x3f, 0x8f, 0xf2, 0x6a, 0xea, 0xcd, 0x88, - 0x41, 0xb5, 0x7c, 0x8a, 0x6a, 0x77, 0x72, 0x20, 0x5c, 0x26, 0x37, 0x56, 0x95, 0x07, 0x88, 0x9e, 0x5f, 0x96, 0x1e, - 0x09, 0x73, 0xa9, 0x98, 0x92, 0x06, 0xcf, 0x89, 0xa0, 0xb7, 0x30, 0x85, 0x18, 0x1e, 0x49, 0xdf, 0xa0, 0xf2, 0xee, - 0x8f, 0xeb, 0x7d, 0xaf, 0x0b, 0xdc, 0x19, 0x3b, 0xdf, 0x74, 0x2f, 0x9d, 0x19, 0x34, 0x7a, 0xfd, 0x73, 0xa8, 0x5a, - 0x84, 0xc1, 0x4e, 0xd3, 0x85, 0xa0, 0x09, 0xea, 0x5f, 0x8c, 0x06, 0xd6, 0x32, 0x5d, 0xeb, 0xed, 0x20, 0x53, 0x21, - 0x31, 0xfe, 0x5f, 0x64, 0xbc, 0x0c, 0x98, 0x9c, 0x8c, 0xe2, 0x16, 0x3c, 0x00, 0x37, 0xd3, 0x90, 0x0b, 0x94, 0xd9, - 0xc3, 0x13, 0xa8, 0xc9, 0x58, 0x84, 0x67, 0x39, 0xe6, 0x3a, 0x75, 0x60, 0x3d, 0xb2, 0x79, 0x58, 0xa3, 0x70, 0xb5, - 0x9c, 0x4d, 0x4e, 0xc9, 0x8e, 0x59, 0x5d, 0xed, 0x63, 0x77, 0xc6, 0x25, 0x9e, 0x39, 0x1f, 0xf2, 0x6d, 0xec, 0x71, - 0xc0, 0x1c, 0x87, 0x07, 0x0e, 0x33, 0xe7, 0x21, 0x82, 0x5c, 0x37, 0xc0, 0x16, 0x60, 0xb2, 0x93, 0xb5, 0x6b, 0x94, - 0xb0, 0x78, 0x73, 0x03, 0x20, 0x8f, 0x64, 0x12, 0x42, 0x2e, 0x1b, 0x7e, 0x96, 0x5a, 0xaa, 0x8f, 0x80, 0x8f, 0xd5, - 0x87, 0x9a, 0x06, 0x42, 0xdc, 0x36, 0x42, 0x1e, 0x30, 0x26, 0xae, 0xcc, 0x2f, 0xaa, 0x09, 0x2e, 0xf8, 0x2f, 0x7b, - 0x1d, 0x7f, 0xdd, 0xac, 0xfb, 0x9e, 0x21, 0x22, 0x1d, 0xac, 0x45, 0x64, 0xbd, 0x22, 0x85, 0xff, 0x86, 0xdf, 0x03, - 0x45, 0x09, 0xc5, 0x52, 0xb1, 0xfc, 0x88, 0xea, 0x1e, 0xe3, 0x1e, 0xf2, 0xde, 0x4e, 0x7e, 0x1f, 0x09, 0x83, 0x1e, - 0x50, 0x63, 0x94, 0xa4, 0x38, 0x52, 0xab, 0x9e, 0x7b, 0x14, 0x2c, 0x85, 0xa5, 0x86, 0xe7, 0x88, 0xd2, 0xd5, 0xf7, - 0x0a, 0xb5, 0xf0, 0x1f, 0x2d, 0x6d, 0x9e, 0x86, 0x7d, 0x44, 0xf2, 0x4d, 0x46, 0xd7, 0xc8, 0x42, 0x25, 0x51, 0x78, - 0x23, 0x04, 0x9e, 0x73, 0xc6, 0x53, 0x7d, 0x84, 0x98, 0x07, 0xa2, 0xc9, 0xc8, 0xf5, 0x80, 0xde, 0xd1, 0xe6, 0xe8, - 0x45, 0x72, 0x4c, 0xdf, 0xb4, 0x0f, 0x83, 0xb0, 0x1d, 0x5b, 0x5c, 0x6a, 0x4c, 0x44, 0x6b, 0x5a, 0x75, 0xd9, 0x23, - 0x52, 0xef, 0x3c, 0x15, 0xa3, 0x04, 0x25, 0x72, 0x13, 0x15, 0xdd, 0x39, 0x4e, 0xed, 0xa2, 0xe8, 0xf6, 0x19, 0x4b, - 0xb8, 0x18, 0x55, 0x7c, 0x5f, 0x06, 0x9f, 0x44, 0x52, 0x34, 0x60, 0xd8, 0x80, 0xaf, 0xf7, 0xff, 0x60, 0xe8, 0x66, - 0x20, 0x97, 0xda, 0x30, 0x65, 0xf3, 0xe9, 0x9c, 0x66, 0x5b, 0xc3, 0x7d, 0x83, 0xd6, 0x97, 0x50, 0xcf, 0xb9, 0xcf, - 0x88, 0xdf, 0x4a, 0xcd, 0x2d, 0x26, 0xab, 0x36, 0x1b, 0x59, 0x4c, 0xd6, 0x61, 0xd5, 0x3d, 0x46, 0xa6, 0x10, 0xff, - 0x42, 0x93, 0x5c, 0x10, 0x1e, 0x55, 0xc9, 0x82, 0x7f, 0xd0, 0xcc, 0x60, 0xc3, 0x79, 0x9d, 0xfe, 0x8d, 0x32, 0x80, - 0xf7, 0x3b, 0xcb, 0x5a, 0x41, 0x35, 0x25, 0xb5, 0xe3, 0xba, 0x4b, 0xc7, 0x2f, 0x5d, 0xa0, 0x07, 0x99, 0x89, 0x67, - 0x47, 0x25, 0x21, 0x66, 0x81, 0x75, 0x2f, 0x91, 0xfa, 0xe6, 0x27, 0xe9, 0x91, 0x36, 0x78, 0xce, 0x42, 0xb4, 0xa0, - 0x17, 0x15, 0xfb, 0x7b, 0xa5, 0x34, 0xbd, 0x57, 0x76, 0x06, 0xaf, 0x8d, 0x99, 0xca, 0x41, 0xb0, 0xee, 0x12, 0x7a, - 0xb9, 0x3c, 0xc9, 0x8f, 0x6d, 0xe6, 0x92, 0xe6, 0x23, 0xe9, 0xef, 0xaa, 0x14, 0xeb, 0xc7, 0x80, 0xd6, 0xbf, 0xa6, - 0xcc, 0x92, 0x14, 0x68, 0x30, 0x58, 0x8d, 0x15, 0xdb, 0x04, 0x1c, 0x52, 0x68, 0x22, 0xa2, 0x89, 0x76, 0xdc, 0xee, - 0x68, 0x7c, 0x86, 0xd4, 0x47, 0xb8, 0x40, 0x32, 0xe0, 0x91, 0x43, 0x4c, 0x56, 0xc5, 0x2e, 0xc0, 0x17, 0xb8, 0x7d, - 0x3c, 0x83, 0x7e, 0xd8, 0x6e, 0xdd, 0x20, 0xe5, 0xa6, 0x1c, 0x17, 0x01, 0x6b, 0x08, 0x80, 0xa7, 0x5c, 0x13, 0xad, - 0x06, 0x52, 0x7d, 0x69, 0x04, 0xec, 0xbb, 0x83, 0xfa, 0x38, 0x9a, 0xa6, 0x8c, 0x65, 0xd3, 0xc4, 0x4b, 0xc9, 0x23, - 0xc4, 0x88, 0x7d, 0x85, 0x53, 0x8e, 0xc0, 0xbc, 0xc3, 0xef, 0xac, 0xd7, 0x42, 0x7a, 0x9b, 0xe8, 0x73, 0x93, 0x81, - 0x87, 0xe1, 0xc7, 0xd8, 0x7e, 0xd1, 0xd3, 0xce, 0xd6, 0x9c, 0xbf, 0x0a, 0x48, 0x46, 0x47, 0xe1, 0x5f, 0x85, 0x67, - 0xa5, 0x6d, 0x12, 0x42, 0xfc, 0x03, 0xd1, 0x75, 0x86, 0x33, 0x48, 0xb0, 0x48, 0x5f, 0x2e, 0x6a, 0x17, 0x39, 0x05, - 0x95, 0xf6, 0x99, 0xd5, 0xca, 0xb2, 0x7c, 0x7b, 0xfb, 0x8f, 0x73, 0x6b, 0x53, 0x8d, 0x0b, 0x1e, 0x72, 0x8d, 0xcb, - 0x56, 0x36, 0xfc, 0xa2, 0x8d, 0xf7, 0x96, 0x6c, 0x36, 0x72, 0xd5, 0x57, 0x2e, 0xe1, 0x9f, 0xf8, 0x51, 0x46, 0xb2, - 0xf1, 0x02, 0xac, 0x45, 0x6a, 0x79, 0xe4, 0xea, 0xa9, 0xed, 0x7b, 0xbd, 0x50, 0xac, 0x0c, 0xee, 0xfc, 0xe2, 0x38, - 0x41, 0x92, 0xca, 0x43, 0xfe, 0x9c, 0xaf, 0xe3, 0x1c, 0x3b, 0xab, 0xe9, 0x68, 0x45, 0xef, 0x48, 0x5d, 0x0e, 0x16, - 0x5b, 0x8e, 0x92, 0xf3, 0xc1, 0xb9, 0x6b, 0x86, 0x1e, 0x1c, 0x45, 0x3d, 0x9f, 0x29, 0x89, 0x05, 0x5c, 0x9a, 0xbb, - 0xa7, 0x08, 0x7a, 0x84, 0x48, 0x8c, 0xd0, 0xbb, 0xa0, 0x21, 0x18, 0xb6, 0x39, 0xdf, 0x64, 0x82, 0x36, 0x6b, 0xd1, - 0x2e, 0xe2, 0x17, 0xc3, 0xa2, 0xf0, 0xda, 0xbb, 0x7a, 0xe4, 0x8a, 0xe5, 0x12, 0x1a, 0x03, 0x59, 0x83, 0x62, 0xff, - 0x9a, 0x04, 0x3f, 0xe8, 0x9f, 0xad, 0x16, 0x1a, 0x2b, 0x53, 0xe6, 0xc7, 0x8c, 0x55, 0xea, 0x9c, 0xb5, 0xf4, 0x6e, - 0x6a, 0x17, 0x94, 0x9c, 0x6c, 0xe5, 0xfc, 0x5a, 0xcc, 0xbf, 0x1f, 0xaa, 0x9f, 0x66, 0xca, 0x3b, 0x84, 0x27, 0xcc, - 0xd7, 0x89, 0xa2, 0x80, 0xac, 0x9b, 0x25, 0xce, 0x52, 0x52, 0xc7, 0x2a, 0x89, 0x12, 0x63, 0x3b, 0x87, 0x47, 0x08, - 0x42, 0xd2, 0xd9, 0xac, 0xce, 0x8c, 0xc9, 0x95, 0x14, 0x6f, 0x87, 0x72, 0x25, 0x9c, 0xc5, 0x22, 0x4d, 0x50, 0x74, - 0xf9, 0x86, 0x5c, 0x2a, 0xf4, 0x54, 0x97, 0x76, 0x74, 0xa8, 0xf4, 0x80, 0x7f, 0x74, 0x73, 0x89, 0x99, 0x54, 0xa0, - 0x11, 0x9f, 0xc7, 0x96, 0x54, 0x22, 0x59, 0x15, 0x39, 0x0c, 0xd4, 0xca, 0xe4, 0x27, 0xdb, 0xe7, 0x32, 0x5a, 0x37, - 0x21, 0x70, 0xdd, 0xe6, 0x4a, 0xe2, 0xee, 0x5f, 0x26, 0xf3, 0x74, 0x00, 0xf6, 0xcb, 0x72, 0x9d, 0x37, 0x3a, 0xe1, - 0xf2, 0xe8, 0xec, 0x53, 0x13, 0xec, 0x78, 0x07, 0x2f, 0x26, 0x12, 0x04, 0x07, 0x89, 0x44, 0xa4, 0x82, 0x33, 0x90, - 0x78, 0x02, 0x03, 0x70, 0x72, 0xfe, 0x88, 0x9f, 0x17, 0x64, 0x79, 0x01, 0x5c, 0xe1, 0xa8, 0x02, 0x44, 0x82, 0x04, - 0x8d, 0x2e, 0xbc, 0x9b, 0x63, 0xf5, 0x9a, 0x2d, 0xb7, 0xab, 0xd2, 0x79, 0x50, 0x73, 0x24, 0x85, 0x92, 0x30, 0xe2, - 0x0c, 0x8b, 0x1f, 0x6c, 0x4a, 0x94, 0xaf, 0x1a, 0x81, 0x30, 0xb2, 0x58, 0xe2, 0x85, 0x46, 0x83, 0x00, 0x8f, 0x8f, - 0x90, 0x32, 0xd9, 0x36, 0xe3, 0x98, 0x7d, 0x4d, 0x89, 0x73, 0x86, 0xcc, 0x10, 0x4a, 0x06, 0xe6, 0x68, 0x09, 0x60, - 0x9d, 0xc5, 0x18, 0x4d, 0xa5, 0x29, 0x3e, 0x3f, 0x47, 0xad, 0xd6, 0x91, 0x57, 0x36, 0x43, 0xbd, 0x0d, 0x56, 0x4c, - 0x89, 0x00, 0xc7, 0x21, 0xa4, 0x97, 0xc0, 0x82, 0x32, 0xe6, 0xb6, 0x24, 0x97, 0x22, 0x3f, 0x24, 0x6b, 0x49, 0xd3, - 0x66, 0x60, 0x70, 0xae, 0x9b, 0x7c, 0x31, 0x1f, 0x8e, 0x92, 0x69, 0x15, 0x6c, 0x1a, 0xeb, 0xfe, 0x3d, 0x6c, 0x9a, - 0x6e, 0x72, 0xe5, 0xb6, 0x0a, 0xc6, 0x6a, 0xe6, 0x78, 0xc4, 0xe6, 0x6c, 0xc0, 0xcb, 0xef, 0x41, 0x1a, 0x2e, 0x1e, - 0x42, 0x64, 0xda, 0x4f, 0xfb, 0xa7, 0xd8, 0xbb, 0xe5, 0xf2, 0x64, 0x06, 0xce, 0xda, 0xd8, 0x1d, 0xa7, 0xa8, 0xae, - 0x25, 0x51, 0x2e, 0x09, 0x9b, 0x0f, 0x80, 0xa1, 0xd6, 0xf7, 0xa2, 0xec, 0xff, 0x2e, 0xe9, 0x89, 0xa2, 0xc2, 0x73, - 0x9d, 0xd7, 0x67, 0xa9, 0x3f, 0x80, 0x76, 0x1f, 0xc7, 0xe4, 0xce, 0x38, 0xcc, 0x11, 0x50, 0x99, 0xbd, 0x5f, 0xbf, - 0xa2, 0x83, 0xb6, 0x95, 0xea, 0x4f, 0x28, 0xce, 0x1f, 0x94, 0xd1, 0x3a, 0x5b, 0xe6, 0xfc, 0x6c, 0xc1, 0x40, 0x67, - 0x92, 0x96, 0x92, 0xca, 0x27, 0xfe, 0x87, 0xea, 0xf0, 0x31, 0xb5, 0x47, 0x8c, 0x4d, 0x24, 0x09, 0x7e, 0x92, 0x27, - 0x7c, 0x4c, 0x35, 0x13, 0xb5, 0x3d, 0x43, 0x8a, 0x7a, 0x63, 0x44, 0xa6, 0x52, 0x4d, 0x96, 0x15, 0x9b, 0x58, 0xe4, - 0x04, 0xbb, 0xba, 0xb0, 0x2e, 0x7d, 0xa2, 0x3e, 0xa5, 0xa6, 0xe6, 0x20, 0x5c, 0x18, 0x48, 0x77, 0xdb, 0x55, 0x8f, - 0x16, 0x4b, 0x5a, 0x28, 0x52, 0x12, 0x39, 0x89, 0xa4, 0x69, 0x1c, 0xa9, 0x0b, 0x60, 0x9e, 0xa3, 0xec, 0x56, 0xb2, - 0x06, 0x6b, 0x6b, 0x3c, 0x51, 0x27, 0xd5, 0x91, 0x9b, 0x3a, 0xcc, 0xac, 0xa7, 0x9a, 0xf9, 0x3f, 0x3b, 0x26, 0x52, - 0xd0, 0x71, 0xe5, 0x91, 0x27, 0x94, 0xe6, 0xcd, 0x54, 0xed, 0x64, 0x48, 0x8f, 0x3c, 0xd7, 0xdb, 0xa4, 0x63, 0x9f, - 0x0b, 0x25, 0x0e, 0xdd, 0x74, 0x39, 0xd5, 0x25, 0xf0, 0xf1, 0x55, 0xfc, 0x91, 0x50, 0x2c, 0x49, 0xa4, 0x61, 0xee, - 0x6c, 0x34, 0xb6, 0x34, 0x56, 0x97, 0x8a, 0x2c, 0x0e, 0x4b, 0x43, 0xd5, 0x55, 0x94, 0xc5, 0x1b, 0x35, 0xd8, 0x44, - 0xd4, 0x45, 0x7d, 0x0d, 0x3a, 0xa5, 0xf3, 0x1c, 0x68, 0xa9, 0xc5, 0xdd, 0x53, 0x41, 0xef, 0x27, 0x5c, 0x53, 0x01, - 0x0e, 0x26, 0x1e, 0xb9, 0x78, 0xc1, 0xe9, 0x32, 0xa7, 0x2b, 0xd8, 0x5f, 0x34, 0x52, 0x8d, 0x33, 0x0b, 0x55, 0x39, - 0x33, 0xaa, 0xda, 0x99, 0x75, 0xd3, 0x0d, 0x86, 0x39, 0x57, 0xbb, 0x1c, 0x59, 0x94, 0x25, 0x59, 0x1c, 0x57, 0xa6, - 0x89, 0xe7, 0xf6, 0xca, 0x65, 0xa4, 0xf3, 0x4a, 0xb6, 0x98, 0x8c, 0x89, 0x4b, 0x3d, 0x32, 0x3d, 0x31, 0xb2, 0x6c, - 0xa0, 0xb6, 0x4b, 0xaf, 0xa9, 0x3a, 0xf6, 0x8b, 0x3b, 0x16, 0x85, 0x97, 0xb9, 0xc6, 0xf4, 0x38, 0x09, 0x19, 0xf2, - 0xa5, 0x35, 0x50, 0x62, 0x1b, 0xfc, 0x58, 0x8e, 0xf6, 0xd3, 0x69, 0x09, 0xac, 0x49, 0x44, 0xa0, 0xea, 0xb5, 0x81, - 0xfc, 0xb8, 0x4d, 0x0f, 0xe9, 0xcb, 0x16, 0x2e, 0xca, 0x1f, 0xca, 0x61, 0x73, 0xe0, 0x10, 0x66, 0x02, 0xa3, 0x60, - 0xa1, 0xbc, 0x92, 0xc0, 0x26, 0xf0, 0x3b, 0x46, 0xcd, 0x76, 0xbb, 0xd2, 0xfb, 0x00, 0x32, 0x19, 0x37, 0x21, 0x3c, - 0x80, 0xc2, 0xeb, 0x29, 0x28, 0x57, 0x88, 0x03, 0xcd, 0x14, 0xa0, 0xc3, 0x0f, 0xe9, 0xc3, 0x13, 0x90, 0x1f, 0xd3, - 0xe1, 0x47, 0xb7, 0x72, 0x1b, 0x6d, 0x73, 0x2c, 0x4f, 0x95, 0x87, 0x6a, 0x1c, 0x21, 0x4a, 0x72, 0x61, 0xb1, 0xa8, - 0xdb, 0x2b, 0x57, 0xb4, 0xbd, 0xf1, 0x5e, 0xdf, 0xb0, 0x4d, 0x3a, 0xfe, 0x18, 0xe6, 0xb8, 0xc2, 0xa8, 0x46, 0x15, - 0x6c, 0xe9, 0x1d, 0xb0, 0xd5, 0x5d, 0x25, 0xb0, 0xc7, 0xa6, 0xb1, 0xb9, 0x00, 0x1d, 0x1a, 0xa2, 0x0c, 0xa4, 0x54, - 0x35, 0x0b, 0x64, 0x72, 0xf5, 0x29, 0xec, 0xb6, 0xa6, 0x31, 0x9b, 0x90, 0xf7, 0xbf, 0xa1, 0x79, 0x15, 0x96, 0x7c, - 0xc2, 0xfe, 0x10, 0xc9, 0x67, 0xf8, 0xd2, 0x47, 0x8d, 0xf8, 0x1e, 0xe0, 0x6a, 0x5f, 0x0a, 0x45, 0xa6, 0x38, 0xb6, - 0xc7, 0x6b, 0xd4, 0x26, 0xf3, 0xf0, 0x50, 0x47, 0x17, 0x36, 0xe4, 0x47, 0x38, 0x61, 0xfb, 0x31, 0xce, 0x93, 0x0b, - 0x8c, 0xe8, 0xbb, 0x18, 0x37, 0x07, 0xe8, 0xca, 0x00, 0xbc, 0x2d, 0xa3, 0x5e, 0x2a, 0x1f, 0xec, 0xf0, 0x16, 0x75, - 0xb3, 0xe3, 0x34, 0xd8, 0x66, 0xc4, 0xa1, 0x1c, 0x0a, 0x70, 0x97, 0xbd, 0xaa, 0x52, 0xe4, 0xf4, 0xd6, 0xf4, 0x8e, - 0xeb, 0x0d, 0xf2, 0x45, 0x13, 0x4d, 0x1d, 0x4c, 0x7a, 0x00, 0x13, 0x6a, 0x39, 0xa0, 0x31, 0x7a, 0xb5, 0x25, 0x5b, - 0x5c, 0x0b, 0x9e, 0xd9, 0x02, 0xd2, 0xbc, 0x22, 0xd5, 0x6e, 0x94, 0x46, 0x53, 0x32, 0x34, 0x69, 0x13, 0x8b, 0xd4, - 0x40, 0x32, 0xab, 0x57, 0x75, 0x22, 0x55, 0x83, 0x2d, 0x70, 0x60, 0xb3, 0x20, 0xc3, 0x37, 0xfb, 0x93, 0x01, 0x63, - 0x4b, 0xaf, 0x7d, 0x4f, 0x76, 0x1f, 0x35, 0xe4, 0x1a, 0x12, 0x19, 0xc7, 0x39, 0xd1, 0x6c, 0xaa, 0x88, 0xf8, 0x64, - 0x1d, 0x15, 0xb0, 0x36, 0x97, 0x6d, 0xe5, 0x83, 0x0a, 0x7a, 0x6c, 0xb0, 0xc9, 0x06, 0xd7, 0x8e, 0x19, 0xd6, 0x17, - 0x9b, 0x8a, 0xd4, 0x65, 0xfa, 0x40, 0xc4, 0x34, 0x83, 0x44, 0xa8, 0x3b, 0x36, 0xbe, 0xae, 0x4a, 0xbb, 0x20, 0xec, - 0xfb, 0xab, 0x74, 0xae, 0x61, 0xe3, 0x15, 0x90, 0xb9, 0xbe, 0xea, 0x00, 0x03, 0xbc, 0x02, 0x71, 0x31, 0xe0, 0xe3, - 0x2d, 0x90, 0x29, 0xf9, 0x77, 0xd4, 0x73, 0xa3, 0x94, 0x47, 0x2d, 0xef, 0x86, 0x19, 0xde, 0x6a, 0x2f, 0xf3, 0x0f, - 0x4b, 0x1f, 0xf2, 0x21, 0x41, 0x85, 0x2c, 0xa4, 0xe6, 0x49, 0xd4, 0xcd, 0x1d, 0x88, 0x6d, 0xdd, 0xe7, 0x22, 0xa3, - 0x58, 0xc4, 0xca, 0x23, 0xc0, 0x5d, 0x04, 0xbc, 0xdb, 0xac, 0x94, 0x88, 0x6f, 0x0e, 0xa5, 0x55, 0xde, 0x12, 0xcd, - 0x55, 0x60, 0xde, 0x45, 0x2b, 0x3b, 0x9c, 0xea, 0x90, 0x81, 0x9d, 0x4a, 0xed, 0x94, 0x44, 0xef, 0xb1, 0xc2, 0x5e, - 0xb3, 0x8d, 0xf5, 0x5b, 0x3b, 0xfa, 0x10, 0x56, 0x0b, 0x56, 0xdb, 0x73, 0x85, 0xe6, 0x66, 0xa2, 0x80, 0x58, 0x60, - 0xcd, 0xde, 0xbe, 0x49, 0x14, 0x42, 0xe1, 0x82, 0xcb, 0x52, 0x2a, 0x91, 0x62, 0xdb, 0x01, 0x83, 0x44, 0x13, 0x26, - 0xaa, 0x8a, 0x73, 0x23, 0xf6, 0x3c, 0x6b, 0xb0, 0xc0, 0xb3, 0x92, 0x0c, 0x8a, 0xd0, 0xba, 0xad, 0x76, 0xa9, 0x40, - 0xef, 0x67, 0x81, 0x95, 0x53, 0xe5, 0x04, 0x0e, 0xa9, 0x46, 0x1c, 0x9f, 0x05, 0x23, 0xb7, 0x4a, 0xf9, 0x16, 0x61, - 0xce, 0xe0, 0xcc, 0x7f, 0x5d, 0x7a, 0x6d, 0x7a, 0x52, 0x0a, 0x0e, 0xd3, 0xf7, 0xe7, 0x30, 0xe9, 0x15, 0x18, 0x9d, - 0xf7, 0x9e, 0xf7, 0x4a, 0x16, 0xf8, 0xeb, 0xb5, 0xbe, 0x14, 0x45, 0xb3, 0x29, 0x4f, 0x3d, 0xb9, 0x94, 0xf9, 0x96, - 0x06, 0xae, 0x53, 0x1c, 0xad, 0xee, 0xd9, 0x9b, 0x15, 0x30, 0x13, 0xcb, 0xf0, 0xef, 0xc1, 0xd6, 0xbe, 0x81, 0xf3, - 0x62, 0x09, 0x44, 0x7e, 0x63, 0x7c, 0x7d, 0xc8, 0xd3, 0xe2, 0x85, 0xcf, 0xcf, 0x08, 0x0b, 0x15, 0xe6, 0x8a, 0x84, - 0xc7, 0xa7, 0x4a, 0xab, 0x2c, 0x41, 0xc3, 0xb4, 0x7c, 0xa6, 0xc7, 0x8b, 0xfa, 0x56, 0x39, 0x7a, 0xeb, 0x2f, 0xb3, - 0xd2, 0x98, 0xcf, 0xcf, 0x93, 0x47, 0x4a, 0xbc, 0x7e, 0xf2, 0x89, 0xaa, 0xf2, 0x67, 0xc3, 0x6d, 0xbd, 0xef, 0xe1, - 0xd7, 0xde, 0x7e, 0x90, 0x65, 0x81, 0xc8, 0xaa, 0x71, 0x6d, 0xe3, 0xa8, 0xf2, 0x34, 0xed, 0x85, 0x58, 0x28, 0xc2, - 0x0f, 0x8a, 0x0e, 0x2c, 0x2f, 0x73, 0xa9, 0xe6, 0x5c, 0x85, 0x8a, 0x46, 0xec, 0x69, 0xfc, 0x7c, 0x08, 0x4b, 0x61, - 0x2a, 0x32, 0x08, 0xe3, 0x0e, 0xed, 0x92, 0x64, 0xec, 0x96, 0x32, 0x6d, 0xeb, 0x77, 0x0b, 0xe5, 0x35, 0x15, 0x13, - 0x30, 0x85, 0x77, 0x20, 0xb9, 0x99, 0x2d, 0xb6, 0xd6, 0x39, 0xa9, 0xa3, 0x02, 0xfb, 0x31, 0xa6, 0xc0, 0x61, 0xa7, - 0x9b, 0xe9, 0x73, 0x81, 0x1b, 0x9a, 0xf2, 0x50, 0x6f, 0x3a, 0xc3, 0x95, 0xd7, 0xf1, 0x43, 0x75, 0xe6, 0x6c, 0x81, - 0x96, 0x6b, 0xe4, 0x2b, 0xbe, 0xaa, 0x96, 0x20, 0xf2, 0x40, 0xf2, 0xb7, 0xaf, 0xbe, 0x7b, 0xab, 0x4b, 0x45, 0xd2, - 0x19, 0x6e, 0xd5, 0xb0, 0x3b, 0x58, 0xe8, 0x6e, 0x75, 0x26, 0x91, 0x04, 0x1a, 0x90, 0x5d, 0x1b, 0xde, 0x0b, 0x1b, - 0xe8, 0x4e, 0x3b, 0x5c, 0x3b, 0xa9, 0x22, 0x68, 0x9d, 0x5d, 0x65, 0x0c, 0x6d, 0xd9, 0x45, 0xc4, 0x2d, 0xbb, 0x0e, - 0x47, 0xd1, 0xcd, 0xb4, 0x10, 0xd6, 0xc6, 0xe3, 0x1e, 0x54, 0x6f, 0x33, 0x20, 0x25, 0x22, 0x12, 0x28, 0x17, 0xe2, - 0x6f, 0x5d, 0xa8, 0x59, 0xc6, 0xdd, 0xa6, 0x43, 0xec, 0x26, 0x89, 0xeb, 0x83, 0x66, 0xf0, 0xd6, 0xa5, 0x95, 0xd7, - 0x19, 0x52, 0xf8, 0x48, 0x45, 0x06, 0xce, 0x0d, 0x53, 0x7b, 0xda, 0x65, 0x1d, 0xc6, 0xbc, 0x54, 0xca, 0xaa, 0x08, - 0xb8, 0xd5, 0x00, 0xcf, 0xda, 0xb7, 0x70, 0x4c, 0x13, 0x1b, 0x9a, 0xa7, 0xbe, 0xcf, 0xd1, 0x76, 0x37, 0x5e, 0xb4, - 0xe2, 0xab, 0xd7, 0xd6, 0x71, 0xd9, 0x3c, 0xeb, 0x6e, 0x13, 0x56, 0xb1, 0x9f, 0x22, 0x29, 0x6c, 0x1a, 0x8b, 0xb9, - 0x26, 0x71, 0x4c, 0x02, 0xa3, 0x05, 0xb0, 0x37, 0xd1, 0x2c, 0xbb, 0x58, 0x22, 0xb5, 0x75, 0x58, 0x77, 0x73, 0xc0, - 0xe1, 0xdb, 0xce, 0x57, 0xaa, 0x76, 0x53, 0x83, 0x12, 0x39, 0xe7, 0xc3, 0xfe, 0xc2, 0xed, 0xfe, 0x50, 0xe2, 0x4d, - 0xdd, 0xc6, 0xe2, 0x48, 0x34, 0x16, 0x10, 0x5c, 0x66, 0x8c, 0xda, 0x2c, 0x8b, 0x10, 0x9d, 0x5a, 0x59, 0xff, 0x40, - 0x05, 0x48, 0x25, 0xd4, 0x6a, 0x71, 0x33, 0x81, 0x05, 0xc7, 0xa4, 0xd4, 0xc6, 0xe1, 0xc7, 0x3f, 0x89, 0xa7, 0x54, - 0xb4, 0x69, 0xd4, 0x13, 0xcd, 0x05, 0xfb, 0x72, 0x88, 0x46, 0x20, 0x77, 0x1b, 0xd6, 0x38, 0xf5, 0xa2, 0xb3, 0xb9, - 0x51, 0xe8, 0xb0, 0x32, 0x55, 0x60, 0xfc, 0xad, 0xc2, 0x6c, 0x20, 0xe7, 0x2a, 0x89, 0x95, 0xdb, 0x19, 0x78, 0x61, - 0x84, 0x0e, 0x62, 0x00, 0xbd, 0x9d, 0xfc, 0x54, 0x7f, 0x5a, 0x5d, 0x94, 0x71, 0x22, 0x4c, 0x4e, 0xdf, 0xdb, 0xc1, - 0x83, 0xda, 0x6c, 0xe7, 0x52, 0xbc, 0xe6, 0x39, 0x81, 0xf6, 0x95, 0x9f, 0xfd, 0x5e, 0xbf, 0x3f, 0x71, 0x13, 0x5a, - 0x56, 0x20, 0x75, 0x8a, 0x7f, 0xd5, 0x9d, 0x51, 0xee, 0x76, 0xce, 0xb3, 0x09, 0xcb, 0x63, 0x92, 0xec, 0x1b, 0x7f, - 0x5b, 0xb8, 0xe4, 0x68, 0xc9, 0x9f, 0x38, 0x0f, 0x8c, 0x62, 0x31, 0x4d, 0x16, 0xa6, 0x7e, 0x4d, 0xd2, 0xde, 0xc4, - 0x75, 0x6c, 0xc4, 0xf1, 0x9f, 0xe3, 0x10, 0x3d, 0x49, 0x84, 0xd4, 0x7a, 0x4b, 0x51, 0x3d, 0xaa, 0x7b, 0x27, 0xea, - 0x42, 0x36, 0x0f, 0x39, 0x5a, 0x93, 0x31, 0x9d, 0xd4, 0x5d, 0xaa, 0x91, 0x68, 0x04, 0x4b, 0xb7, 0x76, 0x3b, 0x39, - 0x3c, 0xc4, 0x4b, 0xfb, 0x28, 0x12, 0x15, 0xbd, 0x0b, 0x4d, 0x71, 0x68, 0xa3, 0x58, 0x5f, 0x59, 0x89, 0x05, 0xd6, - 0x5e, 0x2a, 0xad, 0xe6, 0x82, 0xae, 0xbc, 0x1c, 0x15, 0x3a, 0x27, 0x00, 0x5b, 0xcb, 0x39, 0x91, 0x01, 0x74, 0x62, - 0xb1, 0x70, 0x1d, 0x72, 0x03, 0xca, 0x10, 0xa1, 0x72, 0x4b, 0x8f, 0x53, 0x69, 0xd5, 0x28, 0x96, 0x80, 0xc4, 0x70, - 0xc6, 0x7c, 0xeb, 0x63, 0x36, 0x6e, 0x64, 0x0c, 0xae, 0x5a, 0x76, 0x6d, 0x19, 0x6b, 0x6b, 0x85, 0xb4, 0x0e, 0x98, - 0x5a, 0x65, 0x3f, 0x35, 0xbe, 0xf3, 0xe7, 0xbd, 0x23, 0x4d, 0x6f, 0x71, 0x24, 0x11, 0x06, 0x6d, 0xaf, 0x18, 0x0b, - 0x53, 0x84, 0xdb, 0xec, 0xf6, 0x8a, 0xd0, 0xdd, 0x5f, 0x0a, 0x7c, 0x5b, 0xb8, 0x31, 0x15, 0x37, 0x8e, 0x1e, 0x5f, - 0x14, 0x4c, 0x04, 0xe3, 0xd0, 0x54, 0x95, 0xf0, 0x6e, 0xba, 0x0a, 0x0a, 0x72, 0x2a, 0x2a, 0x1c, 0x78, 0xb0, 0x5e, - 0x66, 0xf4, 0x28, 0x12, 0x8a, 0x2d, 0xae, 0x6a, 0x4d, 0x14, 0x77, 0x19, 0x17, 0xa4, 0x2f, 0x87, 0xf9, 0xb7, 0xaa, - 0x1b, 0xba, 0x66, 0x55, 0xba, 0x45, 0xe2, 0x6b, 0x53, 0x8d, 0x46, 0x44, 0xe5, 0x7b, 0xe9, 0x03, 0xf3, 0x58, 0x4b, - 0x77, 0x3e, 0xed, 0x13, 0xae, 0x0c, 0x1c, 0x18, 0xfa, 0x48, 0xf7, 0x57, 0xeb, 0xea, 0x24, 0x1f, 0x57, 0x9f, 0x7c, - 0x0d, 0xcf, 0x1f, 0x8c, 0x9d, 0x76, 0x70, 0xcb, 0x21, 0x7d, 0xcc, 0xf9, 0x35, 0x33, 0xbd, 0x45, 0xab, 0xbd, 0x51, - 0xa3, 0x2e, 0xb0, 0x99, 0x4c, 0xd3, 0x63, 0xfe, 0xe9, 0xad, 0xe8, 0xc1, 0x05, 0x27, 0x89, 0x2f, 0x20, 0xe1, 0x86, - 0xed, 0xd5, 0xc7, 0x47, 0xaa, 0xeb, 0xd6, 0x09, 0x25, 0x76, 0x23, 0x95, 0xed, 0xa0, 0x42, 0xc8, 0x0e, 0xf7, 0xc4, - 0xd5, 0x1b, 0xec, 0x73, 0x88, 0xd3, 0xd1, 0x00, 0x29, 0x93, 0x26, 0xf6, 0x25, 0x8c, 0xf7, 0xc5, 0x1c, 0x36, 0x2b, - 0x48, 0xbc, 0xea, 0xc2, 0x29, 0x94, 0x58, 0xd9, 0xf3, 0xea, 0x78, 0x1d, 0xdd, 0xe4, 0x23, 0x9a, 0x32, 0xd0, 0xdc, - 0xbd, 0xe5, 0x2e, 0x17, 0x18, 0xdd, 0x6b, 0xf3, 0xf1, 0xdd, 0xce, 0x68, 0x4d, 0x12, 0xf9, 0xc6, 0xb7, 0xf9, 0x70, - 0xf3, 0xf8, 0x85, 0x86, 0xe2, 0x00, 0xd7, 0x71, 0xb8, 0xfd, 0xe1, 0xb2, 0x2a, 0xf7, 0xaa, 0x1f, 0xd4, 0xc0, 0x91, - 0x52, 0x4f, 0x0d, 0x67, 0x61, 0x4f, 0x30, 0xe1, 0xd4, 0x81, 0xb3, 0xe6, 0x83, 0x90, 0x73, 0xf9, 0xd7, 0x2e, 0x94, - 0x73, 0x37, 0x6e, 0x16, 0x9e, 0x06, 0x36, 0x76, 0x86, 0x3a, 0x5c, 0xea, 0xce, 0x6c, 0x49, 0x3c, 0xc3, 0x8f, 0xbb, - 0x9a, 0x08, 0x4b, 0xed, 0x81, 0xaf, 0x57, 0xbc, 0x9c, 0xfb, 0xdb, 0xe1, 0x8d, 0xe2, 0x82, 0x30, 0x33, 0xbe, 0x88, - 0x4a, 0x93, 0x2c, 0x69, 0xf8, 0x36, 0xb2, 0x19, 0x75, 0xed, 0xbb, 0x68, 0x45, 0x50, 0x32, 0x22, 0x54, 0x71, 0x68, - 0xc6, 0x50, 0x06, 0xe8, 0x58, 0x45, 0x69, 0xcf, 0xd7, 0x06, 0xb3, 0x4e, 0x36, 0x73, 0x04, 0x74, 0x44, 0xdf, 0x2f, - 0x5f, 0xd4, 0xb3, 0x7f, 0xdd, 0x1f, 0x1e, 0xbe, 0xc8, 0x55, 0x7d, 0xd4, 0x00, 0xfc, 0x8e, 0x54, 0xf5, 0xe8, 0x8d, - 0xe5, 0x57, 0x5a, 0x82, 0xad, 0x66, 0x07, 0x46, 0x9d, 0xa4, 0x8d, 0xd4, 0x86, 0xcc, 0x32, 0x67, 0x52, 0x28, 0x04, - 0x2d, 0x3d, 0x9e, 0x63, 0x31, 0x05, 0x50, 0xd2, 0xe5, 0x8a, 0x88, 0x0b, 0x06, 0x61, 0x15, 0x87, 0x31, 0x2c, 0xa4, - 0x69, 0x3d, 0xdb, 0xce, 0xa2, 0x51, 0x83, 0xd0, 0x35, 0x86, 0x44, 0x85, 0x99, 0xf5, 0x8c, 0x83, 0x5c, 0x6a, 0xbb, - 0x20, 0x4f, 0x7e, 0x73, 0x15, 0x03, 0xd0, 0x63, 0x22, 0x79, 0x5a, 0xd5, 0x44, 0x96, 0x90, 0xcf, 0xa4, 0x61, 0xd3, - 0xfb, 0xc3, 0x37, 0x31, 0x3d, 0xfa, 0xd8, 0xd5, 0xd6, 0x1f, 0xa2, 0xe4, 0xb9, 0x17, 0x29, 0x5f, 0xeb, 0xb4, 0x65, - 0x77, 0xa2, 0x4d, 0xd0, 0x44, 0xdb, 0x82, 0xb0, 0x05, 0x3a, 0xd0, 0x67, 0x3c, 0x1a, 0x2e, 0x1b, 0x51, 0x16, 0x8b, - 0x94, 0x28, 0x87, 0xfc, 0xe6, 0xec, 0x11, 0xe2, 0xb4, 0x16, 0x46, 0x03, 0xcb, 0xbd, 0x60, 0x18, 0x45, 0x17, 0xec, - 0x81, 0x4f, 0x2b, 0x52, 0x5c, 0x7d, 0xbb, 0x58, 0xf3, 0xba, 0x32, 0xd1, 0x36, 0x98, 0xf0, 0x0e, 0x1a, 0x1e, 0x61, - 0xaf, 0x71, 0x4e, 0x83, 0xae, 0xeb, 0xc9, 0xd3, 0x8a, 0x8c, 0x4d, 0x65, 0xa5, 0x1e, 0x01, 0xb7, 0xd8, 0xdb, 0x7e, - 0xd4, 0x36, 0x07, 0x7b, 0x36, 0xb6, 0xea, 0xc6, 0xb6, 0xc7, 0x10, 0xdc, 0x3a, 0x41, 0x3e, 0xdd, 0x29, 0x7d, 0x9e, - 0xe7, 0x4b, 0x6b, 0x13, 0xe8, 0xf0, 0xba, 0x35, 0xed, 0x71, 0x84, 0x11, 0x11, 0xb7, 0x99, 0x2e, 0x58, 0x58, 0x4a, - 0x6f, 0xa9, 0xae, 0x88, 0xe1, 0xd9, 0x0e, 0x59, 0x0d, 0x40, 0x2f, 0xb0, 0x3f, 0x94, 0xe7, 0x25, 0x5c, 0xe8, 0xf9, - 0xf0, 0xd1, 0x45, 0x95, 0x97, 0xa5, 0x1d, 0x66, 0x7b, 0xd6, 0xdd, 0xa0, 0xc2, 0xf5, 0xa9, 0x5a, 0x9d, 0x50, 0x20, - 0x96, 0x9e, 0xa3, 0xbf, 0xef, 0x53, 0x47, 0x3c, 0xcf, 0x08, 0x61, 0x27, 0x36, 0x9b, 0x07, 0x20, 0xf6, 0x41, 0xc7, - 0x04, 0x01, 0x42, 0xd0, 0x10, 0xab, 0x3d, 0xa0, 0x1e, 0xbf, 0x33, 0xf4, 0x7d, 0x44, 0x7a, 0x13, 0xa0, 0x32, 0x05, - 0xc5, 0x89, 0xda, 0xa7, 0x24, 0x22, 0x27, 0x3f, 0xc9, 0x2e, 0x9b, 0xb1, 0xa8, 0x93, 0xc0, 0xf9, 0x88, 0x53, 0xb0, - 0x14, 0x0a, 0xe7, 0xc5, 0x33, 0x01, 0x7c, 0x3a, 0x67, 0x8b, 0x69, 0xe1, 0x8b, 0x1c, 0x94, 0xcd, 0xa4, 0xc7, 0xf5, - 0x38, 0xb7, 0x7d, 0x4c, 0x38, 0x2a, 0xca, 0xd8, 0xd8, 0x5b, 0x75, 0x66, 0x8c, 0xf0, 0xd5, 0x44, 0xa8, 0xf7, 0x63, - 0x9c, 0xb7, 0xf7, 0x3d, 0x3e, 0xe2, 0x52, 0x6c, 0x2a, 0x84, 0xc2, 0x82, 0x9a, 0xa7, 0xf4, 0x47, 0x59, 0xe7, 0xd4, - 0xc8, 0x82, 0xf2, 0xb8, 0x82, 0x91, 0xa2, 0x4c, 0xfb, 0xec, 0xc9, 0x9e, 0x32, 0x48, 0x6c, 0xe7, 0x65, 0xa2, 0x2b, - 0x05, 0x0c, 0xa2, 0x54, 0x0a, 0x76, 0xb5, 0x2d, 0x14, 0xc9, 0x20, 0x1c, 0x43, 0xbb, 0x11, 0x47, 0x55, 0xe6, 0x90, - 0x84, 0x7c, 0xcd, 0xd7, 0x38, 0xb3, 0xdd, 0x1c, 0x52, 0x18, 0x6c, 0x51, 0x4d, 0x46, 0x81, 0xd0, 0x6e, 0x41, 0x40, - 0xe8, 0xd2, 0x85, 0xdf, 0xe0, 0x97, 0x47, 0xa9, 0x6c, 0x26, 0x38, 0x4f, 0x17, 0x6e, 0xe1, 0x97, 0x1e, 0xb5, 0x62, - 0xc7, 0x5b, 0x6b, 0xe3, 0x12, 0xe5, 0xa2, 0x65, 0xfe, 0x23, 0xf6, 0xb8, 0x80, 0x03, 0x5b, 0x60, 0x6d, 0xe8, 0x0e, - 0x95, 0x61, 0x34, 0x70, 0xe2, 0x01, 0x24, 0xb5, 0xbb, 0x61, 0x49, 0x5b, 0xd4, 0x7f, 0x32, 0xd7, 0xea, 0x1a, 0x34, - 0x81, 0x59, 0xab, 0xc5, 0x36, 0x4d, 0x85, 0x1c, 0x32, 0xaa, 0x1a, 0xb0, 0x52, 0x6d, 0x87, 0x34, 0x59, 0x22, 0x87, - 0x24, 0x71, 0x27, 0x73, 0x06, 0x55, 0x56, 0x7a, 0xd1, 0xff, 0xa8, 0x44, 0xe4, 0x43, 0x5e, 0xff, 0xa4, 0x8a, 0x67, - 0x99, 0xd4, 0x8f, 0xc2, 0xa1, 0x4a, 0x63, 0x93, 0x0d, 0x6d, 0x00, 0xa3, 0x0f, 0x73, 0xa8, 0x2c, 0x74, 0x6c, 0x95, - 0xfb, 0x6e, 0x5a, 0xc9, 0xb1, 0x21, 0x9f, 0xcc, 0x18, 0x30, 0xdf, 0x7e, 0x0b, 0x62, 0x8f, 0x5b, 0xcc, 0x38, 0xdc, - 0x4b, 0x7e, 0xbe, 0x4c, 0x44, 0xc1, 0x1f, 0x4e, 0xc3, 0x0e, 0x7c, 0xd3, 0x21, 0xa6, 0xcd, 0x15, 0x33, 0x64, 0x06, - 0xa5, 0x6d, 0x09, 0x31, 0x2d, 0x78, 0x4a, 0xa4, 0xfb, 0xf7, 0xfe, 0xc4, 0xde, 0xd3, 0x7c, 0x21, 0x3f, 0x59, 0x9d, - 0x83, 0xbe, 0x55, 0x97, 0x3e, 0x86, 0x17, 0xc6, 0x7d, 0x00, 0x50, 0xb9, 0xbd, 0x6d, 0xc5, 0x71, 0x7b, 0x5f, 0x85, - 0xf8, 0x83, 0x19, 0x66, 0x1c, 0xaf, 0x52, 0x64, 0x63, 0x81, 0xe9, 0x94, 0x59, 0xa9, 0x5b, 0x35, 0x2b, 0xfb, 0xc7, - 0xf4, 0x5f, 0x1a, 0x0d, 0xb0, 0x2f, 0x17, 0xf9, 0xf9, 0x76, 0x99, 0x28, 0xb0, 0xc2, 0x22, 0xd1, 0x7b, 0x17, 0x40, - 0xba, 0x83, 0x48, 0xc6, 0x9f, 0xf7, 0x70, 0xd1, 0xf0, 0xf0, 0x17, 0x39, 0x68, 0xd9, 0x79, 0xe5, 0x44, 0x69, 0x3e, - 0xaf, 0xd8, 0x09, 0x44, 0x9e, 0x3a, 0x45, 0x98, 0xfe, 0x7d, 0x72, 0xe5, 0xb5, 0x47, 0x4e, 0xce, 0x5e, 0x40, 0xbf, - 0x26, 0x6e, 0x9f, 0x9f, 0x65, 0x1d, 0xfe, 0xb1, 0x44, 0x85, 0xb0, 0x52, 0xe8, 0x41, 0x55, 0x08, 0x5f, 0x70, 0xe2, - 0x00, 0x4e, 0x3d, 0x7c, 0x78, 0xc6, 0xdf, 0x0e, 0x43, 0xfb, 0xf8, 0x99, 0xb3, 0x69, 0x89, 0xf1, 0x12, 0x83, 0x45, - 0xb5, 0xd4, 0x78, 0x7e, 0xff, 0x34, 0xeb, 0xe9, 0x9e, 0xb1, 0x4f, 0x8b, 0x9e, 0xac, 0x6a, 0x9a, 0x37, 0x24, 0xce, - 0x7f, 0xd8, 0xfc, 0x5a, 0x1b, 0x1f, 0xec, 0xdc, 0x56, 0x25, 0x47, 0xd6, 0x05, 0xae, 0xcb, 0xaa, 0x55, 0xd5, 0x37, - 0x03, 0xce, 0x49, 0x8f, 0xc5, 0x4b, 0x9d, 0xdd, 0x2f, 0xe8, 0x8f, 0x66, 0x3a, 0x5a, 0x1f, 0x7d, 0x70, 0x2d, 0x42, - 0xd5, 0xa4, 0x33, 0xba, 0x37, 0xbf, 0xc3, 0x39, 0xe5, 0x33, 0xd7, 0xf1, 0xb9, 0x5b, 0x0b, 0xe5, 0x09, 0x8f, 0x16, - 0x1a, 0x85, 0xa1, 0x3b, 0x77, 0x8f, 0xe0, 0x5a, 0x24, 0xcd, 0xc8, 0xde, 0xc2, 0x39, 0xd3, 0xf8, 0x4c, 0x7f, 0x36, - 0x0b, 0xf5, 0xa7, 0x3e, 0x14, 0x14, 0x11, 0xf3, 0x2b, 0xa6, 0x62, 0x28, 0x25, 0xc1, 0x43, 0x44, 0x04, 0x5a, 0x47, - 0x51, 0x3e, 0x55, 0x57, 0x57, 0xca, 0xea, 0x97, 0xb3, 0x2c, 0x28, 0x92, 0xd9, 0x14, 0x59, 0xb9, 0xe2, 0x8f, 0x4e, - 0x72, 0x96, 0xeb, 0x42, 0x40, 0xce, 0x3e, 0xc0, 0x89, 0xfd, 0x9b, 0x41, 0xe0, 0xb6, 0xb6, 0xd6, 0xfe, 0x48, 0x50, - 0x63, 0x14, 0x7c, 0x8b, 0x00, 0x8c, 0xc4, 0xd0, 0x46, 0xf9, 0xe4, 0x56, 0xba, 0x50, 0x51, 0xbd, 0x3f, 0x71, 0xf7, - 0x3f, 0xbf, 0xbb, 0xc9, 0x0d, 0x1b, 0x77, 0x69, 0x0e, 0x4d, 0xe6, 0xc9, 0x39, 0xda, 0xc8, 0xee, 0xba, 0x5f, 0x06, - 0xf9, 0x2d, 0x5f, 0x92, 0xf4, 0x74, 0x44, 0x60, 0x4b, 0xcb, 0x8f, 0x48, 0x45, 0x49, 0x22, 0x90, 0x63, 0xad, 0x00, - 0x50, 0x33, 0x21, 0x95, 0x8a, 0x1c, 0x45, 0x9e, 0x8c, 0x7a, 0x33, 0xa7, 0x24, 0x2d, 0x69, 0x37, 0xa8, 0x31, 0x2c, - 0x87, 0xaf, 0xb9, 0x36, 0x4b, 0x7d, 0xad, 0xa4, 0xec, 0xc4, 0xf6, 0x82, 0x05, 0x94, 0x38, 0xa6, 0xe0, 0x82, 0xd5, - 0x58, 0x9a, 0x36, 0xaf, 0x27, 0x18, 0xd0, 0x32, 0x97, 0x76, 0xd9, 0x12, 0xf2, 0x95, 0xfa, 0x7d, 0x58, 0x8c, 0x90, - 0x7c, 0x63, 0xa1, 0x58, 0xda, 0xaa, 0x55, 0xb9, 0xf3, 0x1c, 0x3f, 0xd0, 0xa4, 0x48, 0x1d, 0xed, 0x61, 0xfa, 0x16, - 0x8e, 0xc4, 0xe0, 0x66, 0x4e, 0xb9, 0xa4, 0x4c, 0xe3, 0xd2, 0x9f, 0xa4, 0xff, 0xaa, 0x2f, 0x43, 0x3e, 0xc1, 0x51, - 0xac, 0xfe, 0x83, 0x6a, 0xcc, 0x40, 0x40, 0xea, 0x4b, 0x10, 0x15, 0xc3, 0x68, 0xe6, 0x10, 0xdd, 0xa0, 0xf5, 0x99, - 0x3a, 0x91, 0xce, 0x5e, 0x6c, 0x70, 0xd2, 0x97, 0x73, 0xa2, 0x79, 0xe1, 0x3b, 0x8c, 0xf7, 0x81, 0x01, 0x0c, 0x0a, - 0xf3, 0x60, 0x0c, 0xec, 0xb2, 0x26, 0x6d, 0x29, 0xb8, 0x41, 0x0d, 0x34, 0x81, 0x07, 0x78, 0x3a, 0x89, 0x90, 0x8b, - 0x7c, 0x66, 0x71, 0x27, 0xbb, 0x98, 0x52, 0x6b, 0x7e, 0x2c, 0x84, 0x85, 0xfe, 0xdd, 0x62, 0x2b, 0xcb, 0x1d, 0x3c, - 0x13, 0x11, 0x54, 0x05, 0x0a, 0xbc, 0x72, 0x79, 0x43, 0xa7, 0x25, 0x70, 0xf0, 0x3e, 0xb5, 0xe2, 0x06, 0x07, 0xbe, - 0x0d, 0x2a, 0x5d, 0xb0, 0x1f, 0xb4, 0xeb, 0xdf, 0x7b, 0xae, 0xc2, 0x22, 0xea, 0x21, 0xde, 0x6a, 0xae, 0x57, 0x77, - 0xf7, 0xbe, 0xd7, 0xf1, 0x59, 0x53, 0xcb, 0x1e, 0x7f, 0xc6, 0x10, 0x0a, 0x4e, 0xd0, 0x2a, 0x15, 0x12, 0x30, 0xf0, - 0xc7, 0x2d, 0x6c, 0xfc, 0x92, 0xa5, 0xdb, 0x11, 0x4b, 0x7f, 0xfd, 0xba, 0xa2, 0xc9, 0xae, 0xba, 0xa9, 0x27, 0xa0, - 0x88, 0xbd, 0xa3, 0x55, 0x76, 0xb8, 0x4a, 0xcd, 0x7b, 0xc5, 0xbb, 0x1e, 0xf8, 0x94, 0x0e, 0xcc, 0x28, 0xb0, 0x17, - 0xc4, 0x1c, 0x18, 0xeb, 0xc7, 0x46, 0x79, 0xd7, 0x4f, 0xbe, 0x4b, 0xd1, 0x46, 0xad, 0xaf, 0xfc, 0x41, 0x10, 0xdf, - 0x67, 0x46, 0xac, 0xbd, 0x04, 0x66, 0x30, 0xba, 0xd3, 0x36, 0x1d, 0x76, 0xe5, 0x3e, 0x9e, 0x1f, 0xb2, 0xde, 0x41, - 0x40, 0xa5, 0xe8, 0x47, 0x81, 0x4b, 0x26, 0x30, 0x98, 0x83, 0x23, 0xdb, 0x8b, 0x3d, 0xf9, 0x44, 0xcc, 0x85, 0x28, - 0x45, 0x33, 0x46, 0x01, 0xc1, 0xc8, 0x61, 0x85, 0xed, 0x3f, 0xc2, 0x76, 0x01, 0x70, 0x8b, 0x87, 0x0c, 0x7b, 0x5e, - 0xe3, 0x4d, 0xbc, 0x1d, 0x35, 0xcc, 0x99, 0xd4, 0x5b, 0xd0, 0x4e, 0x8f, 0x21, 0xf9, 0x7d, 0x1a, 0x24, 0xa3, 0x22, - 0xf7, 0x28, 0x12, 0x84, 0xd7, 0x45, 0x4e, 0x5a, 0x80, 0x75, 0x77, 0xe8, 0xa6, 0x5f, 0x01, 0x62, 0xfa, 0x5e, 0x02, - 0xfe, 0x44, 0x6e, 0x22, 0x16, 0xbc, 0xdd, 0x34, 0xc4, 0x1d, 0x4c, 0x80, 0xa1, 0x11, 0x9e, 0x41, 0xd0, 0x08, 0x92, - 0x11, 0xdd, 0x6d, 0xee, 0xa7, 0xcc, 0x7f, 0x56, 0xe4, 0xc7, 0xb2, 0x31, 0xae, 0x79, 0xd3, 0xde, 0xc5, 0x6f, 0x11, - 0xa9, 0x00, 0x62, 0x67, 0xca, 0x2c, 0x54, 0x89, 0xc9, 0xd7, 0x85, 0x8d, 0x7d, 0x6e, 0x94, 0x25, 0xdb, 0xe7, 0xf5, - 0xd7, 0x66, 0xd8, 0x92, 0x66, 0xb6, 0xb7, 0x39, 0xe3, 0xb3, 0x8a, 0x89, 0x85, 0x17, 0x05, 0xce, 0xfd, 0xed, 0xd7, - 0xfd, 0xf9, 0x70, 0x95, 0x2d, 0xdb, 0x29, 0x53, 0x8f, 0x23, 0x25, 0xcb, 0x5a, 0x7f, 0xbb, 0x32, 0x93, 0xb7, 0x6e, - 0xd1, 0x13, 0xec, 0xa8, 0x35, 0xf3, 0x25, 0x47, 0xda, 0xba, 0x87, 0x93, 0xec, 0xba, 0xc0, 0x2e, 0xef, 0x04, 0xd0, - 0xb4, 0x74, 0x42, 0xf1, 0x73, 0x25, 0xb4, 0xac, 0x1d, 0xe0, 0x24, 0x7e, 0xfa, 0x62, 0xe2, 0xa5, 0x98, 0xad, 0xc1, - 0x36, 0xbf, 0x62, 0x5e, 0xc4, 0x60, 0xcf, 0x8d, 0x0a, 0xe1, 0x8c, 0xf3, 0xbe, 0x05, 0xb3, 0xf4, 0x1b, 0xaf, 0xdc, - 0xe6, 0x73, 0x82, 0xfd, 0x96, 0x16, 0xc1, 0xc0, 0xc4, 0x5d, 0xf5, 0x5a, 0xe3, 0x2c, 0x84, 0xa8, 0x6b, 0xb9, 0x2f, - 0x62, 0xe6, 0x36, 0xa7, 0xe9, 0x5d, 0xad, 0xc9, 0x8c, 0xfd, 0xe2, 0x4a, 0x33, 0xeb, 0xbb, 0xef, 0x20, 0x6b, 0xad, - 0x2a, 0xf4, 0x2b, 0x52, 0xcf, 0x64, 0xfd, 0x27, 0xb0, 0x19, 0x8b, 0x1d, 0x16, 0x4b, 0x2b, 0x75, 0xe7, 0xaa, 0xf4, - 0x03, 0x9e, 0x54, 0x00, 0x72, 0x11, 0xd0, 0x99, 0x6e, 0x3d, 0x77, 0x8b, 0x45, 0x3d, 0xea, 0xc1, 0xad, 0xbf, 0xb7, - 0x1a, 0x06, 0x41, 0xac, 0x63, 0xbf, 0x22, 0x78, 0x3c, 0x5e, 0x89, 0xdf, 0x0b, 0xaf, 0xc8, 0x0f, 0x5b, 0x1e, 0xff, - 0x7c, 0x01, 0x65, 0xfa, 0x49, 0x34, 0xed, 0xfc, 0x6c, 0xc3, 0xc2, 0xa4, 0x7c, 0x3a, 0x8f, 0xfc, 0x1e, 0xcd, 0xcd, - 0x15, 0xb4, 0xdc, 0xf3, 0x03, 0x97, 0xf2, 0x7f, 0x16, 0x29, 0x4b, 0x6a, 0x85, 0x66, 0xd9, 0x36, 0xc1, 0xd1, 0xdd, - 0x9e, 0xe2, 0xc1, 0x73, 0x9c, 0x50, 0x68, 0x6f, 0x4a, 0xbd, 0x55, 0x85, 0x9a, 0xa8, 0xb5, 0x85, 0x02, 0x65, 0xfd, - 0x88, 0xf6, 0x51, 0x71, 0xc4, 0x0d, 0x23, 0x3d, 0xea, 0x6f, 0x6a, 0x6d, 0x91, 0x5d, 0x47, 0xed, 0x97, 0xb5, 0xfb, - 0x7d, 0x12, 0x24, 0xff, 0x6d, 0x05, 0xc8, 0xac, 0x0d, 0xd5, 0x9b, 0x80, 0x69, 0x44, 0x31, 0x47, 0xc1, 0x8f, 0xd1, - 0x92, 0x42, 0xa3, 0x0c, 0x2e, 0x1c, 0x11, 0x66, 0x2d, 0xb5, 0xe4, 0x19, 0x43, 0xf0, 0xbc, 0xd1, 0xd0, 0x91, 0xf0, - 0xb5, 0xe9, 0x5d, 0x76, 0x66, 0x36, 0x4c, 0xce, 0x3d, 0xa2, 0x21, 0x9b, 0x7a, 0xaa, 0x28, 0x01, 0xf7, 0xcd, 0x72, - 0x7c, 0x75, 0x50, 0xb2, 0x26, 0xb5, 0x57, 0xc1, 0x6e, 0x1f, 0x72, 0x73, 0x19, 0xbd, 0x35, 0x54, 0x6b, 0xf8, 0xde, - 0x48, 0xd6, 0xb0, 0xca, 0x35, 0x90, 0xd8, 0xce, 0x8f, 0x30, 0x49, 0x45, 0x77, 0xf9, 0x16, 0x34, 0xde, 0x51, 0x95, - 0xcb, 0x4e, 0xeb, 0xda, 0xbb, 0x03, 0x37, 0x61, 0xd8, 0xfa, 0xd4, 0x8d, 0x8e, 0xf4, 0xfd, 0x80, 0x0d, 0x9a, 0x95, - 0x4a, 0x03, 0x4e, 0xf9, 0x05, 0x15, 0xad, 0xf3, 0xbb, 0x25, 0x5f, 0xec, 0x19, 0xee, 0x83, 0x11, 0x32, 0x26, 0x8e, - 0xc0, 0x8e, 0x1a, 0xe0, 0x29, 0x61, 0xc6, 0xe1, 0xc7, 0x9e, 0xdb, 0xd7, 0xc6, 0xa0, 0x7f, 0xa5, 0xd9, 0x50, 0x40, - 0x8d, 0xf6, 0xb8, 0x92, 0x54, 0x3a, 0x86, 0x19, 0x93, 0xc2, 0x87, 0x54, 0x28, 0x73, 0xfc, 0xbb, 0x73, 0x4d, 0xb1, - 0x66, 0x38, 0x57, 0x23, 0xd3, 0x86, 0xf3, 0xbf, 0x1a, 0xf3, 0x5b, 0x8e, 0xef, 0x20, 0xaa, 0x9e, 0x8e, 0x41, 0x87, - 0x50, 0x4a, 0x50, 0x76, 0x65, 0x42, 0x55, 0x03, 0xfd, 0xa2, 0x19, 0x6d, 0x9a, 0xd6, 0x8f, 0x91, 0xf3, 0xbf, 0x6e, - 0xbf, 0xb6, 0x93, 0x8b, 0xd6, 0x0a, 0xeb, 0xe2, 0x07, 0x3f, 0x30, 0xe4, 0xb5, 0x7b, 0x7e, 0x76, 0xab, 0x5c, 0xd9, - 0x53, 0x5b, 0x3c, 0x75, 0xc1, 0x97, 0xe9, 0xfa, 0x18, 0xbc, 0x2c, 0x20, 0x35, 0x8d, 0xaa, 0x75, 0xec, 0x13, 0x16, - 0x5a, 0xec, 0x3a, 0x6f, 0xd7, 0x2f, 0x4f, 0xaa, 0x89, 0x57, 0x2c, 0x03, 0x3a, 0x3f, 0xb3, 0x29, 0xf6, 0x91, 0x16, - 0x97, 0x0d, 0xff, 0x32, 0xa0, 0xe7, 0xd9, 0x40, 0x9e, 0xf9, 0x59, 0x7f, 0xae, 0x3f, 0xbe, 0xe5, 0x21, 0xa1, 0x14, - 0xb7, 0x35, 0x4e, 0xef, 0x1a, 0xdb, 0xcc, 0x3b, 0xb3, 0xb4, 0x8f, 0x9d, 0x66, 0x3e, 0xa2, 0x22, 0x5d, 0x70, 0x12, - 0xb6, 0xa7, 0x43, 0xba, 0x92, 0x6d, 0x16, 0x9a, 0x39, 0xf5, 0xa5, 0x71, 0x59, 0x9c, 0xd7, 0x69, 0x73, 0x31, 0xf7, - 0x82, 0xae, 0x03, 0x38, 0xd7, 0x29, 0x47, 0x70, 0x55, 0x11, 0x28, 0x9a, 0x9a, 0xb9, 0xa2, 0x78, 0x68, 0x2d, 0x76, - 0x73, 0x6b, 0xf7, 0x53, 0x8c, 0xb8, 0xd4, 0xa5, 0x2a, 0x51, 0x92, 0x6c, 0x59, 0x29, 0x90, 0xc9, 0x82, 0xac, 0x39, - 0x49, 0x15, 0x0e, 0xfa, 0x37, 0x87, 0x74, 0xf6, 0x62, 0xd8, 0x87, 0x70, 0xe6, 0x6b, 0xa9, 0x0a, 0xa3, 0x59, 0xac, - 0xe6, 0x39, 0x88, 0x54, 0x6d, 0x1f, 0x28, 0xa7, 0xca, 0x7d, 0x5b, 0x40, 0xe6, 0x46, 0xca, 0x4b, 0x51, 0x47, 0x6e, - 0x78, 0x4a, 0xbf, 0x36, 0x3d, 0x10, 0xa3, 0xd5, 0xb0, 0xa3, 0x8d, 0x66, 0xb3, 0x59, 0x14, 0x53, 0x8f, 0x43, 0x9b, - 0xd4, 0x7c, 0x1b, 0x51, 0xaf, 0x50, 0x35, 0xb3, 0x6f, 0x4c, 0x3d, 0x62, 0xc9, 0x9c, 0xe2, 0x35, 0x14, 0x26, 0xc9, - 0x3d, 0x8b, 0x2d, 0xea, 0x16, 0x6d, 0x6e, 0xce, 0x1c, 0x1b, 0x92, 0xb8, 0x8a, 0x4b, 0x99, 0xae, 0x34, 0x1e, 0x05, - 0xc2, 0x61, 0x25, 0xda, 0x4e, 0x30, 0x1b, 0xf3, 0xf4, 0x83, 0x9f, 0x92, 0x9f, 0x7b, 0x04, 0x4c, 0xb3, 0x2f, 0x60, - 0x2b, 0xe9, 0xce, 0x8c, 0xf8, 0x48, 0x41, 0xce, 0xe1, 0x2b, 0x86, 0xe9, 0x7b, 0x9b, 0xc8, 0x72, 0x1f, 0xe7, 0x53, - 0x82, 0x32, 0x39, 0xa9, 0x76, 0x68, 0xbc, 0x81, 0xd8, 0x1b, 0x20, 0x9e, 0x86, 0xb0, 0x04, 0x4f, 0x23, 0x60, 0x90, - 0xf8, 0xbc, 0x5c, 0xc5, 0x43, 0x2d, 0x6e, 0x7c, 0x97, 0x79, 0x08, 0x70, 0xb6, 0x0c, 0x43, 0x6d, 0x62, 0x92, 0xfb, - 0xb3, 0x06, 0x74, 0x27, 0xa2, 0x62, 0x49, 0x66, 0x97, 0x75, 0x15, 0x85, 0xf9, 0x77, 0x5e, 0x2f, 0x53, 0x27, 0x9c, - 0x2d, 0xde, 0xb8, 0x0d, 0x80, 0xe9, 0x42, 0x7b, 0xaa, 0x93, 0x13, 0x93, 0xe2, 0xd7, 0x50, 0x1a, 0xd6, 0x09, 0x0d, - 0x14, 0x89, 0xfa, 0x79, 0xb4, 0x9e, 0x98, 0xa2, 0x38, 0xff, 0x11, 0x91, 0x0c, 0x4c, 0x12, 0xc8, 0x60, 0xb4, 0x7b, - 0xc5, 0x9a, 0x50, 0xac, 0xfd, 0xa4, 0x65, 0xd3, 0x99, 0xfb, 0x36, 0x83, 0x98, 0xbd, 0x1f, 0x04, 0x0f, 0x04, 0xff, - 0xe5, 0x56, 0x27, 0x8c, 0xa0, 0x04, 0xc3, 0xec, 0x30, 0xff, 0x89, 0xac, 0xba, 0x2d, 0xe8, 0xa8, 0x57, 0xe4, 0xaa, - 0x77, 0xe2, 0x52, 0x67, 0x12, 0x55, 0x4f, 0x7e, 0x9e, 0xb8, 0xfb, 0x56, 0x36, 0x46, 0x0b, 0xdc, 0xe7, 0xc8, 0x27, - 0x57, 0x6e, 0x66, 0xdb, 0xc8, 0xa8, 0xa6, 0x28, 0x12, 0x8b, 0x98, 0xca, 0xbc, 0x79, 0x8e, 0xfb, 0xf0, 0xaa, 0xb9, - 0x83, 0xef, 0xb7, 0x39, 0xd8, 0xca, 0xac, 0xdc, 0xe5, 0xf2, 0x6d, 0x7a, 0x68, 0xd0, 0xfd, 0xae, 0x73, 0xa4, 0x4b, - 0xef, 0xa6, 0x72, 0x5b, 0xb7, 0x3b, 0x53, 0xe7, 0x23, 0xf5, 0xd4, 0xf1, 0xf9, 0x99, 0x74, 0x63, 0xb7, 0xfe, 0x53, - 0x35, 0xc1, 0x4f, 0xf9, 0x02, 0xb4, 0x34, 0x55, 0x7c, 0x2c, 0x28, 0xa3, 0x16, 0xdd, 0xc1, 0x57, 0x6e, 0xb9, 0x15, - 0xf4, 0x2b, 0x9f, 0xab, 0xbc, 0x70, 0x8d, 0x5c, 0x3a, 0x7b, 0xe1, 0x04, 0x36, 0xf5, 0xe0, 0x9d, 0xf1, 0x57, 0xc1, - 0x25, 0xe0, 0xda, 0x10, 0x07, 0x23, 0x25, 0x89, 0xf7, 0xd5, 0xc0, 0x1b, 0x11, 0xf1, 0x8f, 0x82, 0xa1, 0x51, 0xfa, - 0x96, 0xfa, 0x18, 0x5b, 0x0c, 0xb3, 0x3e, 0xaa, 0x94, 0xab, 0xb3, 0xb9, 0xe0, 0xd4, 0x39, 0xfa, 0x13, 0x46, 0xdc, - 0xf3, 0x00, 0xe7, 0xac, 0xae, 0x7f, 0x06, 0xe7, 0xb5, 0xfd, 0xcc, 0x38, 0x1f, 0x8a, 0xa6, 0x44, 0xeb, 0xdd, 0x78, - 0xa7, 0x3c, 0x9b, 0x2d, 0xe7, 0x67, 0x15, 0x5e, 0x0d, 0xf7, 0x19, 0x9f, 0x5e, 0xfa, 0x1d, 0x98, 0x3c, 0x0e, 0xba, - 0xf4, 0xbe, 0x72, 0x78, 0xef, 0x4e, 0xc5, 0x4a, 0x15, 0x35, 0xe2, 0xd8, 0xa1, 0x7b, 0x8d, 0xc7, 0xbd, 0x0b, 0xcc, - 0x1a, 0xab, 0x93, 0xc3, 0x43, 0x6b, 0xab, 0x7c, 0x5d, 0x65, 0x84, 0x9d, 0xc4, 0x37, 0xcb, 0xc6, 0x94, 0x48, 0xb0, - 0xbf, 0x0d, 0x94, 0x63, 0x38, 0x10, 0xe1, 0x61, 0xc2, 0x9b, 0xac, 0xc2, 0xbc, 0x96, 0x8a, 0x32, 0xab, 0x1d, 0xfe, - 0x5a, 0x71, 0x8d, 0x68, 0x07, 0x4b, 0xae, 0xa4, 0x0d, 0x66, 0xd1, 0xa5, 0xa4, 0x61, 0x75, 0xc3, 0xa1, 0x5e, 0xdf, - 0x15, 0x5c, 0xd5, 0xb6, 0x66, 0x91, 0xfc, 0x45, 0xd9, 0x8e, 0x95, 0x08, 0x9b, 0x29, 0xf3, 0x3c, 0xfb, 0x3f, 0xa2, - 0x4a, 0x87, 0xdc, 0x02, 0xa6, 0xf6, 0x43, 0xba, 0x42, 0x52, 0x8c, 0x0d, 0xda, 0x4f, 0x4a, 0x57, 0x32, 0xef, 0xf8, - 0x8d, 0xc5, 0x75, 0xcb, 0x50, 0xe4, 0x62, 0x33, 0x56, 0x17, 0x1b, 0xc0, 0xc2, 0x2a, 0x07, 0xcc, 0x46, 0x14, 0xcd, - 0xa2, 0x6c, 0xca, 0xa3, 0xed, 0x16, 0xaf, 0x5b, 0xb4, 0xfa, 0xfb, 0x33, 0xd1, 0x33, 0x5b, 0x27, 0x55, 0x9d, 0x65, - 0xbd, 0x7f, 0x65, 0xc5, 0x5c, 0xe1, 0xe1, 0x47, 0x7b, 0x6e, 0xe7, 0x88, 0xce, 0xfb, 0xbb, 0x9b, 0xfe, 0x85, 0xdd, - 0xfc, 0x7f, 0x49, 0x37, 0x61, 0x86, 0xf5, 0xe4, 0xf6, 0xd3, 0x4b, 0xac, 0x09, 0xe7, 0x3f, 0xb2, 0x89, 0x61, 0xdb, - 0x15, 0xd4, 0x16, 0x35, 0x66, 0x9c, 0x12, 0x3c, 0xf6, 0x81, 0x0a, 0xed, 0x61, 0xe2, 0x0a, 0x61, 0x54, 0x79, 0xaa, - 0x44, 0xfa, 0x5c, 0xfc, 0xb2, 0x4d, 0x64, 0xd0, 0x19, 0x87, 0xb2, 0x81, 0x9d, 0xdb, 0xb5, 0xca, 0xcc, 0xd6, 0xd2, - 0xfa, 0x8f, 0x99, 0x62, 0xf3, 0x7f, 0xc0, 0x12, 0xf5, 0x90, 0x47, 0x7e, 0x59, 0xb5, 0x08, 0xef, 0x0d, 0xe5, 0xe6, - 0x21, 0xc8, 0x2d, 0x8b, 0x0e, 0x7f, 0x60, 0x3e, 0x40, 0x8e, 0x60, 0x8c, 0x1a, 0xb0, 0x52, 0x4e, 0x21, 0x97, 0xf9, - 0x71, 0xaa, 0xc9, 0x50, 0xcb, 0x72, 0x9d, 0xb1, 0x4a, 0x23, 0xaf, 0x59, 0x99, 0xa7, 0x59, 0x91, 0x6b, 0x94, 0x0d, - 0x15, 0xd7, 0x9f, 0x91, 0xa3, 0x51, 0x1b, 0xd0, 0x10, 0xbb, 0xe3, 0x9c, 0xd8, 0x28, 0x73, 0xd4, 0x71, 0x72, 0x4b, - 0x9e, 0x59, 0x57, 0x33, 0x5b, 0x89, 0x93, 0x8b, 0x77, 0x9b, 0xb1, 0x6d, 0x77, 0x34, 0x2e, 0x99, 0x27, 0x8e, 0x73, - 0x74, 0x7d, 0xa3, 0xcd, 0x9e, 0x97, 0xec, 0xb8, 0xf8, 0x3f, 0x48, 0x0e, 0xdd, 0x3c, 0x1a, 0x11, 0xcc, 0xc5, 0x25, - 0x45, 0xa9, 0xe9, 0xe6, 0x48, 0x02, 0x1b, 0x1e, 0xff, 0xb9, 0x89, 0xae, 0xf8, 0x78, 0x6e, 0x56, 0x46, 0x14, 0x5b, - 0x9c, 0xd8, 0x9f, 0xed, 0x61, 0xd5, 0x7a, 0x44, 0xc2, 0x81, 0xb3, 0xce, 0xfa, 0x60, 0x9f, 0xeb, 0xd2, 0xff, 0xe0, - 0x07, 0x36, 0x12, 0x82, 0x8d, 0x61, 0xf5, 0xce, 0xfe, 0xa7, 0x66, 0xc5, 0x85, 0xae, 0x35, 0x3b, 0x5e, 0xf8, 0x57, - 0x5c, 0xe1, 0x2d, 0x49, 0x65, 0x25, 0x37, 0x2e, 0x77, 0x2a, 0xe3, 0x05, 0x55, 0x3a, 0x66, 0x61, 0xe8, 0x58, 0x4c, - 0xaf, 0x0e, 0x4a, 0xaf, 0x08, 0x68, 0xa8, 0xce, 0xb9, 0xab, 0x95, 0xd9, 0x04, 0x97, 0x11, 0x92, 0x4a, 0x81, 0xbb, - 0xc2, 0x90, 0xe9, 0x9d, 0x6f, 0x86, 0x7e, 0x30, 0x14, 0x66, 0x6e, 0x40, 0xd8, 0x32, 0x41, 0xa5, 0xc3, 0x9a, 0x15, - 0x7b, 0x41, 0x9b, 0x0c, 0xe6, 0x3c, 0xa2, 0xde, 0x6b, 0xa4, 0xbf, 0x73, 0xc2, 0x05, 0x38, 0x4a, 0x81, 0xc2, 0x80, - 0x2e, 0x6f, 0x3c, 0x40, 0x72, 0x89, 0x10, 0x63, 0x0d, 0x85, 0xd4, 0x26, 0x7e, 0x39, 0xbf, 0xe2, 0x9e, 0xf7, 0xb3, - 0xe3, 0xac, 0xeb, 0x5b, 0x03, 0x79, 0x98, 0x5f, 0xbf, 0xbd, 0xce, 0x7a, 0x90, 0xb3, 0x21, 0x71, 0xb1, 0xb2, 0xf3, - 0x8a, 0x76, 0x76, 0x45, 0x5b, 0xea, 0x6a, 0x54, 0xe1, 0xb6, 0x86, 0x29, 0x52, 0x54, 0xb1, 0xe1, 0x7a, 0x1b, 0xba, - 0x20, 0xe9, 0x8b, 0x35, 0x85, 0x84, 0x19, 0xbb, 0xa6, 0x30, 0x95, 0x3b, 0xa1, 0x47, 0x67, 0xc3, 0x40, 0x5f, 0x6c, - 0xfd, 0x02, 0xf4, 0xa7, 0x8d, 0x8d, 0x36, 0x7d, 0x4f, 0x54, 0x46, 0xcc, 0x29, 0xfa, 0xbc, 0xc3, 0xec, 0xd3, 0xfe, - 0x44, 0x77, 0xb0, 0x5a, 0x5f, 0xc6, 0x5f, 0x56, 0x6c, 0xd4, 0xc7, 0xd6, 0x33, 0x26, 0x89, 0x53, 0xc9, 0xed, 0x41, - 0x49, 0x41, 0x66, 0xde, 0x44, 0x0d, 0x19, 0x29, 0xad, 0x39, 0x8f, 0x20, 0xfe, 0x77, 0xae, 0x98, 0x99, 0x98, 0xf6, - 0x63, 0x5c, 0x52, 0x1f, 0x7f, 0xf7, 0xc4, 0x5b, 0xbb, 0x77, 0x9a, 0xa1, 0x63, 0xf6, 0x00, 0x81, 0x9c, 0x57, 0x5e, - 0xba, 0x60, 0x68, 0x6e, 0xad, 0x54, 0xb3, 0xa6, 0x51, 0xfe, 0xb3, 0xbb, 0x32, 0x05, 0x03, 0xfb, 0x44, 0xad, 0x3f, - 0xdb, 0xe5, 0x66, 0xea, 0x1b, 0xb3, 0x57, 0x03, 0x4e, 0x04, 0x66, 0x36, 0xdd, 0x54, 0xfa, 0xaf, 0xfb, 0xfe, 0x3b, - 0x16, 0xa0, 0xd8, 0xd9, 0xc8, 0x1f, 0x9a, 0x8a, 0xe0, 0xc6, 0x77, 0x67, 0x2f, 0x86, 0x2d, 0x0a, 0x05, 0x5f, 0x46, - 0x99, 0xee, 0x32, 0xf2, 0x07, 0x0d, 0x6d, 0xf0, 0x4b, 0x7a, 0x63, 0x1b, 0x97, 0x61, 0x1f, 0xed, 0x61, 0x12, 0xbb, - 0x60, 0x68, 0x6b, 0x62, 0x41, 0x50, 0x35, 0x75, 0xde, 0x30, 0x22, 0xa1, 0x6f, 0xad, 0x95, 0xcf, 0xeb, 0xd8, 0x33, - 0xde, 0x71, 0x3e, 0x64, 0x62, 0x04, 0x7e, 0x8b, 0xb6, 0x5b, 0x12, 0xca, 0xb8, 0x74, 0x0c, 0x32, 0xb5, 0x47, 0x6d, - 0xc7, 0xc9, 0xb4, 0xed, 0x76, 0xd4, 0xee, 0xd1, 0xdd, 0xcd, 0x6f, 0x06, 0xa5, 0xed, 0x8e, 0xf0, 0x2d, 0xbc, 0x3a, - 0x73, 0xe4, 0x7e, 0xeb, 0xee, 0x24, 0x5b, 0xa0, 0x37, 0x33, 0x15, 0x14, 0x75, 0xc2, 0xc9, 0x33, 0xd6, 0xf8, 0xbf, - 0xd0, 0x54, 0xc1, 0x10, 0x98, 0xcc, 0x44, 0xb2, 0xdb, 0x82, 0x7c, 0x16, 0xfa, 0xfb, 0x14, 0x6e, 0x15, 0xb2, 0xb4, - 0x2d, 0x66, 0x08, 0xa7, 0x7a, 0xd0, 0x0c, 0x5e, 0x42, 0x81, 0x28, 0xed, 0x9d, 0xa1, 0x32, 0xe8, 0x41, 0xa5, 0x03, - 0x99, 0x28, 0x06, 0x35, 0x4b, 0x61, 0xca, 0x9b, 0x90, 0x7a, 0xf7, 0x7b, 0xbd, 0xf5, 0x77, 0xf9, 0xde, 0x8c, 0x22, - 0x1e, 0xf5, 0xd6, 0x49, 0x02, 0x82, 0x5f, 0x71, 0x20, 0x13, 0xe5, 0xf5, 0x92, 0x18, 0xb1, 0x8e, 0xc7, 0x49, 0xae, - 0x16, 0x1d, 0xaf, 0xc4, 0x39, 0x25, 0x15, 0x42, 0xce, 0x01, 0x0c, 0x13, 0x05, 0xee, 0xe5, 0x38, 0x82, 0xf5, 0x80, - 0x67, 0x72, 0x45, 0x3d, 0x1b, 0x8b, 0xbb, 0xfd, 0xef, 0xe5, 0xd5, 0xed, 0x9a, 0xf6, 0x36, 0x49, 0x01, 0x56, 0x5d, - 0x54, 0x82, 0xef, 0xfe, 0xfc, 0x29, 0xe4, 0xb1, 0x64, 0x87, 0x5a, 0x2a, 0x73, 0x30, 0x5b, 0x74, 0x1d, 0x72, 0xd6, - 0xa7, 0xaa, 0x3a, 0x36, 0x39, 0xa0, 0x86, 0xd3, 0xb4, 0x73, 0xc1, 0x78, 0x9c, 0xb0, 0x86, 0x73, 0xc2, 0x1a, 0x76, - 0xa8, 0x68, 0x23, 0x8c, 0x6e, 0x68, 0x31, 0x96, 0xb4, 0xc6, 0x7c, 0x3b, 0x20, 0x24, 0xf8, 0x7a, 0xa1, 0x95, 0x8b, - 0x8c, 0xe3, 0x8f, 0x2d, 0x06, 0x13, 0xec, 0x12, 0x2b, 0xdd, 0x84, 0x7f, 0x0d, 0xcf, 0x95, 0xbe, 0x95, 0x27, 0x71, - 0x73, 0x6f, 0xce, 0xe1, 0x44, 0xe3, 0x51, 0x93, 0x8c, 0xfc, 0x94, 0xf5, 0xa8, 0x94, 0xe4, 0x3f, 0x37, 0x8f, 0x81, - 0x33, 0x73, 0x8b, 0x7d, 0x25, 0x30, 0x26, 0x54, 0x3a, 0x96, 0xf1, 0x2f, 0x11, 0xf5, 0xd9, 0x68, 0xc4, 0x0c, 0x0a, - 0xe3, 0x5c, 0x25, 0x56, 0xe2, 0x3e, 0xdb, 0xa2, 0x97, 0xf2, 0xae, 0x31, 0x46, 0x25, 0x4c, 0xc5, 0x2f, 0x46, 0xf6, - 0x18, 0xa9, 0xb7, 0x73, 0xb6, 0xfd, 0x5c, 0x13, 0xdd, 0x73, 0x3a, 0x90, 0x04, 0x8d, 0x4b, 0x66, 0x0a, 0x90, 0xc4, - 0x04, 0x63, 0x72, 0x07, 0x2c, 0xda, 0xa6, 0x75, 0x9e, 0xc2, 0xab, 0x56, 0xe3, 0x49, 0x65, 0x7b, 0xdf, 0x65, 0x65, - 0x2e, 0xdb, 0x8e, 0x4e, 0x5b, 0x12, 0x24, 0x8d, 0x1a, 0xa7, 0x48, 0x48, 0xd5, 0xd3, 0xac, 0x0c, 0x0b, 0x84, 0xb5, - 0xe2, 0x9c, 0xbe, 0xb9, 0x35, 0x99, 0x9d, 0x17, 0xb1, 0x57, 0x78, 0x15, 0x85, 0x08, 0x6e, 0x67, 0x13, 0x89, 0x0f, - 0x63, 0xcb, 0x3a, 0x59, 0xc8, 0xd2, 0xb7, 0x6e, 0xad, 0x4b, 0xc0, 0x0f, 0xde, 0xea, 0xb7, 0xfb, 0xf1, 0x38, 0xb4, - 0x30, 0xd6, 0x47, 0xb8, 0xf8, 0xa8, 0x17, 0x2c, 0xad, 0x7c, 0x89, 0x08, 0x4a, 0x9b, 0xa5, 0xd7, 0xbf, 0x60, 0xb1, - 0x29, 0x2f, 0x57, 0x2c, 0x34, 0x36, 0x74, 0x33, 0x0d, 0xd5, 0x32, 0x31, 0x27, 0x15, 0x55, 0x31, 0xc7, 0x00, 0x3d, - 0xee, 0x20, 0x73, 0xcb, 0x22, 0x6b, 0xd2, 0xc3, 0x59, 0x09, 0xcc, 0xd7, 0x60, 0xe7, 0x38, 0x03, 0xea, 0xd8, 0xa4, - 0xea, 0x17, 0x0b, 0xa0, 0x24, 0x6e, 0xe0, 0x5b, 0x21, 0x77, 0xa1, 0xca, 0x1e, 0x29, 0xa4, 0xb0, 0x0e, 0x2c, 0xe1, - 0xac, 0x60, 0xc5, 0xd8, 0x3e, 0x6c, 0xe6, 0x8f, 0x51, 0x6f, 0x01, 0xd3, 0x43, 0x08, 0xf3, 0xdd, 0x1d, 0xb8, 0x11, - 0x1d, 0xad, 0xc9, 0xe4, 0x1e, 0x27, 0xc8, 0xa2, 0x9f, 0xfb, 0x25, 0x31, 0x14, 0x4f, 0xc8, 0xcb, 0x51, 0x33, 0x16, - 0xb5, 0x60, 0x5a, 0xa6, 0xcd, 0x2d, 0xdf, 0x7d, 0x6d, 0x23, 0xaa, 0x47, 0xc4, 0xa5, 0x42, 0x48, 0x1d, 0x14, 0xe8, - 0x0e, 0x73, 0xa9, 0xeb, 0xc9, 0xb3, 0x45, 0xf1, 0x2c, 0x9b, 0xae, 0x12, 0xfc, 0xe9, 0xe3, 0x0d, 0xb5, 0xbd, 0x09, - 0xa8, 0xf4, 0x5e, 0x77, 0x9c, 0x93, 0xde, 0x51, 0x89, 0x88, 0x26, 0x19, 0x7f, 0xfb, 0xc8, 0xbc, 0x05, 0x91, 0x58, - 0xeb, 0xe1, 0xd2, 0xeb, 0xb7, 0xaf, 0x51, 0xb0, 0x6a, 0x22, 0x9c, 0xbd, 0xa5, 0x49, 0x1c, 0xbc, 0x14, 0x21, 0x19, - 0x8a, 0x60, 0xe4, 0xa3, 0x82, 0xd8, 0x8a, 0xad, 0x12, 0x75, 0xb5, 0x86, 0x40, 0xc4, 0x39, 0xd8, 0x20, 0xb3, 0x8c, - 0xce, 0x99, 0xd7, 0xbe, 0x3c, 0x44, 0xf1, 0xd2, 0x14, 0xf5, 0xbf, 0x5a, 0x16, 0x7e, 0xf4, 0x70, 0xe0, 0x75, 0x64, - 0xe5, 0xac, 0x77, 0xbd, 0x54, 0x6e, 0xcb, 0x3a, 0x6e, 0xad, 0x7a, 0x4f, 0x9e, 0x20, 0xa7, 0xd1, 0xa6, 0x97, 0xe2, - 0xd6, 0x21, 0xa9, 0x31, 0xbc, 0x56, 0xb5, 0xa8, 0x8f, 0x0b, 0x77, 0xd8, 0x8b, 0x5a, 0xa9, 0x77, 0x30, 0x11, 0x5d, - 0xf7, 0xed, 0x9f, 0x88, 0x6a, 0xc8, 0x98, 0x8e, 0x35, 0xe4, 0x0e, 0x6c, 0xc1, 0xf4, 0x54, 0xd2, 0x77, 0x02, 0xf1, - 0xf8, 0x48, 0xb2, 0xab, 0xff, 0x94, 0xd1, 0xfd, 0x85, 0x8c, 0x81, 0x91, 0xd1, 0x1d, 0x61, 0x2d, 0xc2, 0xbd, 0x34, - 0xe8, 0x18, 0x23, 0x94, 0x4f, 0x89, 0x66, 0x66, 0xd9, 0x6d, 0x5e, 0x90, 0xd8, 0xe7, 0x5a, 0xcd, 0xde, 0x72, 0x9d, - 0x48, 0xd0, 0xa2, 0x04, 0xe2, 0xe5, 0x96, 0x19, 0x17, 0x80, 0xae, 0x8d, 0x9b, 0x14, 0x71, 0xb8, 0xb1, 0xd9, 0xdb, - 0x00, 0xa0, 0x7d, 0xfe, 0xfd, 0x4c, 0xe9, 0xe2, 0x76, 0x41, 0x09, 0x9b, 0x1f, 0x2c, 0x26, 0x8b, 0x5b, 0x19, 0x14, - 0x62, 0x23, 0x04, 0x0f, 0x64, 0x13, 0x8d, 0xdd, 0x7a, 0x8a, 0xd8, 0x3c, 0x5f, 0x20, 0x6d, 0x51, 0x78, 0x26, 0x67, - 0x93, 0xfd, 0x8b, 0x76, 0xb0, 0x81, 0xb1, 0x6e, 0x52, 0x94, 0xdf, 0x95, 0xa6, 0xa3, 0x8c, 0xda, 0xc7, 0x2f, 0x37, - 0x5c, 0x94, 0xa5, 0x26, 0x30, 0x9a, 0x46, 0xdd, 0xf2, 0xf7, 0x89, 0x13, 0x0c, 0x5d, 0x19, 0x01, 0xca, 0xb9, 0x94, - 0x09, 0x9f, 0xb3, 0x6f, 0x90, 0x16, 0x00, 0xf2, 0x9b, 0x1f, 0xb5, 0xe3, 0x63, 0x73, 0xbd, 0xfc, 0xd2, 0xb6, 0xa5, - 0x44, 0xf4, 0x5f, 0xda, 0x2a, 0xdb, 0xb1, 0x0f, 0x54, 0xf1, 0x30, 0x6a, 0x44, 0xcb, 0x9a, 0x0f, 0x59, 0xfb, 0x14, - 0x0f, 0x9b, 0x7b, 0x6f, 0x76, 0xa6, 0xc8, 0x86, 0xda, 0x25, 0xfb, 0xcb, 0x4b, 0x3a, 0x2f, 0xaf, 0xd6, 0x0c, 0x5e, - 0xed, 0x11, 0xea, 0x2a, 0x02, 0x05, 0x8f, 0xc1, 0x01, 0xbe, 0x36, 0xfb, 0x9e, 0x2d, 0x28, 0xf0, 0xcf, 0x8e, 0x9d, - 0xbf, 0x3c, 0x9f, 0x43, 0x02, 0x59, 0x9f, 0x35, 0x49, 0x04, 0x44, 0x24, 0x74, 0x3a, 0xdb, 0x1a, 0x82, 0x3c, 0x8c, - 0x2c, 0x1e, 0xb1, 0x59, 0xc6, 0x7f, 0xb1, 0x98, 0x8b, 0xcb, 0x7b, 0x36, 0xb9, 0x9f, 0x9b, 0xb7, 0xce, 0x00, 0xa9, - 0x6d, 0x9a, 0xc9, 0x48, 0x75, 0x64, 0x1a, 0x40, 0x05, 0xed, 0x85, 0x52, 0x4a, 0x46, 0xa9, 0x1c, 0x23, 0xb6, 0x6b, - 0x23, 0xe3, 0xe2, 0x64, 0x49, 0xc3, 0xb0, 0x24, 0xf8, 0x35, 0x11, 0x04, 0xbd, 0x54, 0x44, 0xf5, 0x70, 0x51, 0xca, - 0xdb, 0x21, 0x8f, 0x06, 0xd0, 0x52, 0xe3, 0x6d, 0x92, 0xa7, 0xdd, 0x8b, 0x73, 0x17, 0x59, 0x71, 0xf3, 0xa7, 0xc4, - 0x0f, 0x95, 0x63, 0x3c, 0x29, 0x90, 0x18, 0xe7, 0x5d, 0xb9, 0xf3, 0xa0, 0x0e, 0xc4, 0x1c, 0x13, 0x3c, 0xd2, 0xb3, - 0xaa, 0x3d, 0x98, 0x19, 0x68, 0x53, 0x1a, 0x4d, 0x15, 0xb5, 0x01, 0xe5, 0xff, 0x80, 0xbe, 0xca, 0xa7, 0xe5, 0x91, - 0x6b, 0x10, 0x86, 0xd2, 0x7a, 0x4b, 0xc3, 0x4b, 0x42, 0x68, 0x71, 0xae, 0x4c, 0x32, 0x08, 0xbc, 0xf1, 0xa1, 0xd7, - 0x35, 0x7e, 0x10, 0x25, 0x40, 0x73, 0xe6, 0x27, 0x1f, 0x3e, 0x9e, 0x03, 0x14, 0xce, 0x5a, 0x32, 0xfa, 0xb3, 0xab, - 0x09, 0x4b, 0xba, 0x5d, 0x34, 0xbb, 0x11, 0xca, 0x57, 0x29, 0x58, 0x5a, 0x58, 0x8a, 0xde, 0xa2, 0x3c, 0x30, 0x6c, - 0xb7, 0xb2, 0x7d, 0xfb, 0x5f, 0x1e, 0xde, 0x2b, 0x74, 0x91, 0xb0, 0x1d, 0xe2, 0xa7, 0xa8, 0xe9, 0x2f, 0x3e, 0x9c, - 0x9e, 0x8c, 0x61, 0xbb, 0x2b, 0x61, 0xee, 0x30, 0xcf, 0xb1, 0xbf, 0x74, 0xe4, 0x86, 0xb6, 0x12, 0x31, 0xf9, 0x5a, - 0x36, 0x61, 0x11, 0x07, 0x0c, 0x64, 0xae, 0x06, 0xb9, 0x83, 0x23, 0x04, 0xa6, 0xd6, 0x7c, 0xf2, 0xff, 0x54, 0x2d, - 0x1e, 0x9f, 0x2d, 0x8b, 0x4a, 0x82, 0x7c, 0x2b, 0xed, 0xf3, 0xd8, 0x87, 0xa4, 0x1d, 0xd8, 0xf7, 0x08, 0x16, 0xbd, - 0xdd, 0x61, 0x51, 0x68, 0xa1, 0x83, 0xb8, 0xa4, 0xce, 0xa7, 0xf0, 0xea, 0xe5, 0x32, 0x85, 0xd0, 0x29, 0x0b, 0x3c, - 0x5f, 0x45, 0x38, 0xa6, 0xf7, 0xc7, 0x03, 0x95, 0x05, 0xa5, 0x5c, 0x4e, 0xf0, 0x29, 0x6f, 0xea, 0x70, 0x06, 0xd4, - 0x90, 0xf6, 0xa9, 0x70, 0xc5, 0x3f, 0x4a, 0x59, 0x17, 0x3a, 0xb3, 0x90, 0xaa, 0x30, 0xd9, 0x91, 0xf0, 0xbf, 0x54, - 0xcc, 0x90, 0xe1, 0x85, 0x50, 0xa5, 0x0d, 0x7c, 0x6d, 0x8b, 0xae, 0x94, 0x17, 0x6d, 0x0b, 0x7d, 0x2c, 0x76, 0x65, - 0x4e, 0x00, 0xba, 0x01, 0x5a, 0x7b, 0xed, 0x82, 0xbb, 0x1b, 0xee, 0x65, 0x9f, 0x15, 0xf7, 0x6e, 0xda, 0x00, 0x07, - 0x5f, 0x20, 0xa7, 0xbe, 0x7f, 0x45, 0x71, 0xfe, 0x69, 0x2b, 0x1e, 0x2d, 0xc4, 0x94, 0x80, 0x09, 0x24, 0xe4, 0x1b, - 0x3e, 0xb6, 0x66, 0xc4, 0x3e, 0x7e, 0x08, 0x37, 0x4a, 0x09, 0x2b, 0x8d, 0x3c, 0x38, 0xca, 0xed, 0x37, 0x55, 0x86, - 0xe4, 0xb6, 0x9c, 0x83, 0xc2, 0x10, 0x0b, 0x07, 0xdc, 0x65, 0xae, 0x6c, 0x7f, 0xbc, 0x4a, 0x8f, 0xc2, 0x9e, 0xb8, - 0x50, 0xb1, 0x18, 0x6a, 0x64, 0xc4, 0x2b, 0x1e, 0xaa, 0xb3, 0xd2, 0xc4, 0x00, 0x19, 0x61, 0x80, 0x8e, 0x29, 0x6d, - 0x84, 0x40, 0x09, 0x01, 0x5b, 0x7e, 0xa8, 0xa3, 0x42, 0x13, 0xa1, 0x08, 0xa1, 0x25, 0xd2, 0x1c, 0x1d, 0x64, 0x65, - 0x86, 0xa4, 0xd2, 0x63, 0x76, 0x4c, 0x07, 0x96, 0x05, 0x58, 0x52, 0x29, 0x0a, 0x20, 0x9f, 0x8c, 0x51, 0xab, 0x88, - 0x50, 0xe2, 0xae, 0xbc, 0x4c, 0x1a, 0x0e, 0x58, 0xc3, 0x5c, 0x34, 0x17, 0x4b, 0xd6, 0x75, 0x38, 0x94, 0x21, 0x4d, - 0xae, 0x5a, 0x05, 0x79, 0xa7, 0x3f, 0x4f, 0x63, 0xce, 0x57, 0x04, 0x42, 0x9b, 0xfb, 0x91, 0xcb, 0x05, 0xc2, 0x8f, - 0x74, 0x6c, 0x8c, 0x91, 0x91, 0xb4, 0x76, 0x20, 0x75, 0x51, 0x22, 0x24, 0xc4, 0x95, 0x74, 0x41, 0x73, 0x3e, 0x14, - 0x22, 0x3e, 0x3b, 0x61, 0xae, 0x0f, 0x12, 0xb3, 0x44, 0xe5, 0xdf, 0x37, 0xcb, 0x61, 0xf5, 0x42, 0xf0, 0xb0, 0xd8, - 0xae, 0xaa, 0x1c, 0x28, 0x24, 0x12, 0xd6, 0xa8, 0x13, 0xe6, 0xce, 0x1b, 0xcb, 0xdf, 0x14, 0xc1, 0x9e, 0x27, 0x64, - 0x26, 0x18, 0xa5, 0x57, 0x51, 0xae, 0x54, 0xef, 0x94, 0x39, 0x8c, 0xdc, 0xf0, 0xee, 0xa6, 0xf8, 0xc1, 0x81, 0xbc, - 0x67, 0x53, 0x7a, 0xc4, 0xdb, 0xfd, 0x50, 0x4b, 0x9c, 0x53, 0x24, 0x39, 0x41, 0x29, 0xe8, 0xfe, 0xc3, 0x6b, 0x47, - 0x25, 0x31, 0xfe, 0xd0, 0xa2, 0xf4, 0x5b, 0x8b, 0xa7, 0xb9, 0x96, 0x33, 0x6d, 0xd2, 0xcc, 0x9c, 0x6f, 0x46, 0x15, - 0x9b, 0x2b, 0x63, 0x68, 0x5d, 0x70, 0x20, 0x00, 0x37, 0x83, 0x75, 0x2a, 0xad, 0xcf, 0xf5, 0x07, 0x08, 0x7d, 0xe3, - 0x3e, 0x28, 0xb3, 0x1d, 0x3c, 0x1a, 0x63, 0xc8, 0xeb, 0x67, 0x57, 0x75, 0xd0, 0x65, 0x44, 0x82, 0x00, 0x16, 0x7a, - 0xc8, 0xe1, 0x95, 0xba, 0x9c, 0xd9, 0xca, 0xec, 0xd1, 0xe6, 0xb5, 0x1c, 0x6f, 0x1d, 0x69, 0x38, 0x2e, 0x8e, 0x67, - 0x1f, 0x2c, 0x9d, 0x47, 0xe8, 0x48, 0xca, 0x8d, 0xf7, 0x4a, 0x20, 0xdf, 0x10, 0x19, 0xa8, 0xe7, 0xa2, 0x02, 0xb0, - 0x2b, 0x8b, 0xaa, 0xe4, 0x75, 0x78, 0xe8, 0xf9, 0x38, 0x32, 0x8f, 0x18, 0xe3, 0x10, 0x55, 0x46, 0x1e, 0x9d, 0xdc, - 0x2e, 0x2d, 0x32, 0x6a, 0x2e, 0x98, 0x5a, 0xcd, 0xbb, 0xea, 0x94, 0x07, 0xb2, 0xc9, 0xd7, 0x2b, 0x2d, 0xb4, 0x1e, - 0x89, 0x15, 0xdd, 0xac, 0x8a, 0x7a, 0x58, 0x20, 0x62, 0xbd, 0xff, 0x04, 0x91, 0x47, 0x2c, 0x1f, 0x64, 0xd4, 0x22, - 0x6d, 0xae, 0xc4, 0x4a, 0x29, 0x60, 0x76, 0x81, 0x42, 0x2b, 0xef, 0x10, 0x5c, 0xf9, 0x4d, 0x85, 0x44, 0xda, 0xc5, - 0x5d, 0x07, 0xea, 0x11, 0xbf, 0x35, 0xb2, 0x59, 0x1f, 0xa8, 0xe5, 0x7c, 0x2b, 0x2a, 0x1a, 0x22, 0x23, 0xd2, 0xd1, - 0x6f, 0x38, 0x01, 0xc3, 0x82, 0x0c, 0xe9, 0xf4, 0x3c, 0xf5, 0x58, 0xa0, 0xc5, 0x50, 0xe5, 0x54, 0x8c, 0x65, 0x52, - 0xdd, 0x0a, 0x96, 0x29, 0xb3, 0x50, 0x12, 0x5d, 0x41, 0xcb, 0xec, 0x35, 0xd8, 0x7c, 0xcf, 0x6a, 0x5b, 0x64, 0x44, - 0xc2, 0x35, 0xc2, 0x1f, 0x86, 0x31, 0x00, 0xaf, 0x12, 0xa5, 0xf3, 0xc0, 0x68, 0xc5, 0x24, 0xe6, 0x71, 0x0a, 0xaf, - 0x9b, 0x2a, 0x79, 0x81, 0x5b, 0xf3, 0xd4, 0xd4, 0x58, 0x7e, 0xff, 0xfa, 0xfb, 0x41, 0x53, 0x65, 0xad, 0x40, 0x7e, - 0xb2, 0x6e, 0xfd, 0xfb, 0x2e, 0xf7, 0x20, 0x6f, 0xd3, 0xfb, 0x7e, 0x1c, 0xf2, 0x0d, 0x04, 0x82, 0x51, 0x0a, 0xd3, - 0xc5, 0xfa, 0xb4, 0xc2, 0xe8, 0x7a, 0x49, 0xbb, 0x32, 0x7d, 0x40, 0xc2, 0xfb, 0x7a, 0xfb, 0x19, 0xa1, 0xcc, 0x12, - 0xfb, 0x50, 0x10, 0xc5, 0x4a, 0x94, 0x47, 0xe6, 0x67, 0x73, 0x6f, 0x45, 0x5c, 0x30, 0x53, 0xfd, 0x62, 0xf2, 0x30, - 0x24, 0x1c, 0x98, 0x99, 0x08, 0x07, 0xd6, 0xb4, 0xf0, 0xec, 0x6a, 0xc1, 0x9f, 0x96, 0x12, 0xe0, 0x11, 0xeb, 0x2a, - 0xfd, 0xbd, 0x8c, 0xa5, 0x18, 0xb1, 0xbd, 0x9d, 0xa1, 0xf4, 0x9c, 0x59, 0x07, 0x1d, 0x5e, 0x89, 0x82, 0x2d, 0xce, - 0xf4, 0x03, 0x33, 0x39, 0x8a, 0x0b, 0xaa, 0x25, 0xfc, 0xed, 0xad, 0xe2, 0xbe, 0x54, 0x3c, 0xa7, 0xb0, 0xef, 0x49, - 0xbe, 0xf8, 0x20, 0xed, 0xbd, 0xa8, 0x82, 0x56, 0x38, 0xb5, 0xc1, 0x0d, 0xf1, 0xd1, 0x9d, 0xf2, 0x50, 0x29, 0x42, - 0x08, 0xa3, 0xe8, 0x17, 0xf5, 0x08, 0x79, 0x81, 0xd7, 0xcb, 0xb7, 0x75, 0x7d, 0x48, 0x98, 0x82, 0xb8, 0xbe, 0xdb, - 0xc2, 0x19, 0x7d, 0x6a, 0x46, 0xcd, 0xdd, 0x71, 0xf5, 0x17, 0xea, 0x16, 0xef, 0x40, 0xb4, 0xc3, 0x74, 0x0f, 0x8f, - 0xeb, 0xfa, 0x68, 0x72, 0xd4, 0x85, 0x26, 0x6e, 0x35, 0xdb, 0xaf, 0xb4, 0x64, 0x9e, 0x06, 0x30, 0x68, 0x8c, 0xfa, - 0x7d, 0xc9, 0x1e, 0x8d, 0xef, 0x78, 0x4b, 0x64, 0x43, 0xb8, 0x0d, 0xe8, 0x70, 0x70, 0xa1, 0xa7, 0xfe, 0x46, 0xf6, - 0x6b, 0xb9, 0x04, 0xc7, 0x4b, 0xf1, 0x63, 0xdd, 0xf0, 0x47, 0x99, 0xd0, 0xec, 0x24, 0xa6, 0xc1, 0xfd, 0xb6, 0xb8, - 0xb2, 0xc2, 0x65, 0x12, 0x0a, 0x0b, 0x68, 0x40, 0x60, 0x01, 0x45, 0xd0, 0xe7, 0x30, 0x49, 0x94, 0x3d, 0x9c, 0xb3, - 0xdd, 0xdb, 0x7b, 0x71, 0x4c, 0x24, 0x9d, 0xd2, 0xe4, 0xe8, 0x07, 0x5e, 0x4c, 0x67, 0x51, 0x93, 0x68, 0xa4, 0x5f, - 0x31, 0x27, 0x2f, 0x1b, 0xe9, 0xc8, 0x00, 0x31, 0x67, 0x15, 0xa2, 0x0b, 0x69, 0xdf, 0x3f, 0x23, 0x32, 0xa0, 0x62, - 0x50, 0x37, 0xc3, 0x0e, 0xb1, 0x29, 0xe7, 0xb5, 0xeb, 0x07, 0x46, 0x4b, 0x7c, 0xb1, 0x3c, 0xca, 0xb0, 0xfc, 0x31, - 0x1f, 0xa3, 0xef, 0xbc, 0x95, 0x65, 0xe8, 0xc2, 0x72, 0x7e, 0x17, 0x93, 0xa5, 0x3a, 0x5c, 0x3d, 0x09, 0xe9, 0x7e, - 0x6a, 0xcd, 0x4b, 0xff, 0xb3, 0xe9, 0x82, 0xb4, 0xcf, 0x3c, 0xa4, 0x7e, 0x92, 0xc7, 0x68, 0xf7, 0x55, 0x2f, 0xac, - 0xd3, 0x85, 0x11, 0x66, 0xfa, 0xa8, 0x9a, 0x85, 0x0f, 0x55, 0x66, 0xcb, 0xc6, 0xd3, 0x52, 0xcc, 0x1f, 0x1d, 0xc1, - 0x1a, 0x82, 0x26, 0x24, 0x1b, 0xf7, 0x25, 0xf1, 0x83, 0xea, 0x82, 0xf1, 0x60, 0x87, 0xe5, 0xf5, 0xfc, 0x66, 0xd7, - 0xbe, 0xd3, 0xf2, 0xa9, 0xe0, 0x1f, 0x3c, 0xc4, 0xca, 0x9f, 0x0a, 0x87, 0x25, 0x2e, 0xbe, 0xb0, 0x52, 0x67, 0xbd, - 0x28, 0xa4, 0xad, 0xd5, 0xac, 0xa8, 0x65, 0x37, 0x64, 0xe5, 0x55, 0xde, 0xf2, 0x52, 0x0a, 0x7e, 0x4d, 0x45, 0x4e, - 0x72, 0x9d, 0x72, 0x31, 0x18, 0x78, 0x33, 0x27, 0xfd, 0x7a, 0x42, 0x1b, 0xb9, 0x81, 0x71, 0xfa, 0x3a, 0xb6, 0x2e, - 0x90, 0x04, 0x76, 0xf5, 0x91, 0x0b, 0x4f, 0x20, 0x91, 0xd5, 0xc7, 0x73, 0x36, 0xd6, 0xcd, 0x4e, 0xbe, 0x97, 0x77, - 0x19, 0x47, 0xf0, 0x4c, 0x21, 0xde, 0x9c, 0xd7, 0x06, 0xfc, 0x23, 0xef, 0x70, 0x6e, 0xe5, 0x7d, 0x50, 0x8d, 0xa1, - 0x35, 0x6c, 0x69, 0xe0, 0xab, 0x0b, 0x8c, 0x61, 0x02, 0xd5, 0x24, 0x08, 0x8e, 0xd6, 0x62, 0xbb, 0x20, 0x38, 0x96, - 0x51, 0x78, 0xb1, 0x3a, 0xe5, 0x17, 0x66, 0x53, 0x11, 0x45, 0x26, 0xfc, 0xc2, 0x0e, 0xd5, 0x66, 0x84, 0x43, 0xfc, - 0x58, 0x11, 0xe5, 0x43, 0x8d, 0x07, 0x20, 0x0e, 0x60, 0x7a, 0xd3, 0x88, 0x68, 0x7f, 0x8d, 0x1a, 0x05, 0x35, 0x3c, - 0x73, 0x97, 0xde, 0x59, 0x23, 0x2e, 0xeb, 0x6f, 0x0a, 0xcc, 0x2b, 0xb1, 0x6c, 0xaf, 0xac, 0xcb, 0x92, 0xf3, 0x3d, - 0x68, 0xe2, 0xb8, 0xd3, 0xce, 0x92, 0xb3, 0xe4, 0x00, 0x6d, 0xbb, 0xa7, 0xcd, 0x7c, 0x42, 0x72, 0x71, 0xb5, 0xeb, - 0x14, 0xa4, 0x32, 0xaf, 0x62, 0x23, 0x95, 0xe2, 0xbc, 0x73, 0x09, 0x38, 0xdc, 0x4f, 0xf1, 0xff, 0x55, 0xa2, 0xbe, - 0x9b, 0x5f, 0x10, 0xc6, 0x76, 0x52, 0xbf, 0x1c, 0x36, 0x6d, 0x61, 0x76, 0x70, 0xdd, 0xe2, 0xa6, 0xdd, 0x10, 0x55, - 0xd9, 0x5b, 0x6b, 0xbe, 0x34, 0x0f, 0x79, 0x61, 0x39, 0xb3, 0xd0, 0xea, 0xf3, 0xb8, 0x65, 0x44, 0xf9, 0xd4, 0x85, - 0xc3, 0xb1, 0xd0, 0x90, 0xcb, 0x9b, 0x43, 0x5d, 0xe4, 0xc7, 0xa4, 0xed, 0x01, 0x03, 0x49, 0x3d, 0xd1, 0xc6, 0x87, - 0x6e, 0xe6, 0xb6, 0x3b, 0x03, 0xe9, 0x7a, 0x39, 0x0d, 0x25, 0xb3, 0x18, 0xb8, 0x70, 0x34, 0xe6, 0xa9, 0x43, 0xa7, - 0x5d, 0xb1, 0x11, 0xd6, 0x1d, 0x0c, 0x57, 0x62, 0x54, 0x75, 0x18, 0xbb, 0xa6, 0xd5, 0x39, 0x56, 0xaa, 0xc7, 0xde, - 0x67, 0x1d, 0x91, 0xe0, 0x09, 0x05, 0x47, 0x1e, 0x78, 0x86, 0xcf, 0xea, 0xa0, 0xc3, 0xa3, 0x8e, 0xc0, 0xa1, 0xba, - 0x40, 0x5f, 0x1d, 0xc6, 0x40, 0x39, 0x82, 0x50, 0x44, 0xbe, 0x7b, 0xa0, 0x0e, 0xe1, 0x6b, 0x7e, 0x82, 0x99, 0x52, - 0x7a, 0x3e, 0x66, 0x7b, 0xf0, 0xe5, 0x80, 0xfb, 0xfd, 0x17, 0x9e, 0xd2, 0x35, 0x8c, 0xc3, 0x0f, 0xbf, 0xd5, 0x62, - 0xf9, 0xfd, 0x00, 0xf3, 0xed, 0x20, 0xd5, 0x25, 0x1c, 0xe5, 0x2a, 0xc0, 0x1f, 0x6d, 0x19, 0x77, 0x0d, 0x86, 0xf5, - 0x11, 0x14, 0x11, 0x1e, 0x71, 0x30, 0xdc, 0x2c, 0x05, 0x80, 0xe2, 0x3c, 0xac, 0x80, 0xc8, 0x42, 0x34, 0x3f, 0x2f, - 0x97, 0x58, 0x57, 0x65, 0x68, 0x4b, 0x4b, 0x36, 0x8f, 0x13, 0x31, 0x6c, 0x26, 0x49, 0x25, 0x44, 0xaf, 0x88, 0x18, - 0x11, 0x33, 0x43, 0xeb, 0xa5, 0xfd, 0x9e, 0xba, 0x2a, 0x08, 0xa3, 0xd6, 0x6d, 0xb8, 0xd7, 0xf5, 0x55, 0x2f, 0x6a, - 0xb5, 0x5f, 0x6b, 0x65, 0x00, 0xfb, 0x96, 0x7c, 0x80, 0x22, 0x09, 0x5b, 0xda, 0xf1, 0x6f, 0x07, 0x72, 0xd1, 0x3f, - 0x84, 0xb0, 0x89, 0x4d, 0x90, 0x73, 0x78, 0xa9, 0x75, 0xf6, 0x36, 0x10, 0xc2, 0x24, 0xd6, 0x6a, 0x3d, 0x82, 0x17, - 0x4d, 0x00, 0xa9, 0xd0, 0x3e, 0x63, 0xf9, 0x88, 0x54, 0x9c, 0x3f, 0x1f, 0x5a, 0x36, 0xb7, 0x3f, 0xe5, 0x13, 0x2b, - 0x47, 0x9c, 0xad, 0x5f, 0x2c, 0x49, 0x56, 0xf0, 0x5d, 0x22, 0xc1, 0x37, 0x96, 0xa1, 0xfa, 0xb4, 0x0f, 0xa0, 0x49, - 0x21, 0xd0, 0xc1, 0xe5, 0x5d, 0x8d, 0x0c, 0xb5, 0x58, 0x46, 0x75, 0xb4, 0xc7, 0x22, 0xd3, 0x17, 0x2f, 0xca, 0xea, - 0xb3, 0x08, 0x0d, 0x27, 0x16, 0xc3, 0x28, 0x95, 0x5e, 0x6c, 0xd1, 0xc6, 0x9f, 0xf4, 0x3f, 0xe6, 0x06, 0xa5, 0xea, - 0x78, 0x85, 0x5b, 0x35, 0x54, 0x87, 0xae, 0xd0, 0x1b, 0xd9, 0xca, 0xb1, 0x7f, 0x79, 0x67, 0x51, 0xc7, 0x9a, 0x36, - 0x08, 0x5e, 0x07, 0xfd, 0xcd, 0x14, 0x9c, 0xec, 0xfc, 0x9a, 0xe8, 0x14, 0x06, 0x08, 0x0a, 0x66, 0x08, 0xf6, 0x19, - 0xcd, 0xa6, 0xa5, 0x74, 0x67, 0xcd, 0x89, 0x3a, 0x36, 0xce, 0x8c, 0xb2, 0x76, 0x29, 0x9e, 0xda, 0x78, 0xeb, 0x05, - 0x3d, 0xf8, 0x5a, 0xbc, 0x58, 0x71, 0x52, 0x5b, 0x46, 0xc4, 0x0b, 0x8e, 0x87, 0xeb, 0x98, 0x43, 0xb5, 0x71, 0x6b, - 0xc1, 0x84, 0x09, 0xad, 0x86, 0xcd, 0xce, 0x5a, 0x4e, 0xf9, 0x5a, 0x1e, 0x27, 0x95, 0x4b, 0x7f, 0xac, 0x90, 0x00, - 0x08, 0x1d, 0x2d, 0x22, 0x09, 0x7c, 0x56, 0x18, 0xc6, 0x1c, 0x0f, 0x92, 0x25, 0xf3, 0x63, 0x25, 0x8f, 0x00, 0x33, - 0x31, 0x5c, 0xbc, 0x0d, 0xbd, 0x7a, 0x82, 0x2e, 0xd9, 0xc1, 0x46, 0xdd, 0x20, 0x08, 0x12, 0xec, 0x00, 0x7f, 0xe1, - 0x7d, 0x17, 0x26, 0xe7, 0xfc, 0x66, 0xeb, 0xf0, 0xff, 0x04, 0x4f, 0xe6, 0x61, 0x6d, 0xbb, 0x1f, 0x6c, 0xd4, 0x97, - 0xff, 0x3f, 0xd5, 0x35, 0xb4, 0x0e, 0x7c, 0xf8, 0xc0, 0x85, 0xc7, 0xab, 0x55, 0x3d, 0x5a, 0x6d, 0xed, 0x80, 0x21, - 0x99, 0x38, 0x51, 0x56, 0xec, 0xa8, 0xde, 0xa1, 0xee, 0x1f, 0x1c, 0xee, 0x8f, 0x1c, 0xa0, 0xfe, 0xc1, 0xc4, 0xdb, - 0x48, 0x23, 0xdd, 0xfd, 0x22, 0x64, 0x62, 0x3d, 0xea, 0x20, 0x57, 0x29, 0xfd, 0xfc, 0xdc, 0xb3, 0x75, 0x84, 0xa5, - 0xab, 0x74, 0x70, 0x7f, 0xd1, 0x59, 0x7b, 0xb0, 0xc1, 0xe5, 0x8b, 0xec, 0x56, 0xad, 0x7d, 0x52, 0xba, 0xca, 0x1a, - 0x6f, 0x02, 0x10, 0x60, 0xab, 0xcc, 0x64, 0xe5, 0xe9, 0x9e, 0x52, 0xc2, 0xbb, 0xd6, 0xe6, 0xec, 0xaf, 0xd7, 0xc1, - 0x29, 0x63, 0x6d, 0x77, 0x71, 0x6b, 0xbd, 0x00, 0x41, 0x39, 0xf7, 0x1a, 0xca, 0x29, 0x84, 0x78, 0x49, 0x0d, 0x2e, - 0x87, 0xb1, 0xf1, 0x10, 0x39, 0x72, 0x88, 0x6e, 0x23, 0x82, 0x75, 0x95, 0xb6, 0x2a, 0x8e, 0xbd, 0x96, 0x27, 0x66, - 0x0b, 0xe3, 0x26, 0xa6, 0x14, 0x16, 0x15, 0x18, 0x79, 0x1a, 0x76, 0x38, 0xdb, 0x11, 0x7a, 0x34, 0x6b, 0x5b, 0x90, - 0x26, 0xec, 0x97, 0xfa, 0x7d, 0x58, 0x82, 0xb1, 0xf9, 0xaa, 0x85, 0xd0, 0x4b, 0xe0, 0x34, 0x79, 0x6f, 0xc8, 0xaf, - 0x2e, 0xf4, 0x8c, 0x70, 0x59, 0x24, 0xf7, 0x58, 0x08, 0x42, 0x65, 0x6b, 0xbb, 0x4c, 0xba, 0x99, 0x63, 0x88, 0xe7, - 0x19, 0x63, 0x68, 0xe1, 0x05, 0x81, 0x4c, 0x13, 0x94, 0x32, 0xfc, 0x16, 0xe1, 0x71, 0x86, 0xb3, 0x43, 0x6e, 0xa6, - 0xc3, 0x48, 0xb8, 0xc2, 0xed, 0x04, 0x99, 0xa5, 0xf9, 0x44, 0xe9, 0xc7, 0x54, 0x75, 0xd8, 0x67, 0x26, 0x21, 0x6a, - 0x8f, 0x58, 0x8f, 0xa7, 0x34, 0x6c, 0x67, 0xfc, 0x9c, 0xe7, 0x52, 0x6c, 0x20, 0xee, 0xae, 0xdc, 0x25, 0xd7, 0xc4, - 0x29, 0xb1, 0xb4, 0xca, 0x38, 0xa4, 0xd0, 0x8e, 0x85, 0xb6, 0xf1, 0x90, 0x1e, 0x44, 0xda, 0xae, 0x0f, 0x49, 0x95, - 0x4e, 0x1e, 0xf3, 0x23, 0x62, 0xc8, 0x4c, 0xbf, 0xc0, 0xda, 0xfe, 0x72, 0xf3, 0x21, 0x04, 0x2a, 0x12, 0x3b, 0x77, - 0x04, 0x3e, 0x0d, 0xb0, 0x79, 0x29, 0x2d, 0x85, 0x56, 0x85, 0xce, 0x55, 0x5b, 0xbd, 0x34, 0x94, 0x0d, 0x51, 0x04, - 0x92, 0x59, 0x96, 0xf0, 0x51, 0xd6, 0x30, 0xc8, 0xa9, 0xdf, 0x35, 0x20, 0xdb, 0x1e, 0x06, 0xeb, 0x7b, 0xaa, 0x2c, - 0xf5, 0xfd, 0xd9, 0x4f, 0x9f, 0xf0, 0xb1, 0x0e, 0x61, 0x95, 0x01, 0xd7, 0x6c, 0x5e, 0xe3, 0xa1, 0x77, 0x9f, 0xcc, - 0xa0, 0x7e, 0xc2, 0x91, 0xbe, 0xc1, 0xd7, 0xc8, 0xfd, 0xcc, 0xcb, 0xf2, 0xca, 0x7b, 0x49, 0x9e, 0x6d, 0x53, 0xea, - 0x27, 0x2d, 0x56, 0xbc, 0x81, 0x3f, 0x75, 0x7e, 0x68, 0xc5, 0xf7, 0x65, 0x75, 0x67, 0xdb, 0x99, 0x33, 0x2c, 0x33, - 0xd8, 0x83, 0x19, 0xba, 0xeb, 0xa3, 0x56, 0x2a, 0xa4, 0x5e, 0xe9, 0xcb, 0x07, 0xef, 0x53, 0xef, 0x53, 0x26, 0x4d, - 0x74, 0x13, 0x98, 0xa2, 0xd2, 0xd7, 0x21, 0xca, 0x0e, 0x69, 0x62, 0xda, 0xa1, 0x4a, 0x14, 0x1d, 0x5e, 0x98, 0x65, - 0x29, 0xc0, 0xf0, 0x8d, 0xe5, 0x53, 0x86, 0x6b, 0x25, 0xa9, 0x20, 0xd4, 0x1a, 0xc4, 0x67, 0x93, 0xe9, 0x7d, 0x99, - 0x1b, 0x0a, 0x58, 0xb0, 0xf8, 0x3a, 0x86, 0x5d, 0xa4, 0x7f, 0x38, 0x7e, 0x27, 0xc1, 0x39, 0xe1, 0x70, 0x64, 0x03, - 0x01, 0x94, 0x69, 0xbb, 0xe0, 0xe2, 0x7e, 0x83, 0x3d, 0xcc, 0xd2, 0x7f, 0x42, 0x6a, 0xc1, 0x69, 0xa0, 0x97, 0xe8, - 0xff, 0xba, 0x33, 0x7f, 0x2a, 0x5f, 0x2f, 0x2c, 0xe6, 0x44, 0xb8, 0xc5, 0xd9, 0x57, 0x96, 0x59, 0xe5, 0x8a, 0xfb, - 0x03, 0x23, 0x13, 0xad, 0x5d, 0x9f, 0x1f, 0xac, 0x56, 0xd4, 0x2a, 0xd4, 0xd0, 0x57, 0xee, 0x7f, 0xa6, 0x7b, 0xb9, - 0x67, 0xc6, 0x3c, 0x14, 0x73, 0x87, 0x75, 0xd1, 0xd0, 0xf8, 0x0c, 0xd1, 0x10, 0xa5, 0xc6, 0x6a, 0xc0, 0x66, 0x4c, - 0xea, 0xd7, 0x83, 0x08, 0x4b, 0xe9, 0x9c, 0x18, 0x55, 0x6a, 0x91, 0x41, 0x82, 0xc9, 0xf1, 0x5c, 0xda, 0x1c, 0x0a, - 0x44, 0xd0, 0xcc, 0x6b, 0x68, 0xf4, 0x35, 0x1e, 0x56, 0xb8, 0xd1, 0xcd, 0x1e, 0x31, 0x64, 0x04, 0x41, 0x65, 0xd9, - 0x18, 0xd9, 0x2e, 0x46, 0x51, 0x38, 0xf5, 0xb3, 0x43, 0x41, 0xf1, 0xcb, 0x99, 0x2f, 0x4d, 0x76, 0xdc, 0x3d, 0x1a, - 0x80, 0xa2, 0x58, 0x97, 0x78, 0xd9, 0x66, 0x22, 0x37, 0xb9, 0xc1, 0x94, 0x20, 0x88, 0x39, 0xfc, 0x09, 0xb2, 0xa4, - 0x88, 0xe9, 0x22, 0x6e, 0x2e, 0xcd, 0xc5, 0xa8, 0x4c, 0x76, 0xf5, 0xc0, 0x6d, 0x68, 0x54, 0xab, 0x89, 0x5e, 0x6b, - 0xdd, 0x0f, 0x0a, 0xd1, 0x09, 0x8b, 0x27, 0xf2, 0x8a, 0x85, 0x48, 0x82, 0x81, 0x00, 0x45, 0xdb, 0xc2, 0x28, 0x0a, - 0xbd, 0x16, 0xd3, 0xd9, 0x72, 0x7e, 0x2e, 0xd3, 0x50, 0x34, 0x3a, 0xe3, 0x96, 0x81, 0x55, 0x3f, 0x6d, 0x96, 0x1f, - 0x78, 0xfc, 0x4f, 0x62, 0xc2, 0xdb, 0x1e, 0x7a, 0x06, 0xe2, 0x53, 0x0f, 0x28, 0xd3, 0x5d, 0x02, 0x85, 0xe9, 0xb9, - 0x8b, 0x50, 0x22, 0x29, 0xea, 0xc6, 0x9c, 0x58, 0x72, 0x1f, 0x95, 0xf8, 0xbe, 0x1a, 0x1f, 0xc2, 0x11, 0xd5, 0x87, - 0xc4, 0x15, 0x05, 0x2c, 0xf2, 0xac, 0x64, 0xf3, 0x39, 0xcb, 0xb8, 0x0e, 0x8b, 0x35, 0x73, 0xce, 0x1b, 0x5e, 0x96, - 0xfa, 0xa0, 0x64, 0x04, 0xef, 0x07, 0x73, 0x08, 0x55, 0xae, 0xc8, 0x0c, 0xf9, 0x55, 0x89, 0xda, 0x0a, 0x58, 0xe3, - 0x0a, 0xc2, 0x6c, 0xed, 0x15, 0xaf, 0x6f, 0x8b, 0x95, 0xfe, 0x40, 0xae, 0x11, 0xf7, 0x70, 0x08, 0x00, 0x0c, 0xfb, - 0xdd, 0x09, 0x8a, 0x91, 0x0a, 0x17, 0xe6, 0x62, 0x68, 0x40, 0xc2, 0x36, 0x48, 0x99, 0xed, 0xc7, 0xf9, 0xf0, 0xee, - 0x9f, 0xd6, 0x9c, 0x1d, 0xac, 0x95, 0x70, 0xed, 0xe8, 0x2a, 0x13, 0xe4, 0xe5, 0x43, 0x94, 0x9d, 0xb9, 0x6d, 0xae, - 0x96, 0x05, 0x51, 0x69, 0x31, 0x9e, 0xad, 0xc4, 0xfd, 0x32, 0x85, 0xc7, 0xce, 0x22, 0xd8, 0x99, 0x97, 0xe0, 0x12, - 0x10, 0x7d, 0x90, 0xf1, 0x91, 0x0d, 0x12, 0xbd, 0xf2, 0x6c, 0xfc, 0x59, 0x75, 0xef, 0x51, 0x9b, 0x2a, 0x52, 0xbb, - 0x1e, 0xb8, 0x3b, 0x94, 0xa4, 0x82, 0x69, 0x77, 0x03, 0xd6, 0xcc, 0xeb, 0x89, 0xc9, 0x37, 0x0a, 0xe2, 0x06, 0x38, - 0xfb, 0x6e, 0x1c, 0x68, 0x1a, 0x58, 0x6f, 0x3e, 0xa2, 0x68, 0x10, 0x6a, 0x44, 0x9c, 0x9b, 0xf5, 0xfa, 0xa5, 0xdf, - 0x81, 0x00, 0xa4, 0x60, 0x56, 0x12, 0xbc, 0x77, 0xe5, 0xa4, 0x10, 0x84, 0xd8, 0x02, 0x88, 0xe9, 0x06, 0x12, 0xc7, - 0x11, 0xe5, 0x1a, 0xcf, 0xbe, 0x59, 0x7a, 0xf4, 0xa2, 0x23, 0x76, 0x7f, 0x09, 0xac, 0xe9, 0x65, 0x07, 0xdb, 0xb9, - 0x09, 0x49, 0x85, 0x32, 0xf4, 0xaa, 0x9e, 0xdd, 0xb0, 0xb9, 0x75, 0x2c, 0x0b, 0x3d, 0x7a, 0x08, 0x72, 0xc9, 0xbc, - 0xb7, 0xd5, 0x18, 0x08, 0xa5, 0x9b, 0x5f, 0x08, 0x14, 0x47, 0xeb, 0x5f, 0x68, 0x93, 0x0c, 0x6d, 0x9c, 0xdb, 0xb4, - 0xb3, 0x66, 0xb7, 0x29, 0xac, 0x5c, 0x72, 0x73, 0xbd, 0x38, 0xa6, 0xa8, 0xa7, 0xf2, 0xbd, 0xd6, 0xb2, 0x67, 0x63, - 0xa0, 0x86, 0xf6, 0xc8, 0x27, 0x85, 0xd0, 0x1b, 0xb6, 0x62, 0x69, 0x24, 0x93, 0xe6, 0xce, 0x3b, 0x27, 0xd4, 0xe6, - 0x61, 0x87, 0xc4, 0x09, 0x73, 0xeb, 0xbf, 0xcf, 0x23, 0x29, 0x8b, 0xf7, 0x29, 0x14, 0x6e, 0x86, 0xea, 0x86, 0xb1, - 0xe8, 0x38, 0x01, 0xb7, 0x95, 0xf5, 0xd3, 0x8c, 0x44, 0xac, 0x16, 0x16, 0xc6, 0x33, 0x80, 0xa9, 0x98, 0x22, 0x6e, - 0x55, 0x30, 0xd4, 0x20, 0x39, 0x57, 0x83, 0x60, 0xa6, 0xd7, 0x8c, 0x9d, 0x79, 0x99, 0xb7, 0xd0, 0xd6, 0xc6, 0x2c, - 0x2c, 0xd4, 0x6c, 0x4c, 0xcd, 0x93, 0x49, 0x01, 0x4b, 0x23, 0xe8, 0xf6, 0x98, 0x1e, 0xee, 0xae, 0x91, 0xef, 0x96, - 0x23, 0x67, 0x17, 0x83, 0xf9, 0xd8, 0xcb, 0xec, 0x71, 0xea, 0xc1, 0xcb, 0x04, 0x33, 0x42, 0x85, 0xad, 0xe2, 0x02, - 0xda, 0xb3, 0xa6, 0xff, 0xc0, 0x37, 0xf1, 0x31, 0x07, 0x37, 0x66, 0xec, 0xad, 0x59, 0xba, 0xe2, 0x3d, 0x1d, 0x23, - 0x64, 0x11, 0x23, 0xf2, 0x9c, 0x35, 0xc5, 0xdc, 0x4a, 0x15, 0xe3, 0x1e, 0x14, 0x82, 0xe5, 0x2b, 0x4c, 0x05, 0x10, - 0x0e, 0x66, 0x37, 0x1a, 0x1c, 0x62, 0x7d, 0xdc, 0xba, 0x5b, 0x20, 0x04, 0x06, 0x50, 0x5d, 0x9c, 0x73, 0x34, 0xd1, - 0x01, 0x90, 0xfc, 0x3e, 0x12, 0x00, 0x49, 0x60, 0x86, 0x22, 0x01, 0x46, 0xaf, 0x5a, 0xfa, 0x9a, 0x17, 0x6b, 0x8c, - 0x8c, 0xd8, 0x23, 0x08, 0xb6, 0x72, 0x8f, 0x2c, 0x90, 0x66, 0x73, 0xaf, 0x75, 0xc2, 0xb7, 0x67, 0x45, 0x25, 0x0e, - 0x2e, 0xbf, 0x2a, 0x23, 0xe9, 0x9f, 0x0c, 0x6a, 0xac, 0x63, 0xe5, 0x29, 0xfd, 0x98, 0xa9, 0xa3, 0x47, 0x77, 0x69, - 0x9a, 0x4e, 0xe6, 0xa0, 0xd8, 0x43, 0x36, 0x28, 0xab, 0x64, 0xec, 0xc4, 0x39, 0x74, 0x22, 0xa9, 0x7f, 0x1c, 0xbd, - 0xbc, 0x53, 0x8f, 0xa2, 0x34, 0xb7, 0xeb, 0x09, 0xb5, 0x72, 0xaa, 0xdd, 0x08, 0xbc, 0x49, 0x79, 0x26, 0x75, 0xc6, - 0x96, 0xfa, 0xa5, 0x42, 0x2a, 0x3b, 0x35, 0x26, 0xb1, 0x93, 0xf3, 0x32, 0xe7, 0xe8, 0x29, 0xbf, 0x10, 0xc6, 0x81, - 0xb1, 0x3f, 0x9d, 0xb6, 0xde, 0xaf, 0xd9, 0x19, 0xe2, 0xf1, 0x6a, 0xaa, 0xf6, 0x21, 0x5d, 0xab, 0x26, 0xa6, 0x40, - 0xd3, 0x9e, 0xa6, 0xff, 0xc9, 0x80, 0x3e, 0x0f, 0xc1, 0x9e, 0xe9, 0x27, 0x21, 0xd5, 0x0e, 0xa2, 0xfd, 0x41, 0x0b, - 0xaf, 0xf0, 0x35, 0x4a, 0xa8, 0xfe, 0xce, 0x09, 0xd0, 0xf1, 0xbd, 0x6e, 0x10, 0x5b, 0x92, 0xb8, 0x98, 0x8b, 0x54, - 0x76, 0x8e, 0x19, 0xd5, 0x40, 0x2e, 0x88, 0x14, 0xcf, 0x75, 0x1a, 0x95, 0x85, 0x2c, 0x79, 0x83, 0x1b, 0x3f, 0xfb, - 0x35, 0x53, 0x28, 0xfc, 0x6a, 0x38, 0x08, 0x58, 0x06, 0x90, 0x30, 0xef, 0x5e, 0x69, 0xce, 0x99, 0x9d, 0x8d, 0x18, - 0xb2, 0x00, 0x2e, 0x75, 0xec, 0x23, 0x74, 0x12, 0x00, 0x10, 0x1d, 0x13, 0x63, 0x20, 0xaf, 0x76, 0x54, 0xff, 0x03, - 0x1c, 0x7a, 0x27, 0xbd, 0x5a, 0x73, 0x37, 0x81, 0x28, 0x42, 0x40, 0x80, 0xc4, 0xde, 0x50, 0x10, 0x45, 0xcb, 0x41, - 0x24, 0x55, 0x62, 0x27, 0xb4, 0x15, 0x9a, 0x05, 0x37, 0xb2, 0x11, 0x69, 0x04, 0xd0, 0x2b, 0xb8, 0x10, 0x33, 0x02, - 0x65, 0x1e, 0x47, 0x1a, 0xbf, 0xa0, 0xc4, 0xcc, 0x4b, 0xc5, 0xe8, 0x73, 0x8a, 0x7a, 0xef, 0x41, 0x74, 0xcf, 0xcd, - 0xa3, 0xd6, 0xc7, 0x84, 0x10, 0x3d, 0x01, 0x6b, 0x28, 0xab, 0x9f, 0xa2, 0x0c, 0x30, 0x1a, 0xa0, 0x6c, 0xef, 0x70, - 0x1e, 0xb0, 0x7c, 0xa9, 0x79, 0x52, 0x0f, 0x1d, 0xf3, 0x88, 0x5c, 0x3a, 0x9f, 0xf7, 0xeb, 0xb8, 0x5e, 0xd4, 0x0e, - 0x2a, 0x04, 0x3c, 0x56, 0x2f, 0xd5, 0x8d, 0x20, 0x37, 0x14, 0xff, 0x45, 0xc5, 0xd4, 0x18, 0xf6, 0x95, 0x5f, 0x4c, - 0x27, 0x1d, 0x6a, 0x87, 0xf5, 0x2e, 0x72, 0x75, 0x2a, 0x4a, 0x00, 0x4e, 0xbb, 0x1d, 0xda, 0x39, 0xb3, 0xe3, 0x6f, - 0x77, 0xfd, 0x68, 0x95, 0x95, 0x6a, 0x51, 0xe7, 0x59, 0x43, 0x41, 0x79, 0x39, 0x1d, 0xff, 0x0b, 0x4f, 0xf6, 0xf2, - 0x64, 0x30, 0xa7, 0x16, 0x61, 0x9c, 0xba, 0xf3, 0xec, 0xfd, 0xc1, 0xdb, 0x61, 0x4b, 0x88, 0x9d, 0xea, 0xe6, 0xd7, - 0x7a, 0xe4, 0x8b, 0xa9, 0xdb, 0x7a, 0x82, 0x1b, 0x37, 0xb7, 0xce, 0xd8, 0xab, 0xc7, 0xd0, 0x31, 0x01, 0xe0, 0xad, - 0x25, 0x8a, 0xa2, 0x22, 0xfc, 0xfb, 0xe3, 0xe9, 0xa1, 0xe6, 0xf4, 0xa0, 0x6f, 0xe3, 0x9d, 0x18, 0x34, 0x05, 0x26, - 0x58, 0x07, 0x0c, 0xf3, 0x01, 0xfd, 0xae, 0xa4, 0x1a, 0xdf, 0xf7, 0xa2, 0xc8, 0x41, 0x4c, 0x66, 0xf0, 0xa5, 0xf1, - 0x4d, 0x56, 0x64, 0x7c, 0x51, 0x01, 0xda, 0xbc, 0x4c, 0xb6, 0x0b, 0xc7, 0x30, 0x85, 0x69, 0xb7, 0xee, 0x7b, 0xec, - 0xa0, 0x5c, 0xdc, 0x1a, 0xc6, 0x6f, 0x24, 0x82, 0xec, 0x8d, 0x65, 0xe6, 0x03, 0xc5, 0xaf, 0x9f, 0x3a, 0x26, 0xb9, - 0xe7, 0x0d, 0xf7, 0x87, 0xa8, 0xa9, 0xd0, 0xf9, 0x5c, 0x79, 0xb7, 0x9c, 0xdf, 0x85, 0xd7, 0xaa, 0x50, 0x77, 0x64, - 0xc9, 0x48, 0xd3, 0x69, 0xe8, 0xe9, 0x5a, 0x2d, 0xda, 0x9c, 0xb8, 0x0e, 0xda, 0xb6, 0xd8, 0x04, 0x12, 0x4b, 0xce, - 0x60, 0xa6, 0xe4, 0x4d, 0x2c, 0x08, 0x0e, 0x1d, 0x41, 0xa1, 0xbb, 0x28, 0x0e, 0x91, 0x30, 0x60, 0xb3, 0x43, 0x85, - 0xdd, 0x47, 0xb0, 0xf1, 0xd5, 0xbc, 0x50, 0xe4, 0x19, 0xab, 0xc2, 0x9c, 0xa9, 0x66, 0x56, 0xb5, 0x5f, 0x0c, 0x70, - 0xf6, 0x4f, 0xb8, 0x97, 0x4e, 0x0c, 0xd6, 0x83, 0x4c, 0x49, 0x0d, 0x9d, 0x72, 0x8a, 0x6a, 0x1e, 0xb1, 0x8d, 0x35, - 0x14, 0x50, 0x3b, 0x3c, 0x32, 0x1c, 0x6e, 0x7a, 0x6f, 0x7c, 0x3e, 0xf0, 0x2c, 0x36, 0xf0, 0xce, 0x21, 0xb0, 0x10, - 0xfb, 0x51, 0xbb, 0x38, 0xb4, 0x35, 0x9b, 0xa1, 0xb0, 0x8d, 0x86, 0x42, 0x3a, 0xb3, 0x17, 0x24, 0xd3, 0x41, 0x1a, - 0x48, 0x5b, 0x87, 0x89, 0xc6, 0xde, 0x57, 0x07, 0xba, 0x03, 0x8d, 0x37, 0x3d, 0xa2, 0xd0, 0xc6, 0xae, 0x1a, 0xc0, - 0x2b, 0x3a, 0x97, 0xe8, 0x66, 0xdf, 0x12, 0xd8, 0xaf, 0x36, 0x83, 0x1b, 0xcb, 0x97, 0x00, 0xa6, 0x14, 0x90, 0x6d, - 0xd9, 0xf8, 0xdc, 0x73, 0x3e, 0x90, 0x4d, 0x98, 0x1c, 0x8d, 0xa3, 0x76, 0x11, 0x76, 0x69, 0x8d, 0xb7, 0x1c, 0x4d, - 0xf5, 0xcf, 0xa5, 0x08, 0x0b, 0xac, 0x2e, 0x98, 0xb6, 0xc5, 0x47, 0x23, 0xac, 0x49, 0x51, 0xb7, 0x87, 0x05, 0xd4, - 0xd8, 0x61, 0x21, 0x95, 0xd6, 0x6b, 0x89, 0x8b, 0x95, 0x18, 0x5f, 0xd3, 0x53, 0x28, 0x0e, 0x2c, 0x3b, 0x47, 0x40, - 0xe3, 0xc1, 0x3a, 0xdf, 0x0a, 0x5f, 0x2a, 0xdc, 0x2f, 0xe5, 0xa8, 0xe4, 0x99, 0x26, 0xce, 0xf5, 0x02, 0xce, 0x08, - 0x89, 0xcb, 0x0f, 0xd8, 0x38, 0x96, 0x00, 0x5b, 0xa6, 0xf7, 0xff, 0x50, 0x87, 0x05, 0xdb, 0x21, 0xc0, 0x2b, 0xee, - 0xa2, 0x7d, 0xa0, 0x5c, 0x01, 0xa4, 0xc9, 0x51, 0x86, 0x5c, 0x7e, 0xd6, 0xbd, 0x42, 0xc6, 0xb4, 0x4f, 0xa2, 0x63, - 0xcb, 0x76, 0x76, 0x20, 0x99, 0x23, 0x43, 0xc2, 0xb8, 0xf5, 0x2a, 0x65, 0xe1, 0x6e, 0x80, 0x63, 0x44, 0xb1, 0xb4, - 0x62, 0x7e, 0x99, 0x29, 0xf2, 0x0a, 0xd8, 0x8d, 0x93, 0xa6, 0xbd, 0x36, 0xa6, 0x4b, 0x64, 0x63, 0x30, 0x76, 0xaa, - 0x08, 0x2c, 0x8c, 0x41, 0x55, 0xb2, 0x20, 0xfc, 0x63, 0x4f, 0x38, 0xe0, 0x7f, 0x72, 0x26, 0x28, 0x3f, 0x1e, 0xcb, - 0x79, 0x52, 0x61, 0x1e, 0xc8, 0x14, 0xf6, 0x70, 0xfa, 0xd2, 0xbf, 0xa2, 0x4f, 0xa9, 0xc0, 0x9a, 0x24, 0xc2, 0xb8, - 0x01, 0x06, 0x55, 0x1b, 0x00, 0x74, 0x83, 0x14, 0x6f, 0x12, 0xd0, 0xe4, 0x26, 0xb4, 0x0a, 0xe5, 0x28, 0x1d, 0xb0, - 0x52, 0xe4, 0x66, 0xd4, 0x14, 0xc1, 0x46, 0xda, 0x41, 0x8a, 0x12, 0x0c, 0x3b, 0xa9, 0x06, 0x69, 0x95, 0x7a, 0x38, - 0x08, 0x7f, 0x4d, 0x80, 0x0e, 0x40, 0x1e, 0x96, 0xff, 0x65, 0x32, 0xc2, 0xcb, 0x23, 0x2b, 0xb9, 0x47, 0xcd, 0x51, - 0x63, 0x62, 0x1a, 0x3a, 0xb9, 0xa5, 0x13, 0xde, 0xd5, 0x1c, 0x71, 0x36, 0x0e, 0x6a, 0xab, 0x9a, 0x0f, 0x06, 0x43, - 0xa7, 0x4e, 0x3a, 0x82, 0xc2, 0x47, 0x39, 0x06, 0x13, 0xda, 0xcd, 0x14, 0x3d, 0x0f, 0x7b, 0x99, 0x97, 0x93, 0x6e, - 0x36, 0x53, 0x00, 0xb6, 0x5a, 0x0a, 0x5b, 0x86, 0x81, 0x31, 0x8c, 0x3f, 0x02, 0x72, 0xc3, 0xa7, 0xcf, 0x4b, 0x23, - 0x8f, 0x4a, 0x2f, 0x6f, 0x7e, 0xf8, 0xf8, 0x31, 0x80, 0xc1, 0x50, 0xd1, 0xe0, 0xc3, 0x67, 0x7d, 0x35, 0xbe, 0x93, - 0xc9, 0xc6, 0x1a, 0xe3, 0x1c, 0x44, 0x79, 0x16, 0xda, 0x91, 0x9f, 0x95, 0x75, 0x51, 0x0c, 0xdb, 0xd7, 0x17, 0x95, - 0xd7, 0x97, 0x22, 0xa4, 0x6a, 0x41, 0x2d, 0x6f, 0x71, 0x8a, 0x8d, 0x43, 0x28, 0x34, 0xe6, 0xa0, 0x08, 0x19, 0x8e, - 0x22, 0x6e, 0xee, 0x34, 0x14, 0x03, 0x52, 0x22, 0x12, 0xe6, 0xd4, 0x69, 0xed, 0x6d, 0x4e, 0xb0, 0xd9, 0x3b, 0x53, - 0x4b, 0x0d, 0xaf, 0x31, 0x61, 0x65, 0xe7, 0x48, 0x91, 0xc0, 0xa5, 0x2d, 0xbb, 0xb5, 0xc8, 0x54, 0xdd, 0x8d, 0x86, - 0xcc, 0x99, 0x15, 0x14, 0xab, 0x97, 0xcf, 0x0a, 0x87, 0x56, 0x50, 0xfc, 0xa6, 0xc1, 0x06, 0xc4, 0x38, 0x76, 0x7f, - 0x7c, 0xae, 0x82, 0x7f, 0xf8, 0x39, 0xdc, 0x93, 0x3f, 0xa6, 0x17, 0xac, 0x52, 0xcc, 0x06, 0x35, 0xdf, 0x25, 0xca, - 0xf4, 0xda, 0x9c, 0x09, 0x4c, 0x1c, 0xf3, 0x82, 0x1e, 0x3e, 0x4b, 0x5f, 0x14, 0xa3, 0xcd, 0xa0, 0x22, 0x65, 0x52, - 0x01, 0x44, 0x34, 0xe9, 0x0e, 0xa9, 0xd7, 0x58, 0xa4, 0x2c, 0x9b, 0x62, 0x9b, 0xe6, 0x4a, 0x6d, 0x1f, 0x3b, 0x6a, - 0x6a, 0xdd, 0x90, 0x48, 0x3c, 0xa4, 0xf9, 0x0f, 0xc5, 0xd7, 0x31, 0x16, 0x84, 0x48, 0x21, 0xad, 0x2f, 0xce, 0x74, - 0x7a, 0x7c, 0x36, 0x8a, 0x3b, 0x1a, 0xc7, 0xa3, 0xfc, 0x6b, 0x8d, 0xe9, 0x54, 0xb0, 0x1f, 0xd2, 0x19, 0x12, 0x47, - 0x9e, 0x1b, 0x17, 0xdd, 0xad, 0xcf, 0x3a, 0x7c, 0x10, 0xb5, 0xbc, 0xe4, 0x1d, 0xbb, 0x21, 0x54, 0x32, 0x98, 0xeb, - 0x94, 0x40, 0x6b, 0xea, 0x0f, 0xfc, 0x0d, 0x5f, 0xb8, 0x51, 0x5d, 0x3a, 0x24, 0x5a, 0xd8, 0x90, 0x60, 0x3a, 0x49, - 0x8a, 0xb4, 0xe0, 0x12, 0x26, 0x9b, 0xa0, 0x58, 0xbb, 0xb0, 0xe0, 0xb3, 0xb0, 0x38, 0x64, 0xf3, 0x8b, 0x2e, 0xc4, - 0x78, 0x07, 0xd6, 0x19, 0xaa, 0x3c, 0x73, 0x8e, 0x41, 0x33, 0x78, 0x61, 0x61, 0xaf, 0x0a, 0x72, 0x88, 0x0b, 0xeb, - 0x80, 0xca, 0xc7, 0xa8, 0x91, 0xf1, 0xe8, 0xad, 0x4d, 0x21, 0x3d, 0xd0, 0x7d, 0xff, 0xce, 0x6f, 0xaf, 0x22, 0x67, - 0x60, 0x88, 0x0b, 0x91, 0x17, 0x1e, 0xcd, 0x6c, 0x70, 0x81, 0x71, 0xe1, 0x6d, 0x8e, 0x7a, 0xf1, 0x1c, 0xce, 0xdb, - 0x37, 0x54, 0x6a, 0x78, 0xc5, 0x94, 0x8e, 0x13, 0x14, 0xdc, 0xa4, 0x97, 0x15, 0xc8, 0xbb, 0x93, 0xb4, 0xd8, 0x35, - 0xbb, 0x14, 0xb2, 0x21, 0x45, 0x67, 0xed, 0x6e, 0x70, 0xca, 0xdc, 0x9e, 0x49, 0x87, 0x65, 0x4e, 0xd1, 0xcc, 0x24, - 0x0a, 0x3d, 0x87, 0x28, 0xc6, 0x8c, 0xa1, 0x49, 0xb5, 0x65, 0x5e, 0xd4, 0x92, 0x0d, 0x95, 0xba, 0x46, 0x50, 0xd6, - 0xcd, 0x91, 0xfd, 0x73, 0xe6, 0xe3, 0xdc, 0xb9, 0xc1, 0xd0, 0x03, 0x79, 0x18, 0x90, 0xb1, 0x3a, 0xc7, 0xfd, 0x85, - 0x8f, 0x69, 0xde, 0xed, 0x5e, 0x73, 0xfc, 0x6b, 0x04, 0x6d, 0x99, 0x7c, 0xd3, 0xfa, 0x07, 0xd5, 0xff, 0x65, 0x03, - 0x46, 0xd6, 0x3a, 0x3e, 0x2c, 0x36, 0xad, 0x37, 0x55, 0x4d, 0x76, 0xd0, 0xd8, 0x99, 0x26, 0x6d, 0x2c, 0x1c, 0xd4, - 0xdd, 0xdb, 0xc8, 0x24, 0x38, 0x6c, 0xde, 0x1c, 0xc2, 0x40, 0x56, 0xc6, 0x77, 0x1b, 0xa1, 0x75, 0xeb, 0xb4, 0xa9, - 0xc3, 0x2f, 0x43, 0x13, 0x0f, 0x7b, 0x8d, 0x56, 0x0c, 0x61, 0x9e, 0x4d, 0x19, 0xbb, 0x02, 0xde, 0x14, 0x41, 0x11, - 0x4f, 0xe3, 0x9a, 0x23, 0x98, 0xd2, 0x6a, 0x60, 0xc8, 0xaa, 0x11, 0xcf, 0x51, 0xa5, 0x6b, 0xd4, 0x73, 0xfb, 0xa6, - 0x07, 0x0c, 0xb9, 0x90, 0xb3, 0x5f, 0x4e, 0x6b, 0x42, 0x67, 0xeb, 0x76, 0xf4, 0x18, 0xf0, 0x0a, 0x91, 0xe8, 0xf3, - 0x41, 0x96, 0x01, 0xb1, 0xc5, 0x64, 0xa5, 0x43, 0x21, 0xc1, 0xe3, 0x76, 0xf8, 0x8c, 0xd5, 0xa7, 0xbc, 0xa7, 0x4f, - 0x59, 0xec, 0x86, 0xa6, 0xd6, 0xc1, 0xdf, 0xa9, 0x12, 0x22, 0x05, 0xae, 0xa5, 0xba, 0xcb, 0x90, 0x55, 0xa5, 0x1e, - 0xfd, 0x41, 0x91, 0x96, 0x46, 0xac, 0xc4, 0x52, 0xa9, 0x1a, 0xd3, 0xfa, 0xef, 0xb4, 0x15, 0x9d, 0xa8, 0xff, 0xb4, - 0xb0, 0x5a, 0x7d, 0x45, 0xac, 0xab, 0x84, 0xe3, 0x45, 0xaf, 0xb6, 0xe8, 0xc8, 0x10, 0x41, 0x02, 0x9f, 0x75, 0xad, - 0x37, 0x1b, 0x3a, 0x0d, 0x68, 0xbc, 0xcd, 0xe3, 0xbf, 0xea, 0xf9, 0x8c, 0xec, 0x05, 0x9a, 0xae, 0x52, 0x9b, 0x02, - 0x5d, 0x41, 0xe0, 0x9c, 0x25, 0xd6, 0x5c, 0xa5, 0x81, 0x09, 0xa6, 0x79, 0xb1, 0xe5, 0x0b, 0xa8, 0x4e, 0xb7, 0x40, - 0x1a, 0x08, 0xd2, 0x50, 0x7a, 0xa9, 0x55, 0xea, 0x36, 0xa6, 0xd3, 0x41, 0x79, 0xd6, 0x06, 0xa5, 0xf8, 0x01, 0xe5, - 0xc0, 0x95, 0x5b, 0xbd, 0x44, 0x09, 0xb2, 0xea, 0xd0, 0xbc, 0x95, 0xbd, 0x66, 0x1e, 0x52, 0xb8, 0x61, 0x4d, 0xc6, - 0x05, 0x6f, 0xfb, 0xdb, 0xdd, 0x40, 0xe6, 0x28, 0x5a, 0x47, 0x4d, 0xc9, 0x42, 0xf7, 0x88, 0xab, 0x08, 0xe9, 0xe7, - 0x85, 0xd2, 0x2d, 0xb0, 0xd3, 0xed, 0xef, 0xd0, 0xba, 0x5b, 0xd4, 0xc5, 0xf2, 0xd0, 0xc3, 0xce, 0x91, 0x7f, 0x04, - 0xef, 0x8f, 0x02, 0x16, 0xc3, 0x4f, 0x13, 0x65, 0x07, 0x6d, 0xf6, 0xfa, 0x7e, 0xb0, 0x4d, 0xbf, 0xa9, 0xb1, 0x1b, - 0xbc, 0x1d, 0xf0, 0x57, 0x1d, 0xac, 0xc2, 0x61, 0x0f, 0xcc, 0x27, 0x16, 0xbf, 0x3f, 0xbf, 0xd8, 0xd7, 0xe0, 0xf8, - 0x44, 0xb8, 0xcd, 0x54, 0x3e, 0xc0, 0xdb, 0x24, 0xb7, 0x74, 0xbd, 0x30, 0xf2, 0xea, 0x02, 0x52, 0x56, 0xce, 0x89, - 0xeb, 0x8b, 0x02, 0x57, 0xa0, 0x85, 0x12, 0x8f, 0x6e, 0x0b, 0xde, 0xb3, 0x86, 0xb4, 0x77, 0x1b, 0x63, 0xd3, 0x69, - 0xef, 0x38, 0x15, 0x87, 0x99, 0x2c, 0xcd, 0x26, 0xe4, 0xdf, 0xae, 0xa8, 0x53, 0x35, 0x70, 0x9f, 0x5f, 0xd7, 0x57, - 0xb3, 0x74, 0x37, 0xee, 0xe7, 0x4f, 0xd9, 0x9e, 0xb0, 0x95, 0x0d, 0x63, 0x76, 0xc8, 0xef, 0x0b, 0x0d, 0xe8, 0x7a, - 0x34, 0x8b, 0x90, 0xfd, 0x40, 0x80, 0xc1, 0x57, 0xe7, 0x8c, 0xa5, 0x59, 0xd9, 0x37, 0xfd, 0xc2, 0x43, 0x49, 0x41, - 0x8c, 0xca, 0x5e, 0x03, 0x64, 0xb4, 0x24, 0x5b, 0xa7, 0xc5, 0x7b, 0x61, 0x01, 0xa3, 0xef, 0x53, 0xe8, 0x54, 0x9f, - 0x64, 0x08, 0xab, 0x2e, 0xd1, 0x78, 0x4e, 0x7b, 0xc4, 0xd7, 0x79, 0x3e, 0x7c, 0x1d, 0x8b, 0x3d, 0x57, 0x58, 0x66, - 0xd8, 0x4c, 0x8c, 0x43, 0x03, 0xf1, 0xc4, 0xa1, 0xfb, 0x91, 0xd1, 0x62, 0xdc, 0x5a, 0x50, 0xc3, 0xe3, 0x2f, 0xe7, - 0x89, 0xa5, 0xeb, 0xdb, 0x80, 0x0c, 0x53, 0x44, 0x9c, 0x59, 0xd4, 0xeb, 0x8b, 0x6a, 0xd8, 0xbd, 0x2e, 0x2a, 0x08, - 0x9a, 0xcd, 0xb7, 0xf5, 0xe0, 0x06, 0xdb, 0x45, 0xed, 0x87, 0x2c, 0xd1, 0xda, 0xba, 0xdf, 0xb4, 0x1b, 0x64, 0x9f, - 0x33, 0x0e, 0xda, 0x40, 0xc0, 0xf8, 0xde, 0xbf, 0xb4, 0xd5, 0xd9, 0xde, 0x3a, 0xcd, 0x5f, 0x88, 0x8b, 0xbf, 0x93, - 0xb0, 0xbc, 0x9b, 0xe1, 0xa0, 0x94, 0x90, 0xe1, 0x04, 0x11, 0xa6, 0xc2, 0xba, 0xb8, 0xe4, 0xb3, 0x0b, 0x13, 0x2b, - 0x23, 0xac, 0x88, 0x76, 0xc4, 0x37, 0x7c, 0x9f, 0xb7, 0x05, 0x04, 0xb1, 0xb3, 0x65, 0xcc, 0x78, 0x46, 0x44, 0x89, - 0x8c, 0xec, 0x30, 0xb1, 0x69, 0x36, 0x61, 0xea, 0xd8, 0x6d, 0x32, 0x68, 0xea, 0x60, 0x9c, 0xc2, 0x06, 0xba, 0x1b, - 0xaa, 0xad, 0xb6, 0x92, 0x8c, 0xf9, 0x85, 0xac, 0x9d, 0xed, 0x8f, 0xea, 0x65, 0x5e, 0x6c, 0x3f, 0xdb, 0x86, 0xe6, - 0x55, 0x31, 0x86, 0x76, 0x20, 0xb3, 0x23, 0xdc, 0x65, 0xea, 0x1e, 0xd2, 0x78, 0xa2, 0x50, 0x6d, 0x25, 0x08, 0x07, - 0xa0, 0x90, 0xa6, 0x39, 0xe3, 0x02, 0xb3, 0xe8, 0xa3, 0x2a, 0xbc, 0xd3, 0x41, 0x59, 0x2d, 0x0d, 0xa0, 0x04, 0x88, - 0xe3, 0x4e, 0x3a, 0x8c, 0xe4, 0xc1, 0x5e, 0xa9, 0x3b, 0xb5, 0x6a, 0x9d, 0xb9, 0x58, 0xec, 0x47, 0x97, 0x5a, 0xec, - 0x11, 0xa6, 0x59, 0x52, 0xeb, 0x07, 0x5a, 0x68, 0xcf, 0x37, 0x5b, 0x13, 0xf3, 0x94, 0x71, 0x48, 0x7b, 0x68, 0x3f, - 0x14, 0xd4, 0x7a, 0x09, 0x8f, 0x95, 0x41, 0x89, 0x64, 0xa7, 0xa6, 0x9d, 0x95, 0x69, 0x0a, 0xde, 0x65, 0x99, 0x78, - 0x15, 0xfa, 0x1d, 0xe1, 0xdd, 0x96, 0x59, 0xdb, 0xc8, 0xc4, 0xcd, 0x49, 0xa4, 0x58, 0x0e, 0xce, 0x35, 0xbc, 0x2d, - 0x3a, 0x76, 0xf1, 0xdb, 0x4c, 0xad, 0x5f, 0xb0, 0x8c, 0x38, 0x5a, 0xc2, 0xcd, 0x0f, 0xe9, 0x91, 0x88, 0x02, 0xa3, - 0xfe, 0x4d, 0x5e, 0xc5, 0x89, 0x1b, 0x66, 0xfc, 0x8e, 0x86, 0x38, 0x54, 0x87, 0xba, 0x67, 0x89, 0x15, 0x88, 0x85, - 0xf3, 0xf7, 0xd5, 0x4d, 0x20, 0x97, 0xde, 0x56, 0xab, 0xe6, 0x61, 0xd4, 0x65, 0xdf, 0x83, 0x3f, 0x85, 0x31, 0x35, - 0x9f, 0xbc, 0x2a, 0xdf, 0x70, 0xcf, 0x64, 0x29, 0x97, 0xf0, 0xba, 0xde, 0x52, 0x73, 0x9c, 0xcb, 0x37, 0x5c, 0x56, - 0x59, 0x6a, 0x33, 0x82, 0x49, 0xdf, 0x5a, 0xe1, 0x38, 0x82, 0x35, 0x14, 0x91, 0xb8, 0x39, 0xa8, 0x83, 0xcd, 0xb1, - 0x0e, 0xe5, 0x36, 0x13, 0xac, 0x43, 0x36, 0xd8, 0x03, 0xc2, 0xa9, 0xc5, 0x66, 0x8e, 0x7d, 0x6c, 0x08, 0x07, 0x74, - 0xdc, 0x9a, 0x22, 0x4a, 0x4e, 0xdd, 0x89, 0xa7, 0x96, 0xe5, 0xbb, 0x19, 0xd3, 0x74, 0x7c, 0x87, 0x18, 0x59, 0x12, - 0x8b, 0xdc, 0xcb, 0x10, 0x97, 0x43, 0x8b, 0xf6, 0x2c, 0x55, 0x21, 0x37, 0xe8, 0xd6, 0x2d, 0x37, 0x1c, 0x3e, 0x7d, - 0x38, 0x2e, 0x7a, 0x22, 0xda, 0x4a, 0x7c, 0x39, 0x85, 0x54, 0x4a, 0xf6, 0x93, 0x0a, 0x2f, 0x54, 0x48, 0xa7, 0xcf, - 0x8a, 0x04, 0xdc, 0xdc, 0x99, 0x0b, 0x57, 0x53, 0xae, 0x65, 0x8d, 0x76, 0x93, 0x9c, 0x08, 0x15, 0x96, 0xcc, 0x18, - 0xbd, 0x78, 0x3f, 0x8b, 0x16, 0xdb, 0x39, 0xc5, 0x76, 0xd8, 0xd4, 0xd4, 0x79, 0x87, 0x22, 0xa7, 0xa1, 0xb2, 0x8c, - 0xc4, 0x2c, 0xca, 0x21, 0x3e, 0x3a, 0x75, 0xbe, 0xc7, 0x28, 0x75, 0x05, 0x73, 0xaa, 0x4d, 0xc9, 0x83, 0xd3, 0xc5, - 0xf9, 0x17, 0x6e, 0x37, 0xfd, 0xe3, 0x28, 0xe8, 0xb7, 0x5a, 0x35, 0x51, 0xad, 0x1c, 0x9a, 0x5d, 0xd7, 0x6e, 0x34, - 0x26, 0xc7, 0x0e, 0x70, 0x67, 0x13, 0xc4, 0x18, 0x3e, 0x2e, 0xc3, 0x2c, 0x9a, 0xe7, 0x53, 0x7d, 0xfd, 0xf3, 0x20, - 0xca, 0xb6, 0x4c, 0xdd, 0x0a, 0xa3, 0xc0, 0xa5, 0x81, 0x07, 0xe3, 0xa8, 0x21, 0x86, 0xcd, 0xa3, 0xa3, 0x6d, 0x22, - 0x93, 0x74, 0xcf, 0x6a, 0xae, 0x00, 0x4d, 0xa7, 0x20, 0xf2, 0xef, 0x71, 0x9b, 0x2f, 0x38, 0x8e, 0x4e, 0xe9, 0xf9, - 0x17, 0xa5, 0x1f, 0x69, 0xf0, 0xc6, 0x78, 0x75, 0xc1, 0xaa, 0x27, 0x23, 0xef, 0x88, 0x87, 0x39, 0x0a, 0xe4, 0x07, - 0xf0, 0x4d, 0xe9, 0x82, 0x73, 0x63, 0xf7, 0x3d, 0xc8, 0x96, 0xed, 0x68, 0x55, 0xc4, 0x1a, 0xf9, 0x1a, 0x27, 0x2e, - 0xb5, 0xfe, 0x92, 0xcc, 0x3a, 0x09, 0xf6, 0x35, 0xf2, 0x78, 0x47, 0xbc, 0xcf, 0xf6, 0x76, 0x68, 0xe3, 0x35, 0x92, - 0xb0, 0xef, 0xb6, 0xeb, 0xaa, 0x2b, 0x4e, 0x7f, 0x62, 0x01, 0xe1, 0x02, 0x36, 0x16, 0xe8, 0x9b, 0xdb, 0x86, 0xd2, - 0xb9, 0xee, 0x1a, 0x7c, 0xaf, 0xf1, 0xce, 0x3b, 0x3b, 0xb8, 0x8a, 0x93, 0x44, 0x60, 0x76, 0xa1, 0x34, 0x26, 0xea, - 0x17, 0x86, 0xe7, 0x6a, 0xcf, 0xb7, 0xea, 0x65, 0x54, 0xb3, 0x67, 0x02, 0xfe, 0x3a, 0x1a, 0x1f, 0x95, 0x79, 0x83, - 0x85, 0xdb, 0x3a, 0x85, 0xee, 0xfc, 0x7d, 0x00, 0xaf, 0xbc, 0x6b, 0xb6, 0x2c, 0xe6, 0x3b, 0xce, 0x82, 0xa5, 0xcd, - 0xdb, 0x5d, 0x99, 0xe0, 0x97, 0x1f, 0x70, 0xaf, 0xf8, 0xd2, 0xc1, 0x93, 0x7e, 0xdd, 0x52, 0xc0, 0x5d, 0xc0, 0xe4, - 0xa5, 0x1b, 0x6e, 0x42, 0xdc, 0xac, 0x72, 0xd3, 0xb7, 0x48, 0xf9, 0xe9, 0xe9, 0xac, 0x79, 0xea, 0x10, 0xde, 0x78, - 0x54, 0x87, 0xca, 0x68, 0x6e, 0xdd, 0xc2, 0x95, 0xfe, 0x05, 0xb7, 0x35, 0x77, 0x30, 0xc3, 0x89, 0x2c, 0x89, 0xc7, - 0x93, 0xee, 0x1e, 0xa0, 0xcc, 0x03, 0x2f, 0x22, 0x29, 0xa2, 0x6d, 0x60, 0x07, 0x2d, 0x28, 0x6b, 0x0d, 0xa2, 0xd6, - 0xde, 0x23, 0x66, 0x7b, 0x69, 0xf7, 0x43, 0xf7, 0xe1, 0x03, 0x0f, 0xd6, 0x44, 0xc8, 0x19, 0x4c, 0x28, 0x7e, 0x97, - 0xf6, 0x73, 0xd8, 0x33, 0xc2, 0x95, 0x56, 0x28, 0x78, 0x8e, 0x1b, 0x54, 0x7d, 0x9f, 0x2d, 0x20, 0x5a, 0x24, 0x20, - 0x67, 0xbb, 0x16, 0xd6, 0xa8, 0x18, 0xf9, 0x49, 0x0c, 0x95, 0xd4, 0xb6, 0x7c, 0x83, 0xa5, 0xbf, 0xae, 0x02, 0x49, - 0x20, 0x31, 0x27, 0xf5, 0xbc, 0xb6, 0x2d, 0x16, 0x37, 0x4b, 0x36, 0x0f, 0x15, 0xa5, 0xe7, 0xe5, 0xd8, 0x79, 0x07, - 0xf5, 0x28, 0xb8, 0x2c, 0xdb, 0xeb, 0xdc, 0x2e, 0xed, 0xae, 0x75, 0x18, 0x4e, 0x52, 0x4e, 0x31, 0xe0, 0xa1, 0x6d, - 0x3d, 0x72, 0xb1, 0xf8, 0xf7, 0x40, 0x1a, 0xde, 0x5d, 0x7e, 0x78, 0x73, 0x4b, 0x6b, 0x4c, 0xd4, 0x66, 0x2a, 0x12, - 0xe1, 0xe4, 0x86, 0x15, 0xc9, 0x90, 0x26, 0x12, 0x3d, 0x7c, 0x91, 0x6f, 0xae, 0x35, 0xac, 0x12, 0xd9, 0x90, 0x61, - 0xb3, 0xd9, 0xcd, 0x64, 0xdc, 0xca, 0x76, 0x7f, 0x3a, 0xf4, 0x26, 0xc8, 0xd6, 0x89, 0xd2, 0x3c, 0x77, 0xd8, 0x62, - 0xed, 0x9e, 0xb1, 0xa8, 0x9f, 0x1c, 0x65, 0x8e, 0x78, 0x43, 0x4c, 0xb4, 0x4d, 0xb9, 0xb6, 0x66, 0x1e, 0x21, 0x70, - 0x6f, 0xed, 0x3f, 0x2b, 0xf6, 0xf1, 0x1a, 0xd3, 0xc7, 0xf4, 0x0b, 0xee, 0xf9, 0xc4, 0xc3, 0xcf, 0x34, 0xc9, 0x0d, - 0xb1, 0xdd, 0x45, 0x3c, 0xe4, 0xa3, 0xbb, 0x61, 0xca, 0x84, 0x65, 0xda, 0x5c, 0xb5, 0x9c, 0x1b, 0x83, 0x54, 0xa1, - 0x48, 0xe5, 0x3e, 0xa2, 0xeb, 0xb0, 0x1f, 0xce, 0xf2, 0x3d, 0x48, 0x64, 0xbe, 0x4f, 0x22, 0x10, 0xeb, 0x1f, 0x05, - 0x71, 0x8e, 0x25, 0x2f, 0x8d, 0x22, 0x8d, 0xfe, 0x84, 0x52, 0x96, 0x72, 0x08, 0xf8, 0xe6, 0x80, 0x73, 0x15, 0x5b, - 0xff, 0x7d, 0xf6, 0x7d, 0x98, 0x76, 0x7d, 0xce, 0xee, 0x16, 0x12, 0xde, 0x72, 0xe2, 0x4e, 0xe5, 0xf1, 0xa9, 0x90, - 0x7b, 0x0f, 0x72, 0x2d, 0xbf, 0xed, 0x86, 0x86, 0xce, 0xf1, 0xb2, 0x45, 0x71, 0x55, 0xa7, 0xf2, 0x47, 0x51, 0xab, - 0xaa, 0xa4, 0x2a, 0x7b, 0x1e, 0x39, 0x93, 0x93, 0xb3, 0xdc, 0xab, 0x0a, 0x43, 0x03, 0x8f, 0x38, 0x5b, 0x62, 0x9d, - 0xbd, 0xc7, 0xcc, 0xe2, 0x84, 0x8f, 0x42, 0x03, 0xc1, 0x2a, 0xe9, 0x13, 0x8e, 0xe8, 0x0b, 0xb4, 0xd3, 0xfa, 0xd2, - 0xcd, 0xed, 0x47, 0x96, 0xb2, 0x83, 0xa3, 0xf1, 0xca, 0x8a, 0x7c, 0x9d, 0x02, 0x31, 0x53, 0xac, 0x62, 0xe4, 0xf3, - 0x1b, 0x84, 0x44, 0xf3, 0x8b, 0xe9, 0x82, 0xf6, 0x2e, 0x6b, 0x51, 0x1d, 0x3e, 0x6b, 0xf6, 0xca, 0x0b, 0x40, 0x1e, - 0x57, 0x15, 0x87, 0x48, 0x7b, 0x83, 0x38, 0x4d, 0x88, 0x7a, 0x76, 0xdb, 0xb3, 0x85, 0x38, 0x31, 0xcb, 0xd1, 0x62, - 0xd2, 0xab, 0x12, 0xd9, 0x6f, 0x4f, 0x4f, 0x20, 0x25, 0x3a, 0x63, 0x2c, 0x19, 0x69, 0x4b, 0xf9, 0x86, 0xe6, 0xf4, - 0x1e, 0xd6, 0x31, 0x11, 0xb1, 0x5a, 0x00, 0x52, 0x0d, 0x29, 0xf4, 0x35, 0x6a, 0x47, 0x44, 0xf8, 0xf9, 0xc8, 0x52, - 0x29, 0x32, 0xc6, 0x37, 0xf6, 0x23, 0x6d, 0x31, 0xab, 0x88, 0x53, 0xc4, 0xac, 0x61, 0x18, 0xd9, 0xb8, 0xc4, 0x28, - 0xa8, 0x56, 0x8f, 0x37, 0xf8, 0x04, 0x83, 0x94, 0x62, 0xab, 0xeb, 0x71, 0xdc, 0xc4, 0x4f, 0x47, 0x22, 0xc2, 0x7d, - 0x82, 0x38, 0x29, 0x81, 0x8d, 0xe7, 0x6f, 0xce, 0x54, 0xe7, 0x7c, 0x69, 0xb0, 0xe7, 0x48, 0xf5, 0x38, 0xc3, 0x40, - 0x13, 0x7c, 0xf5, 0xa3, 0xd9, 0x1f, 0xcb, 0x37, 0x32, 0x8b, 0x3b, 0x09, 0xa9, 0x95, 0xd3, 0xad, 0x36, 0x5b, 0x22, - 0x7e, 0x80, 0x0b, 0x67, 0xbc, 0x47, 0x35, 0x77, 0xf9, 0xa5, 0x26, 0x06, 0x56, 0x98, 0x13, 0x72, 0x37, 0x47, 0x92, - 0xbf, 0x00, 0xe3, 0x66, 0x0d, 0x6d, 0x2e, 0xc7, 0x2e, 0x38, 0x09, 0xe4, 0xea, 0xa6, 0xf4, 0x9b, 0xcf, 0x9e, 0x81, - 0xe9, 0x61, 0xf9, 0xd1, 0xef, 0x32, 0xff, 0x65, 0xac, 0x53, 0x13, 0xfc, 0xc8, 0x51, 0x59, 0x9f, 0xcb, 0xe3, 0xdf, - 0x01, 0xb7, 0x67, 0x7d, 0xe3, 0xcf, 0x93, 0x20, 0xce, 0x35, 0x7f, 0x66, 0xc1, 0x44, 0x36, 0xbb, 0xd6, 0xde, 0xcf, - 0x9f, 0xc1, 0x9b, 0xfb, 0x3d, 0x42, 0xe5, 0x6b, 0xe7, 0x14, 0xea, 0xa6, 0x01, 0x16, 0x7a, 0xf5, 0x60, 0x1f, 0x90, - 0x9b, 0x7d, 0x88, 0x0a, 0x14, 0x39, 0xa2, 0xd2, 0x35, 0xe3, 0x1a, 0xb6, 0x55, 0x3b, 0x94, 0x3d, 0xc3, 0x79, 0x4f, - 0x9b, 0x5d, 0xb5, 0xad, 0x57, 0x69, 0x13, 0x52, 0x98, 0xc2, 0x1a, 0xe8, 0x56, 0x37, 0xec, 0xdc, 0x1c, 0x2c, 0xdf, - 0x5a, 0x06, 0x1e, 0x16, 0xae, 0x98, 0xbd, 0x98, 0xf9, 0x8e, 0x0e, 0xfc, 0x59, 0xca, 0x2b, 0x0a, 0xb5, 0xfa, 0x8d, - 0xe3, 0xa8, 0x87, 0x36, 0xe0, 0x0d, 0x7f, 0x9d, 0x50, 0x9d, 0x3d, 0x67, 0x0b, 0x23, 0x17, 0xe2, 0xd7, 0xba, 0xc1, - 0x27, 0x74, 0xa9, 0x0a, 0x26, 0xe0, 0x4b, 0x9b, 0x1d, 0x0f, 0x5e, 0x87, 0x96, 0xa4, 0xf2, 0xb8, 0x79, 0x8c, 0xe5, - 0xdc, 0x4e, 0xb9, 0x54, 0x2b, 0xf3, 0xa3, 0x1b, 0x25, 0xd8, 0x20, 0x90, 0x24, 0x58, 0x84, 0xf0, 0x4f, 0x30, 0x67, - 0x1c, 0x89, 0x21, 0x7b, 0xa3, 0x62, 0x8a, 0x16, 0x14, 0x0c, 0x91, 0x52, 0xb6, 0x58, 0x60, 0xb3, 0xf7, 0x08, 0x7f, - 0x7f, 0xdf, 0x3a, 0xf5, 0xb4, 0xef, 0x9d, 0x02, 0xed, 0xdb, 0x27, 0xa5, 0xb4, 0xef, 0x9f, 0x14, 0x9d, 0xa5, 0x56, - 0x19, 0xfb, 0x6a, 0xc9, 0xbe, 0x1a, 0xd9, 0x63, 0x3b, 0xdb, 0x43, 0x2b, 0x8e, 0x6b, 0xd0, 0x8e, 0xc5, 0x82, 0x6f, - 0xc9, 0x01, 0xc7, 0x11, 0xcb, 0x92, 0xf5, 0x63, 0xa1, 0xb9, 0x77, 0xb5, 0x1e, 0xf9, 0xf6, 0x00, 0x15, 0x85, 0xb7, - 0xc3, 0xda, 0x8d, 0xac, 0x2c, 0xcf, 0x34, 0xb7, 0x4e, 0xe2, 0xd4, 0xd9, 0xde, 0x42, 0xf1, 0x74, 0xea, 0x08, 0x7e, - 0xd6, 0xe4, 0x70, 0x86, 0x4a, 0x13, 0xf8, 0x8f, 0x1c, 0x3f, 0x36, 0x5a, 0x5b, 0xd1, 0x78, 0xf3, 0xbd, 0x27, 0x1a, - 0x9a, 0xbf, 0xb8, 0x8a, 0x58, 0x98, 0x96, 0x93, 0x7b, 0x07, 0x53, 0x1f, 0x4a, 0xd6, 0xe2, 0x76, 0x07, 0x64, 0xb6, - 0x94, 0x97, 0x39, 0x41, 0x08, 0xa7, 0xba, 0x85, 0xff, 0x2c, 0xa0, 0x31, 0x27, 0x2d, 0x17, 0x53, 0xf9, 0xbc, 0xd5, - 0x86, 0xda, 0xce, 0x59, 0x83, 0x78, 0xec, 0xca, 0x74, 0x57, 0x82, 0x29, 0x3c, 0x9f, 0xc5, 0x15, 0x90, 0xfa, 0x85, - 0x35, 0xff, 0x46, 0xd9, 0xc3, 0xe5, 0x4e, 0x4c, 0x83, 0xff, 0xf7, 0x64, 0x3b, 0x08, 0x27, 0x74, 0x35, 0x4e, 0xb9, - 0x24, 0xe2, 0x7e, 0x6e, 0xa5, 0xed, 0x99, 0x37, 0x72, 0x7d, 0xfb, 0x8c, 0x61, 0x7a, 0xae, 0x42, 0x20, 0x53, 0x68, - 0x3f, 0xdd, 0x3f, 0x8f, 0x71, 0x48, 0xb0, 0x62, 0xcf, 0x09, 0x56, 0x19, 0x98, 0x92, 0x77, 0x33, 0x99, 0x9e, 0xbf, - 0xfc, 0x5f, 0xe6, 0xf7, 0x92, 0xf5, 0xa0, 0x37, 0x7b, 0xcb, 0x36, 0xb7, 0x85, 0x75, 0x69, 0x31, 0xb3, 0x7e, 0x34, - 0xd3, 0xfa, 0x5f, 0x71, 0x80, 0xb7, 0xdb, 0xe9, 0x8a, 0x3f, 0xaa, 0x0c, 0xd2, 0x38, 0x64, 0xdd, 0x6f, 0x4b, 0xd6, - 0x03, 0xde, 0xed, 0xed, 0xf3, 0x5b, 0x1c, 0xfe, 0xb1, 0x09, 0x7c, 0x4b, 0x17, 0x00, 0x02, 0xf8, 0x91, 0xbb, 0x1c, - 0xbb, 0x9c, 0xc9, 0x9d, 0xeb, 0x0a, 0x0b, 0xaa, 0x96, 0x43, 0x16, 0x2e, 0x96, 0x6c, 0x41, 0x3e, 0x5e, 0x13, 0xd9, - 0xe8, 0xc0, 0xec, 0x12, 0xb1, 0xf7, 0x44, 0x09, 0xde, 0x6c, 0xf3, 0x29, 0xd1, 0xf3, 0xe7, 0x04, 0xda, 0x66, 0xd7, - 0x89, 0x0d, 0xb9, 0xb6, 0x38, 0x15, 0x27, 0x00, 0xec, 0x7b, 0xe6, 0xc2, 0xe0, 0x6c, 0xa4, 0xf6, 0x41, 0x0b, 0xa7, - 0xb7, 0x04, 0xfa, 0xd9, 0x38, 0x45, 0xde, 0xef, 0x00, 0x95, 0x52, 0x78, 0xe2, 0x10, 0x13, 0x2a, 0x76, 0xf8, 0x9c, - 0x52, 0x9f, 0x43, 0x8e, 0x9c, 0x77, 0xc0, 0x89, 0x5b, 0xda, 0x74, 0x63, 0x79, 0xbb, 0xe1, 0xbb, 0x8a, 0x74, 0x65, - 0xf5, 0xb0, 0x84, 0xb6, 0xcd, 0xd1, 0x19, 0x67, 0x94, 0xa0, 0xc4, 0x08, 0x29, 0xc3, 0xf3, 0x63, 0x30, 0x48, 0x26, - 0xe3, 0x30, 0xd1, 0x6b, 0xce, 0x0a, 0xa3, 0xff, 0x44, 0x08, 0x23, 0x84, 0x9f, 0x5f, 0x67, 0x6c, 0xdf, 0xa3, 0xbb, - 0xb6, 0xe9, 0x5e, 0x6f, 0x15, 0xb1, 0xcf, 0x2b, 0x0d, 0x4a, 0xa5, 0xd2, 0x58, 0xdb, 0x8c, 0x7f, 0x75, 0x52, 0x9d, - 0x46, 0x1d, 0x8e, 0x70, 0x76, 0xa6, 0xcd, 0xf9, 0xbf, 0x9e, 0x99, 0xb9, 0xd7, 0xce, 0xcb, 0x9e, 0x19, 0xeb, 0xc6, - 0x25, 0x91, 0x16, 0xe4, 0x26, 0x0d, 0x25, 0xeb, 0xb1, 0xd1, 0xde, 0x94, 0x4c, 0xfd, 0xc8, 0xa4, 0x80, 0x8d, 0xb1, - 0xda, 0xe1, 0x32, 0x3e, 0xcd, 0xfb, 0xa8, 0x24, 0x0b, 0x2e, 0xc4, 0xf9, 0x70, 0xbb, 0xb1, 0x22, 0x85, 0x1d, 0x68, - 0xf2, 0xeb, 0x4f, 0xc9, 0xae, 0xf0, 0x9d, 0xdf, 0x81, 0x64, 0xd6, 0x17, 0x2b, 0xc5, 0x06, 0x75, 0x05, 0x7a, 0x03, - 0x2e, 0xdf, 0xd1, 0x1b, 0x65, 0x34, 0x7c, 0x5d, 0x4a, 0x54, 0x92, 0x50, 0x63, 0xa5, 0x60, 0xb7, 0x58, 0x97, 0x51, - 0x14, 0x17, 0x6b, 0xa2, 0x5f, 0x15, 0x4c, 0x16, 0xdb, 0x6e, 0xe0, 0xdc, 0x04, 0xea, 0x51, 0x07, 0x27, 0xfa, 0x3b, - 0x44, 0xde, 0x16, 0xc5, 0x86, 0x6c, 0x1b, 0x88, 0xa1, 0x15, 0xd7, 0x75, 0xaa, 0xb1, 0x3e, 0xf2, 0x80, 0x1e, 0xf7, - 0xaf, 0x8e, 0x21, 0x78, 0xf8, 0x69, 0xc0, 0x52, 0x57, 0x9d, 0x3a, 0xbc, 0x7d, 0x74, 0x63, 0x49, 0xea, 0x71, 0x7e, - 0xf3, 0x4f, 0xc5, 0x36, 0x2d, 0x4f, 0xb7, 0xc8, 0x0d, 0x89, 0xe0, 0x5d, 0x41, 0xf6, 0xd3, 0xc1, 0x6d, 0x7b, 0xa6, - 0x28, 0x0a, 0x2f, 0x94, 0xb4, 0xbc, 0x29, 0x3c, 0x8c, 0xe3, 0x46, 0x8a, 0x1a, 0x2a, 0x32, 0xfe, 0x31, 0x36, 0x2c, - 0x92, 0x9d, 0xad, 0x0f, 0x63, 0xe7, 0x95, 0x90, 0x8f, 0x2b, 0xd7, 0xea, 0x46, 0xa6, 0x63, 0x5b, 0x14, 0x39, 0x7a, - 0x9d, 0x07, 0xa0, 0xf2, 0x47, 0x61, 0x12, 0x50, 0x1c, 0x89, 0x00, 0xa2, 0x3a, 0xf1, 0xa2, 0xe8, 0x81, 0xcd, 0xa1, - 0x19, 0x56, 0x83, 0x7c, 0x2a, 0xfa, 0x1b, 0x15, 0x9d, 0xd9, 0xf1, 0x50, 0xd8, 0x8b, 0x4c, 0xac, 0x3e, 0xe6, 0xae, - 0x43, 0x91, 0x36, 0x72, 0xea, 0x3b, 0xd7, 0x98, 0x37, 0xd0, 0xc3, 0x22, 0x14, 0x7f, 0x61, 0x83, 0x98, 0xb2, 0x01, - 0x2b, 0x64, 0xec, 0x79, 0x04, 0x30, 0x4b, 0xf2, 0x45, 0x12, 0x78, 0xd3, 0xaf, 0x40, 0xbf, 0xc1, 0x7d, 0xb5, 0x6f, - 0x26, 0x4d, 0xb3, 0x70, 0x77, 0x63, 0xbf, 0x2f, 0x36, 0x72, 0x4f, 0x08, 0x22, 0x58, 0x92, 0xd9, 0xb2, 0x96, 0xde, - 0x03, 0x7e, 0xa5, 0xe0, 0x19, 0x78, 0xc8, 0x00, 0x8e, 0x98, 0x96, 0xb8, 0xae, 0xd6, 0x93, 0xab, 0xc3, 0x56, 0x6e, - 0x0b, 0x25, 0x38, 0x44, 0xd2, 0xe8, 0x96, 0x4a, 0xb3, 0x94, 0xee, 0x99, 0xad, 0xbb, 0x91, 0x71, 0xfc, 0x65, 0x50, - 0x9e, 0x58, 0x8f, 0x5f, 0x93, 0xc8, 0x96, 0x44, 0x14, 0x97, 0x5f, 0xe7, 0x90, 0xdd, 0x58, 0xa9, 0x98, 0x0c, 0x54, - 0x3d, 0x79, 0xaa, 0x09, 0xde, 0x60, 0x71, 0x17, 0x1c, 0xe9, 0x03, 0xed, 0x09, 0xc5, 0x49, 0xaa, 0x4a, 0xa9, 0x87, - 0x7e, 0xc2, 0x57, 0xd8, 0xe7, 0xa2, 0xb7, 0xd9, 0x31, 0xcd, 0xd8, 0x67, 0x34, 0xc1, 0x80, 0x3a, 0x09, 0x4e, 0xbb, - 0x66, 0x78, 0xe4, 0x48, 0x6c, 0x3a, 0x90, 0x8f, 0xf1, 0x54, 0xe8, 0x36, 0x6e, 0x56, 0x21, 0x8e, 0x86, 0xd0, 0xfd, - 0x30, 0x14, 0x46, 0x3f, 0xa3, 0x74, 0xac, 0x3e, 0xed, 0xd7, 0x5c, 0xd4, 0xce, 0x98, 0xa2, 0x29, 0x2f, 0xbb, 0xa6, - 0x00, 0x6f, 0xa4, 0xbc, 0xc1, 0x0a, 0xf8, 0xfe, 0xb2, 0x59, 0x57, 0x8f, 0x17, 0x36, 0xf9, 0x0f, 0xae, 0x83, 0x8d, - 0x84, 0x09, 0xfc, 0x11, 0x92, 0x99, 0x2d, 0x82, 0x35, 0x8c, 0xf3, 0x92, 0x58, 0x38, 0x7a, 0x9c, 0xef, 0x07, 0xd9, - 0x1f, 0x57, 0x8d, 0x07, 0x61, 0x0b, 0x0f, 0xad, 0xe4, 0x9c, 0xa8, 0xd7, 0xd4, 0xa9, 0x91, 0x0f, 0x22, 0x93, 0xc0, - 0x84, 0xf2, 0x3c, 0xc1, 0x34, 0xab, 0xb3, 0x59, 0x50, 0xdb, 0x44, 0xc5, 0xa0, 0xd0, 0x8d, 0xdb, 0x99, 0x20, 0xc9, - 0x86, 0xe0, 0x94, 0x97, 0x65, 0xc3, 0xed, 0x75, 0x6b, 0xe6, 0x45, 0xf3, 0xd7, 0x64, 0x87, 0xa5, 0xdf, 0x05, 0x1d, - 0x6b, 0xe3, 0xf5, 0x60, 0x7b, 0xd0, 0x79, 0x58, 0xbc, 0x50, 0x3a, 0x8d, 0xaa, 0x9b, 0x7a, 0x11, 0x37, 0xfb, 0x39, - 0x75, 0x35, 0xd1, 0x6c, 0x09, 0x48, 0x67, 0xa3, 0x7c, 0x8f, 0x9d, 0xb8, 0x46, 0x51, 0x5a, 0x4b, 0xab, 0x5b, 0xe6, - 0x1d, 0x8b, 0x91, 0xbb, 0x81, 0x51, 0x62, 0xed, 0x22, 0x86, 0x9a, 0x9f, 0xc3, 0xdc, 0x9e, 0x98, 0x40, 0x45, 0xff, - 0x3a, 0x9f, 0xcc, 0xec, 0x62, 0x9a, 0x4a, 0x32, 0xcc, 0x07, 0xa5, 0x6f, 0x89, 0xe6, 0xee, 0xf1, 0x9c, 0x93, 0xd2, - 0xb6, 0xad, 0x62, 0x1d, 0xba, 0x85, 0x81, 0x0f, 0x5c, 0x4d, 0x03, 0xc1, 0x15, 0xbd, 0xc5, 0x98, 0x67, 0xf0, 0x7c, - 0xc0, 0xec, 0x1b, 0x79, 0x3e, 0x2f, 0x45, 0xde, 0x3e, 0x91, 0x19, 0xbe, 0x50, 0xa0, 0x98, 0xde, 0xe9, 0x7c, 0x6f, - 0x9f, 0x87, 0x1b, 0x77, 0x59, 0xe0, 0xbe, 0x24, 0x0e, 0x19, 0xfe, 0x75, 0x17, 0x5b, 0xc6, 0x1a, 0xcf, 0x9c, 0xfb, - 0x2d, 0x89, 0x09, 0xa5, 0xda, 0xae, 0x1f, 0xa2, 0xbc, 0x16, 0x61, 0x52, 0x85, 0xa5, 0xdb, 0x2a, 0xa4, 0x32, 0xf4, - 0x45, 0xa4, 0x8a, 0xc7, 0x99, 0x9b, 0x9d, 0xa1, 0x34, 0x82, 0x0c, 0x05, 0x13, 0x64, 0xb5, 0x4f, 0xa2, 0x79, 0x56, - 0xf2, 0xa0, 0x4d, 0x13, 0xf9, 0xf0, 0xba, 0x2a, 0x63, 0xe1, 0x71, 0xe6, 0xde, 0x76, 0xc4, 0xdc, 0xba, 0x8e, 0xf3, - 0xea, 0x03, 0x75, 0x2b, 0x47, 0xe6, 0xb9, 0x62, 0x6c, 0xc5, 0xd8, 0x3d, 0xa8, 0x45, 0xa0, 0x0c, 0x45, 0x12, 0x0e, - 0x6c, 0x31, 0x7a, 0x7b, 0xa1, 0xce, 0x06, 0xc2, 0xad, 0xb2, 0x3e, 0xaa, 0xc5, 0x6b, 0xda, 0xb6, 0x52, 0x0a, 0x4e, - 0xa0, 0x10, 0x4e, 0x34, 0xf6, 0x9c, 0x0f, 0xff, 0xfc, 0x5c, 0xa7, 0x1e, 0xff, 0x99, 0x10, 0x9b, 0xfd, 0x97, 0xf7, - 0xaf, 0x63, 0x0a, 0xd0, 0xeb, 0x9e, 0x75, 0x45, 0x7a, 0xad, 0xeb, 0x35, 0xd2, 0xab, 0xaf, 0x57, 0xb5, 0x39, 0xe1, - 0x59, 0x2d, 0xb5, 0x51, 0x1b, 0x77, 0x6e, 0x14, 0xeb, 0x30, 0x94, 0x94, 0xd4, 0x7e, 0x4f, 0x4f, 0x3f, 0x8d, 0x55, - 0x99, 0x6f, 0xcd, 0xa4, 0x98, 0x4d, 0x5f, 0x1c, 0xab, 0xf5, 0xb8, 0x8c, 0x10, 0xbb, 0x17, 0x43, 0x6d, 0xa5, 0x3a, - 0x35, 0x75, 0x9b, 0xcf, 0x2f, 0xc6, 0xc5, 0xe5, 0xcb, 0xbf, 0x22, 0xc4, 0xf3, 0x11, 0xe3, 0xa1, 0x8d, 0x76, 0xde, - 0x37, 0x48, 0x8d, 0xcb, 0xcd, 0x11, 0xe7, 0x68, 0x56, 0x15, 0xc6, 0x88, 0xa7, 0x55, 0xe7, 0x2e, 0xb8, 0xf6, 0x20, - 0xf0, 0x73, 0x71, 0x55, 0xa9, 0x48, 0x52, 0xdf, 0x36, 0xb0, 0x2e, 0x37, 0x4b, 0x93, 0xc3, 0xdf, 0x0b, 0x2c, 0xe8, - 0x63, 0x53, 0x95, 0xeb, 0x07, 0x25, 0xc4, 0xd8, 0x29, 0x62, 0xca, 0xa9, 0xf4, 0x2e, 0xac, 0x7c, 0x51, 0x5f, 0x4f, - 0x99, 0xfa, 0x36, 0x88, 0x31, 0x8b, 0x31, 0x27, 0x4b, 0x31, 0x27, 0x79, 0x60, 0xfb, 0x3c, 0x06, 0xc6, 0xc5, 0x24, - 0x10, 0xf9, 0x70, 0xe3, 0xca, 0x58, 0xbe, 0x08, 0x18, 0xac, 0xa2, 0x0d, 0x04, 0xd3, 0x3b, 0x33, 0xed, 0xe2, 0x1f, - 0xf3, 0xcb, 0x41, 0x64, 0x5c, 0x89, 0x21, 0xac, 0x8e, 0xf8, 0xad, 0xd3, 0x0b, 0x64, 0x62, 0xe5, 0x8c, 0x66, 0x09, - 0x98, 0x75, 0xd3, 0x34, 0x38, 0x56, 0x4d, 0x83, 0x4a, 0x33, 0xaf, 0xb0, 0x0c, 0x24, 0x31, 0x30, 0x95, 0x6a, 0xf8, - 0x95, 0x16, 0x09, 0xce, 0xf9, 0xfb, 0xae, 0xcf, 0x29, 0x90, 0xc6, 0x99, 0x46, 0xa5, 0xc0, 0xe7, 0x0e, 0xf8, 0x05, - 0xfa, 0x15, 0x9f, 0x88, 0xe3, 0x34, 0xe9, 0x91, 0xc9, 0xe8, 0x81, 0xda, 0x81, 0x90, 0x59, 0x4b, 0x46, 0x61, 0x1a, - 0x42, 0x28, 0x05, 0x44, 0x7c, 0x3f, 0xcc, 0x45, 0x95, 0x35, 0xaf, 0xc6, 0x02, 0xb7, 0x10, 0x31, 0x23, 0xaa, 0x06, - 0x22, 0xc9, 0x48, 0xae, 0x1b, 0x32, 0x2c, 0x96, 0x26, 0x2d, 0xc6, 0xe0, 0x04, 0xc9, 0x3c, 0x9d, 0x08, 0xfe, 0x65, - 0x48, 0xc6, 0x05, 0x6f, 0x7a, 0xaf, 0x80, 0xbe, 0xc6, 0xc5, 0x64, 0xe7, 0xcd, 0xbc, 0xe8, 0x89, 0xb4, 0x5c, 0xe5, - 0x43, 0xa2, 0xd0, 0xdf, 0xd7, 0x7d, 0xef, 0x58, 0x3d, 0x48, 0xc1, 0xbc, 0x2d, 0x6a, 0x8b, 0x96, 0xed, 0xb5, 0x95, - 0xc7, 0x72, 0xdc, 0x3d, 0x4a, 0x50, 0x00, 0xdd, 0x66, 0xd1, 0x0a, 0x3c, 0x49, 0xd6, 0xd8, 0xa9, 0x4f, 0x44, 0x74, - 0x74, 0x1b, 0x25, 0xb3, 0x23, 0x5b, 0x17, 0x3f, 0x90, 0x91, 0xe4, 0xcc, 0x5c, 0x89, 0xce, 0xff, 0x59, 0xf2, 0x26, - 0x17, 0x33, 0x5b, 0x75, 0xc8, 0x01, 0x6e, 0x3a, 0x13, 0x61, 0x8a, 0xf6, 0x56, 0x66, 0x23, 0x44, 0x86, 0x93, 0x49, - 0x16, 0x64, 0xea, 0xc5, 0x5f, 0x8c, 0x14, 0xfc, 0x47, 0xc4, 0x86, 0x96, 0x3c, 0xd2, 0xff, 0x70, 0x0d, 0xe1, 0x5b, - 0x39, 0x1c, 0x24, 0xd5, 0x7b, 0x2d, 0xb8, 0x2d, 0x2d, 0x1b, 0x66, 0x83, 0x24, 0x3c, 0x3e, 0xbb, 0x7c, 0xe6, 0xdb, - 0x83, 0xfc, 0x43, 0x44, 0x08, 0x84, 0xfa, 0xdf, 0xf4, 0xaa, 0x76, 0xf1, 0x32, 0x2a, 0x4e, 0x83, 0xf2, 0xf5, 0xf8, - 0x8c, 0xc3, 0x1b, 0x2a, 0x2f, 0xe0, 0xb7, 0x6f, 0xe7, 0x1c, 0x98, 0x81, 0x2f, 0x63, 0xab, 0xb1, 0x80, 0xbd, 0x70, - 0xd8, 0x63, 0x28, 0x59, 0xc4, 0xa1, 0xed, 0x6c, 0x84, 0xdb, 0xd0, 0xf5, 0x36, 0xdb, 0xaf, 0x94, 0x72, 0x75, 0xc6, - 0xf9, 0x3b, 0xdb, 0xaa, 0xe6, 0x66, 0xd7, 0x0d, 0x5b, 0x2a, 0xe9, 0x69, 0x5f, 0x6e, 0x30, 0x75, 0x43, 0xf6, 0x36, - 0xd4, 0x5a, 0xbe, 0x19, 0xd6, 0x95, 0x37, 0x0b, 0x83, 0x42, 0xc0, 0x98, 0x61, 0xcd, 0x15, 0xb9, 0xd6, 0xca, 0x7e, - 0x30, 0xc5, 0xfe, 0x30, 0x08, 0x89, 0xa8, 0x8a, 0x24, 0x67, 0x83, 0x1e, 0xe7, 0x6a, 0xed, 0x59, 0x3d, 0x02, 0x4b, - 0x27, 0x96, 0x63, 0xcd, 0x0a, 0x06, 0x43, 0xa9, 0xaa, 0xd5, 0x52, 0x77, 0xb8, 0x4a, 0x9f, 0x6a, 0x79, 0xc5, 0x0b, - 0x12, 0xf6, 0x0b, 0x88, 0x4e, 0x7c, 0x77, 0xf7, 0x24, 0xf2, 0x9d, 0x59, 0x7d, 0x63, 0x2a, 0x4d, 0x94, 0x67, 0xc5, - 0x0a, 0x4a, 0xd6, 0x3b, 0xc0, 0x50, 0x51, 0x63, 0x6c, 0xe8, 0x0e, 0x0d, 0xd6, 0xe6, 0x38, 0xdc, 0x17, 0xf6, 0xdb, - 0x82, 0xec, 0x47, 0xfd, 0x9c, 0x93, 0xfb, 0x68, 0xb3, 0xa8, 0x97, 0xf5, 0x56, 0x63, 0xe4, 0x08, 0xaf, 0x37, 0x27, - 0x55, 0xb6, 0xa0, 0xc9, 0x5e, 0x83, 0xd3, 0x4b, 0x33, 0x75, 0xa1, 0xec, 0xc4, 0x8c, 0x28, 0xd3, 0x41, 0x24, 0x09, - 0xba, 0xb3, 0x1e, 0x04, 0xd7, 0x2c, 0x0b, 0x6b, 0x93, 0x91, 0x7b, 0x30, 0x9c, 0x23, 0x15, 0xd1, 0x25, 0x14, 0xc5, - 0x39, 0x9b, 0xd7, 0x3f, 0x30, 0xe4, 0x28, 0x8f, 0xc5, 0xb2, 0x64, 0x41, 0xbd, 0x6f, 0x61, 0xa4, 0x26, 0xfb, 0x74, - 0x2c, 0xa5, 0x90, 0x1d, 0xc0, 0xc6, 0x8e, 0xb6, 0x73, 0xc1, 0x9c, 0xda, 0xba, 0x04, 0x3b, 0xd9, 0xa9, 0xb9, 0x5b, - 0x91, 0x01, 0x91, 0x07, 0x42, 0x14, 0x06, 0x7c, 0x7f, 0x5e, 0x11, 0xc0, 0x9a, 0xe3, 0x14, 0x89, 0x3f, 0x08, 0xe3, - 0x97, 0x1f, 0x14, 0x83, 0x84, 0xe5, 0xae, 0xe7, 0x70, 0xfa, 0x3a, 0x80, 0x56, 0xea, 0xc5, 0xe6, 0x7b, 0x26, 0xca, - 0x46, 0xfe, 0x2a, 0xd6, 0x3a, 0x62, 0x88, 0x70, 0xe0, 0xcb, 0x66, 0x43, 0xd2, 0x78, 0xb3, 0x5c, 0x9c, 0x8d, 0x9a, - 0xae, 0xab, 0x23, 0xee, 0x23, 0x15, 0x84, 0xb1, 0xd9, 0xf0, 0xd0, 0x8d, 0xf3, 0x43, 0xd6, 0x6e, 0x06, 0x87, 0x41, - 0x04, 0x7e, 0x6d, 0xba, 0x56, 0x97, 0x70, 0xb5, 0xa6, 0x99, 0x78, 0x2f, 0xce, 0xa6, 0xfb, 0xba, 0xd7, 0x35, 0xc2, - 0xbf, 0x5c, 0xe2, 0x80, 0xf9, 0x37, 0x52, 0xc5, 0x41, 0x8b, 0x39, 0x7a, 0x8d, 0x4b, 0x9a, 0xe9, 0xa9, 0x21, 0x77, - 0x57, 0xca, 0x7b, 0x28, 0x07, 0xaa, 0x63, 0x3c, 0x3d, 0x64, 0x37, 0x87, 0x5b, 0x80, 0xda, 0x8e, 0x10, 0x57, 0x06, - 0xea, 0x09, 0x80, 0x2b, 0x09, 0x84, 0x65, 0x1e, 0xcf, 0x90, 0xbe, 0x67, 0xd2, 0x09, 0x68, 0xe8, 0x40, 0xb9, 0xe9, - 0x49, 0x99, 0x43, 0xea, 0xa1, 0x0e, 0x52, 0x4c, 0x78, 0xd0, 0xcb, 0xae, 0x96, 0xea, 0x3a, 0x1a, 0x21, 0x69, 0x42, - 0x41, 0xfc, 0x82, 0xa0, 0xe8, 0xab, 0x21, 0xf2, 0x97, 0x89, 0xca, 0xba, 0xaa, 0xf0, 0x06, 0xff, 0x12, 0x2d, 0xb2, - 0xfa, 0xce, 0xcc, 0xec, 0x48, 0x5d, 0x56, 0xa2, 0xf6, 0x02, 0xb0, 0x0e, 0x87, 0xe0, 0x40, 0x22, 0x62, 0x9e, 0x44, - 0x13, 0xd9, 0x54, 0x28, 0x7f, 0xe6, 0xd0, 0x28, 0x80, 0xcb, 0x79, 0x24, 0x68, 0x22, 0xf0, 0xb1, 0x13, 0xe0, 0xcc, - 0x0c, 0x3c, 0x9c, 0xad, 0x26, 0x8d, 0xc0, 0x98, 0x6b, 0xe5, 0xa5, 0x66, 0x1f, 0x33, 0xa2, 0x1c, 0x17, 0x73, 0x23, - 0xbb, 0x6b, 0xf2, 0x70, 0x88, 0x79, 0x62, 0x63, 0x0e, 0xdf, 0xd7, 0x9e, 0x19, 0xd3, 0xbf, 0xcc, 0xc0, 0x27, 0x25, - 0xea, 0x4e, 0x0d, 0x8a, 0xd7, 0xed, 0x9d, 0xd7, 0x56, 0xbb, 0x86, 0x5c, 0x16, 0x1d, 0x06, 0xab, 0xb5, 0xff, 0xd7, - 0x7f, 0x8a, 0xe3, 0xbe, 0x72, 0x3e, 0x06, 0x57, 0x3c, 0x04, 0x87, 0x35, 0x43, 0xcd, 0xaf, 0xeb, 0xe2, 0x39, 0x3e, - 0x6d, 0x1f, 0xe6, 0xc6, 0xd3, 0xdd, 0x81, 0x97, 0xb9, 0x90, 0xfa, 0xcc, 0x12, 0xa2, 0x0f, 0x43, 0x8b, 0x67, 0x63, - 0x54, 0x89, 0xc6, 0x97, 0x0e, 0x29, 0x96, 0x2d, 0x9e, 0xee, 0x04, 0xe2, 0xe5, 0x70, 0x77, 0xb6, 0x40, 0xac, 0x28, - 0x11, 0xe6, 0x74, 0x22, 0xd2, 0x38, 0x02, 0xc6, 0x2b, 0xf1, 0xc0, 0x10, 0x18, 0x69, 0x94, 0x59, 0xd3, 0xfe, 0xb0, - 0x11, 0xd9, 0xe7, 0x90, 0x68, 0x32, 0x6c, 0xca, 0x3b, 0x9b, 0x51, 0x7b, 0x25, 0x12, 0x8a, 0x86, 0x75, 0xdf, 0x4d, - 0x33, 0x2a, 0xef, 0xc5, 0x38, 0x24, 0x0e, 0xe1, 0xa4, 0x77, 0x3f, 0x6d, 0x1f, 0x4a, 0x1e, 0x7e, 0x0e, 0xfb, 0xc3, - 0x0f, 0xfe, 0xe1, 0xe7, 0x70, 0x77, 0x7e, 0xf0, 0x9d, 0x9f, 0x43, 0xde, 0xf9, 0x41, 0xbc, 0x54, 0x9a, 0xbe, 0xd2, - 0x9e, 0x07, 0x63, 0xc5, 0x50, 0x2e, 0xcb, 0xc8, 0x56, 0xaa, 0xe0, 0x17, 0x3f, 0x24, 0xdc, 0xe7, 0x12, 0x29, 0x39, - 0x25, 0x2e, 0x58, 0x89, 0x4a, 0x56, 0x86, 0x4e, 0x81, 0x7d, 0x1a, 0xd0, 0xc3, 0xea, 0xed, 0xe7, 0xfc, 0xcb, 0x6d, - 0x50, 0x74, 0x26, 0xe2, 0x01, 0x24, 0x43, 0xb9, 0x33, 0x07, 0x2f, 0x4c, 0x49, 0x18, 0x65, 0x39, 0x62, 0xb4, 0xa2, - 0xd2, 0x8e, 0xb3, 0x44, 0xef, 0xdc, 0x01, 0x16, 0x82, 0xbe, 0x5d, 0xe8, 0xb5, 0x72, 0x58, 0x7f, 0xff, 0x05, 0x28, - 0x55, 0x57, 0x0c, 0x78, 0x60, 0x1f, 0x7b, 0x71, 0x1f, 0x69, 0xe5, 0xd5, 0xa4, 0x8a, 0x1a, 0x5c, 0x93, 0x83, 0x31, - 0x46, 0x48, 0xdc, 0xd3, 0xbf, 0x64, 0x4d, 0x72, 0xe6, 0xe6, 0xad, 0x66, 0xe1, 0x1e, 0xa3, 0xe7, 0x80, 0xe6, 0xc4, - 0xa8, 0x9a, 0x19, 0xb6, 0x88, 0x5a, 0xb3, 0x9a, 0x33, 0x8b, 0x38, 0x59, 0x8a, 0xad, 0xab, 0xb0, 0xe7, 0x3d, 0x7e, - 0xca, 0xef, 0xe0, 0x2a, 0x37, 0x43, 0x1a, 0xec, 0x8b, 0x0c, 0xec, 0x83, 0x2b, 0x6c, 0x6b, 0x0d, 0xa6, 0x27, 0x9c, - 0xad, 0xc5, 0xf5, 0xd5, 0x14, 0xbe, 0x20, 0xad, 0xa1, 0x2d, 0x45, 0x34, 0xba, 0x4b, 0x26, 0x36, 0x52, 0xda, 0xfa, - 0xe1, 0x6b, 0x0b, 0x8d, 0x36, 0x2b, 0x96, 0x60, 0xc9, 0xee, 0x37, 0x2f, 0xb9, 0x0f, 0x4d, 0xe6, 0x2c, 0xc8, 0x44, - 0xd5, 0x4d, 0x90, 0x36, 0x05, 0xbe, 0x38, 0x59, 0x61, 0x3c, 0x02, 0x59, 0xe4, 0x36, 0x17, 0x87, 0x53, 0x47, 0x2d, - 0xa3, 0xaa, 0x84, 0x48, 0x7d, 0x56, 0xae, 0x93, 0x4b, 0xd0, 0xf1, 0xe2, 0x40, 0x04, 0x97, 0xc3, 0x84, 0x54, 0x6a, - 0x3a, 0x6d, 0xd7, 0x68, 0x6f, 0x21, 0xcf, 0xa1, 0x4e, 0x3f, 0x0d, 0x36, 0x84, 0x21, 0xaa, 0x31, 0xf8, 0x32, 0xf3, - 0xf4, 0x9a, 0x2e, 0x4d, 0xdb, 0xc7, 0x01, 0x04, 0x7a, 0xb1, 0x3d, 0x95, 0xce, 0x5d, 0x9f, 0x92, 0x48, 0x20, 0x91, - 0xf8, 0x02, 0xe0, 0x03, 0x80, 0xaf, 0x7a, 0x89, 0xaa, 0x45, 0x26, 0xbd, 0x54, 0x81, 0x9e, 0x29, 0xb8, 0x03, 0x32, - 0x43, 0x2b, 0x40, 0xe5, 0x8f, 0x48, 0xf1, 0xb5, 0x43, 0xb2, 0x98, 0xf0, 0xd2, 0x50, 0xbc, 0x8e, 0x09, 0xed, 0x7c, - 0x98, 0x9a, 0x5e, 0x22, 0x77, 0x81, 0x94, 0x8e, 0xd8, 0xa2, 0x9f, 0xbe, 0x3c, 0xbb, 0x69, 0xe1, 0x24, 0x8f, 0x2c, - 0xbf, 0xd6, 0xfe, 0x2d, 0x6b, 0xdb, 0x55, 0xf5, 0x47, 0xa6, 0xa4, 0x0e, 0xb4, 0x21, 0x94, 0xeb, 0x99, 0xb2, 0xa7, - 0xf4, 0x15, 0xec, 0x2c, 0x86, 0x45, 0xaf, 0x2d, 0xb3, 0xdd, 0x1c, 0x3e, 0x74, 0xd1, 0x03, 0xd1, 0x84, 0xdb, 0xd7, - 0x48, 0xa0, 0xb9, 0x44, 0xb0, 0x18, 0x9e, 0xe1, 0xd2, 0x6e, 0xfc, 0x92, 0x53, 0x14, 0xc4, 0x2a, 0xf0, 0x21, 0x7d, - 0xff, 0x01, 0x43, 0x86, 0xb2, 0xdd, 0x86, 0xc3, 0x55, 0x0d, 0x34, 0x5f, 0xf7, 0x71, 0xd8, 0xab, 0x13, 0xb0, 0xb6, - 0x64, 0xbe, 0xda, 0xb4, 0x51, 0xec, 0x35, 0x97, 0xa7, 0xbd, 0xb6, 0x52, 0xe0, 0xcf, 0xc5, 0x47, 0x7f, 0x7b, 0x5e, - 0xd4, 0x2c, 0xcb, 0x8b, 0xd2, 0x7b, 0x5b, 0xd5, 0xec, 0xb4, 0x06, 0xa3, 0x3f, 0x4e, 0x85, 0x90, 0x58, 0x0e, 0x93, - 0xd2, 0xf3, 0xd1, 0xa8, 0x16, 0xbb, 0xd7, 0x64, 0x1e, 0x1f, 0x26, 0xa1, 0x9a, 0x4d, 0x8d, 0x3c, 0xb8, 0xd7, 0x9b, - 0x0b, 0x7d, 0x8f, 0x02, 0xd5, 0xbd, 0x16, 0x4e, 0xd5, 0x55, 0x29, 0x41, 0x4c, 0x46, 0x46, 0x33, 0xcd, 0xc6, 0xbc, - 0x0c, 0xdc, 0x9a, 0xa9, 0x7e, 0x41, 0x9f, 0x48, 0xc9, 0x61, 0xd8, 0x59, 0x59, 0x94, 0x8a, 0x49, 0x4a, 0x00, 0x8b, - 0xed, 0x67, 0x71, 0x72, 0x60, 0x50, 0xb5, 0xea, 0x3c, 0x60, 0x24, 0x8e, 0xc5, 0xe2, 0x23, 0x50, 0xf1, 0x5b, 0x07, - 0xa8, 0x12, 0x4e, 0x8f, 0x55, 0x71, 0x1e, 0x7e, 0x10, 0xa5, 0x52, 0x4f, 0x40, 0xa0, 0xa6, 0x4e, 0x5e, 0xe6, 0x5e, - 0xb0, 0x7c, 0x33, 0xa7, 0x8d, 0xbd, 0x30, 0x2b, 0x1d, 0x90, 0x6b, 0xd3, 0x48, 0x0c, 0x45, 0xfc, 0x93, 0x63, 0xe3, - 0x36, 0xba, 0xb0, 0xea, 0x85, 0xe5, 0x5e, 0x54, 0x07, 0xa1, 0x41, 0xe8, 0x90, 0xa7, 0xca, 0x6d, 0x19, 0xd6, 0xe7, - 0x81, 0x97, 0x27, 0xfd, 0x0b, 0x4f, 0x0f, 0x36, 0x3d, 0xfc, 0x80, 0x45, 0x2b, 0x89, 0x34, 0x54, 0xb1, 0x49, 0xe1, - 0x8e, 0x48, 0x95, 0xe5, 0xce, 0xd3, 0x1e, 0xdd, 0x6b, 0x33, 0x0f, 0xd2, 0xd5, 0x47, 0x05, 0x45, 0x6b, 0x68, 0x09, - 0xa5, 0x2e, 0x60, 0x0a, 0xa3, 0x2c, 0xde, 0xe9, 0xab, 0xf5, 0x93, 0x5d, 0x4a, 0xc2, 0x01, 0x1f, 0xc3, 0x60, 0x26, - 0xf0, 0xef, 0x87, 0x48, 0x07, 0x37, 0xb5, 0x6e, 0x85, 0x32, 0x86, 0xb4, 0x42, 0x30, 0x1f, 0x49, 0x74, 0x98, 0xe0, - 0xfb, 0xc1, 0xa4, 0xc8, 0x49, 0xc1, 0x46, 0xe3, 0x37, 0xe3, 0x1a, 0x43, 0xc7, 0x99, 0xf1, 0x9d, 0x9f, 0xae, 0xd8, - 0xdb, 0x72, 0x5c, 0x1d, 0x42, 0xc0, 0xe5, 0x58, 0xee, 0x75, 0x5d, 0x90, 0x75, 0x8c, 0x76, 0x14, 0x3e, 0x23, 0x71, - 0xa9, 0x0b, 0x3d, 0xa5, 0xda, 0x91, 0x33, 0x86, 0x25, 0x38, 0x5d, 0xcd, 0x9f, 0xd8, 0xc6, 0x15, 0xb4, 0xed, 0xec, - 0x34, 0x50, 0xb7, 0x57, 0xc0, 0x83, 0x5d, 0x63, 0x4a, 0x94, 0x25, 0x56, 0x05, 0x34, 0x18, 0x01, 0x6d, 0x59, 0x60, - 0x54, 0x13, 0x31, 0xd1, 0x28, 0x8c, 0x12, 0xa9, 0xa5, 0x94, 0x1d, 0xcb, 0x1f, 0x75, 0x92, 0x4c, 0x92, 0x75, 0x28, - 0x4e, 0x7a, 0x62, 0x92, 0xd4, 0x6a, 0x5d, 0xb6, 0x78, 0x79, 0x21, 0xf6, 0x8b, 0x54, 0x7a, 0x62, 0xef, 0xa0, 0x05, - 0x72, 0xb3, 0xef, 0x69, 0x48, 0x0d, 0x8d, 0xce, 0xf6, 0x46, 0xe7, 0xe5, 0xa9, 0x6c, 0xbe, 0xd5, 0x51, 0xcb, 0xf8, - 0xc6, 0x98, 0xa2, 0x0a, 0xa8, 0x3f, 0xd6, 0x82, 0xf4, 0xfd, 0x4b, 0xb1, 0xce, 0x50, 0x34, 0x4c, 0x5d, 0xf6, 0x58, - 0x8c, 0x74, 0x9d, 0xe6, 0x89, 0x90, 0xe0, 0xde, 0x1d, 0x18, 0x78, 0x44, 0x99, 0x3b, 0x19, 0xd3, 0x09, 0xc2, 0x10, - 0x91, 0x75, 0xb2, 0xe6, 0x7d, 0x6e, 0xfd, 0x7c, 0x14, 0x87, 0x61, 0x0c, 0x1b, 0x4c, 0xae, 0xf6, 0x53, 0x7a, 0xef, - 0xc7, 0x62, 0xa4, 0x76, 0x9d, 0xd9, 0xcd, 0x78, 0x61, 0xa9, 0x3d, 0x16, 0xf6, 0x3f, 0x64, 0x3e, 0xf5, 0x58, 0xe9, - 0xbd, 0xb4, 0x86, 0x34, 0x9e, 0x59, 0x63, 0xd5, 0x5f, 0x82, 0x76, 0xe4, 0x12, 0xed, 0xc4, 0x4e, 0xa9, 0x2a, 0x48, - 0x28, 0x48, 0x8c, 0xa9, 0xed, 0x1c, 0x0c, 0x34, 0x63, 0x9d, 0xb9, 0x63, 0x8b, 0xbe, 0x3d, 0xe5, 0xa4, 0x1c, 0xa0, - 0xbc, 0x14, 0xfe, 0xd9, 0x76, 0x50, 0x62, 0x1f, 0xc7, 0x18, 0x5b, 0x81, 0x7d, 0x48, 0x20, 0x55, 0xc1, 0x84, 0x56, - 0x93, 0x07, 0x74, 0x71, 0x4a, 0xc7, 0x9f, 0x19, 0xe6, 0x4f, 0xb0, 0xfa, 0x9a, 0x27, 0xb6, 0xd9, 0x85, 0x63, 0x4c, - 0xa9, 0xd7, 0xd9, 0x11, 0xeb, 0xa7, 0x74, 0x61, 0x8b, 0xb5, 0x31, 0xa4, 0x6c, 0xc9, 0xd6, 0xb5, 0x45, 0xc8, 0x84, - 0x21, 0xeb, 0x3a, 0x52, 0x71, 0x03, 0xe7, 0x37, 0xe4, 0x02, 0x5e, 0xef, 0xe7, 0x5c, 0xa9, 0x67, 0x11, 0xcd, 0x32, - 0x41, 0xbb, 0x04, 0x72, 0xa4, 0xf3, 0xa2, 0xfe, 0xbf, 0x95, 0x10, 0xa2, 0x4b, 0x6b, 0xba, 0x2d, 0xa1, 0x4e, 0xf2, - 0xd9, 0x59, 0xb4, 0x80, 0xc7, 0x6e, 0x94, 0x1b, 0xe7, 0xb1, 0xb4, 0x09, 0x9e, 0x0d, 0x22, 0x81, 0x0d, 0xcb, 0x29, - 0x51, 0x0d, 0xab, 0xad, 0xee, 0x7a, 0x18, 0x9f, 0xdd, 0xde, 0x28, 0xc4, 0x50, 0x61, 0xf6, 0x77, 0xa0, 0xa4, 0xe2, - 0x5e, 0x97, 0xd4, 0x3a, 0x2a, 0xff, 0x1b, 0xa5, 0x0d, 0x41, 0xe1, 0x8b, 0x9b, 0x82, 0x1d, 0xdc, 0xeb, 0x9e, 0x1a, - 0x8a, 0xfd, 0xfd, 0x42, 0x85, 0x69, 0xa7, 0x0f, 0xca, 0x04, 0x4d, 0x78, 0x0b, 0x72, 0x39, 0xf2, 0xfd, 0x6c, 0x2a, - 0xbf, 0xc8, 0x2f, 0x7d, 0x7b, 0x6d, 0x08, 0x5b, 0xd1, 0x4a, 0x2b, 0x56, 0x47, 0xf9, 0x61, 0x78, 0x13, 0xb7, 0x45, - 0x06, 0x45, 0x7d, 0x5e, 0x63, 0xef, 0x90, 0xaa, 0xc4, 0x6e, 0x7b, 0xe2, 0x06, 0x61, 0x39, 0xe9, 0x12, 0x5c, 0x58, - 0x23, 0x11, 0xa3, 0xd4, 0x9c, 0xe1, 0x54, 0x8b, 0xda, 0xc2, 0x72, 0xae, 0xd5, 0x11, 0x15, 0x10, 0xaa, 0xef, 0xa9, - 0x52, 0xd6, 0xc0, 0xb0, 0x77, 0x9e, 0x86, 0xc1, 0xcb, 0xb1, 0xab, 0x6b, 0xe5, 0xe8, 0x34, 0x5d, 0xf7, 0xb4, 0x40, - 0x81, 0x36, 0xa5, 0xb7, 0x76, 0x59, 0x8e, 0xb7, 0xea, 0x02, 0x17, 0x43, 0x0b, 0x9e, 0x3b, 0xef, 0x00, 0xbe, 0x4a, - 0x1e, 0x29, 0x3c, 0x58, 0xba, 0x76, 0x05, 0xb4, 0x30, 0x99, 0x04, 0x1e, 0x9c, 0xc5, 0x5a, 0x25, 0x6b, 0x51, 0xe1, - 0x35, 0x21, 0x0c, 0xc8, 0x59, 0x1f, 0x6c, 0xbb, 0x31, 0x72, 0x89, 0xda, 0xeb, 0x47, 0x1a, 0x5a, 0x64, 0xfd, 0xa0, - 0x49, 0xcf, 0x03, 0x45, 0xe5, 0xa8, 0x7a, 0x77, 0xa7, 0x8c, 0xbe, 0xc4, 0x3c, 0x61, 0xd4, 0x27, 0x06, 0x8d, 0xf4, - 0x85, 0x3a, 0x22, 0xe4, 0xfc, 0xc4, 0x66, 0xcd, 0x57, 0xfb, 0xf0, 0x9e, 0x10, 0xc6, 0x6a, 0xd3, 0x91, 0xcf, 0x13, - 0x68, 0xcf, 0x96, 0xae, 0x5f, 0xd4, 0x90, 0xe1, 0xb5, 0xe9, 0x72, 0x48, 0xc6, 0x82, 0xa7, 0x66, 0x08, 0x83, 0x5a, - 0xc9, 0x38, 0x4d, 0xec, 0x73, 0x16, 0x26, 0xd2, 0x55, 0xb9, 0x86, 0x00, 0xd7, 0x2f, 0x9c, 0x49, 0xb3, 0xd8, 0x72, - 0x8b, 0x92, 0xd1, 0xa5, 0x26, 0xc4, 0x16, 0x4d, 0x44, 0x06, 0x00, 0xbd, 0x1c, 0xf6, 0x11, 0x90, 0xf0, 0x6d, 0xc2, - 0xb9, 0x79, 0x62, 0x4b, 0x1b, 0xd7, 0x5c, 0x50, 0x18, 0xee, 0xe8, 0xc9, 0x5e, 0x6c, 0x2a, 0x62, 0xcf, 0x60, 0x1e, - 0x9a, 0x8d, 0x65, 0x36, 0x7f, 0xe4, 0xa7, 0xe3, 0x50, 0x0c, 0xa4, 0xff, 0xc0, 0x82, 0xf8, 0x9f, 0xa1, 0x42, 0x5c, - 0x71, 0x41, 0xfe, 0x80, 0x2b, 0x69, 0xf8, 0x82, 0x74, 0x3b, 0x9d, 0xf9, 0xd9, 0xf4, 0xa9, 0x5a, 0x40, 0x50, 0x1e, - 0x08, 0x85, 0x34, 0x17, 0x90, 0xc6, 0x0b, 0x1c, 0x58, 0x2f, 0xec, 0x90, 0x04, 0xb6, 0x9e, 0x8e, 0x64, 0xd2, 0x48, - 0xa7, 0x78, 0xe0, 0x53, 0xbd, 0xb6, 0x3f, 0xd5, 0x31, 0xa5, 0x37, 0xe5, 0x69, 0xd3, 0x3c, 0x15, 0x0f, 0x3d, 0x6b, - 0xab, 0x08, 0x13, 0x06, 0x4f, 0x85, 0x13, 0x5e, 0xef, 0xe9, 0x5a, 0xbb, 0x86, 0xaf, 0xe0, 0x8b, 0x9e, 0x0d, 0xe6, - 0xc2, 0xe6, 0x5a, 0x24, 0xe8, 0x20, 0x4c, 0x17, 0x3e, 0x3e, 0xc2, 0xc8, 0x74, 0x29, 0xbd, 0xa2, 0x1f, 0x0d, 0x0a, - 0xc5, 0xdb, 0xf5, 0x87, 0xf6, 0x2e, 0x82, 0x83, 0xb3, 0x05, 0xd9, 0x98, 0x76, 0x07, 0x20, 0x0f, 0x69, 0x51, 0xd5, - 0x18, 0x23, 0xa4, 0x42, 0x1c, 0x43, 0xc4, 0xe9, 0xf6, 0x55, 0x5b, 0x1e, 0xba, 0xe5, 0x97, 0x3c, 0x23, 0xff, 0x5e, - 0xfc, 0x99, 0xf9, 0xae, 0x6f, 0xd0, 0x15, 0xd7, 0x79, 0x0e, 0xf1, 0xbd, 0xdf, 0xb5, 0x46, 0x42, 0x94, 0x84, 0x7f, - 0x0c, 0x1e, 0x20, 0x66, 0x3c, 0x58, 0x03, 0xf6, 0xbc, 0xba, 0x91, 0x93, 0xe0, 0xbe, 0x60, 0xe8, 0x6d, 0xf3, 0xb5, - 0x7e, 0x3c, 0x26, 0xf1, 0x16, 0x6d, 0x11, 0xbb, 0x52, 0x07, 0x33, 0x76, 0xe2, 0x9c, 0x0f, 0x93, 0xd9, 0x7f, 0x8c, - 0xb0, 0xc0, 0x11, 0x0a, 0x6a, 0x2d, 0xfc, 0xb2, 0x15, 0xc0, 0xad, 0xfe, 0x83, 0x91, 0x02, 0x37, 0xd1, 0x13, 0x3f, - 0xdb, 0x3d, 0xc5, 0x26, 0x38, 0x11, 0x7b, 0x45, 0x6c, 0xcf, 0x81, 0x5a, 0xad, 0x6a, 0x0a, 0xd5, 0xad, 0xd3, 0x41, - 0xe8, 0x62, 0x51, 0x98, 0xeb, 0x75, 0x14, 0xf8, 0xac, 0x5a, 0x56, 0x1d, 0x86, 0x6c, 0x57, 0xa1, 0xf6, 0x24, 0x1b, - 0x16, 0x25, 0x2a, 0x72, 0xe3, 0x78, 0x53, 0xac, 0x03, 0xea, 0xb7, 0x7a, 0x6d, 0x82, 0x5b, 0x2f, 0x78, 0x74, 0x2c, - 0xc8, 0xb5, 0x14, 0x31, 0x78, 0x82, 0xc8, 0xe0, 0x55, 0xb9, 0x40, 0x07, 0xbd, 0x74, 0x5f, 0x37, 0x1f, 0x5a, 0xe3, - 0xe9, 0x6e, 0x1a, 0x3e, 0xfb, 0xb9, 0xf7, 0xd6, 0x88, 0xed, 0x9a, 0x31, 0x32, 0x2e, 0x92, 0x16, 0x3d, 0x75, 0x8d, - 0xcb, 0x35, 0x98, 0x3d, 0xb4, 0x3a, 0x66, 0x98, 0xbf, 0x5c, 0x69, 0x31, 0xc6, 0xef, 0x44, 0x31, 0xed, 0x41, 0x37, - 0x2b, 0xc4, 0x3d, 0xbd, 0x60, 0xc0, 0x5a, 0x4b, 0xbc, 0x69, 0xf5, 0x56, 0x5b, 0x9f, 0x2d, 0xcb, 0x20, 0xfa, 0x46, - 0x53, 0xbe, 0x9b, 0x85, 0x2c, 0x97, 0x29, 0xd6, 0x68, 0x13, 0xf6, 0xe5, 0x72, 0x6f, 0x37, 0xb6, 0x95, 0xf1, 0x6f, - 0x51, 0xf5, 0x64, 0x48, 0x24, 0x2d, 0x51, 0x2a, 0x15, 0x38, 0xe9, 0xc2, 0x10, 0x6b, 0x3a, 0x6a, 0xb9, 0x4e, 0x82, - 0xf9, 0xbe, 0x3b, 0x75, 0x58, 0xfe, 0xf8, 0x9c, 0x17, 0x69, 0xe5, 0x93, 0x22, 0xf6, 0xd9, 0xe1, 0x62, 0x42, 0x39, - 0x85, 0x33, 0xb2, 0xfb, 0x6f, 0x78, 0xb5, 0x2b, 0x80, 0x9a, 0x60, 0xf4, 0x72, 0xc9, 0xd5, 0x50, 0x94, 0x7e, 0x3a, - 0x19, 0xa6, 0x20, 0xac, 0xaf, 0xd6, 0xc2, 0x6b, 0xaf, 0x48, 0x74, 0x89, 0xbf, 0x92, 0x5e, 0x1a, 0x82, 0xa4, 0xed, - 0x50, 0x5f, 0xd5, 0x25, 0x08, 0x74, 0x88, 0x57, 0x12, 0xe0, 0x66, 0xde, 0x82, 0x26, 0x13, 0x19, 0x17, 0x6f, 0x5c, - 0x00, 0x17, 0xc6, 0xdb, 0xa7, 0x1b, 0x48, 0xd6, 0x5a, 0x62, 0x27, 0xa1, 0x9b, 0x5e, 0x1a, 0x9c, 0x00, 0x09, 0x76, - 0x3c, 0x81, 0x26, 0xef, 0x84, 0xcf, 0x5c, 0xaf, 0x26, 0xa6, 0x20, 0x88, 0xe8, 0xde, 0x73, 0xb0, 0x9b, 0xeb, 0x59, - 0x56, 0xd8, 0x84, 0xd8, 0xec, 0xa8, 0xfa, 0x7e, 0xaa, 0xc0, 0xeb, 0xa5, 0x49, 0xc5, 0x46, 0xa1, 0xeb, 0xe4, 0x0e, - 0xc7, 0x01, 0xa6, 0xb3, 0xe4, 0x50, 0xc3, 0x95, 0x8f, 0x65, 0x39, 0x49, 0x09, 0x2d, 0x85, 0x03, 0xce, 0x40, 0x72, - 0xf0, 0x3f, 0x96, 0x74, 0x90, 0x75, 0xf8, 0x89, 0x69, 0x0b, 0xfe, 0x4c, 0x5a, 0xd3, 0xb4, 0x88, 0x56, 0x7b, 0x1d, - 0x6b, 0xd0, 0xbc, 0x4a, 0x9e, 0x4f, 0x0c, 0x60, 0xb3, 0x5a, 0xc8, 0xea, 0xc7, 0x5e, 0x5b, 0xfe, 0x48, 0xf9, 0x29, - 0x0b, 0xb5, 0xa7, 0x7a, 0x6c, 0x85, 0x64, 0xa7, 0x69, 0x51, 0x11, 0xc5, 0xf5, 0x64, 0xbb, 0x21, 0x7e, 0xf8, 0x22, - 0x11, 0x94, 0x4b, 0x05, 0xc4, 0x90, 0x00, 0x04, 0x83, 0x19, 0xd4, 0x90, 0xd0, 0x51, 0x5f, 0x6f, 0x9e, 0x8e, 0x7b, - 0x08, 0x34, 0x4f, 0x85, 0x02, 0x62, 0xba, 0x62, 0x76, 0xbe, 0x0b, 0xa8, 0xe2, 0xfd, 0x1b, 0x6c, 0x9b, 0x56, 0xdf, - 0xd6, 0xb4, 0xca, 0x4f, 0xd6, 0x7f, 0xd4, 0xb9, 0x29, 0xb0, 0x21, 0x36, 0xa8, 0x52, 0x24, 0xac, 0x32, 0x06, 0x88, - 0x46, 0xcf, 0xdc, 0x64, 0x9a, 0xc2, 0xfe, 0xee, 0x3c, 0x5d, 0xf5, 0x75, 0x6a, 0xf3, 0x5d, 0xcf, 0xa5, 0xc4, 0x12, - 0x2e, 0xb3, 0xd0, 0xc7, 0x72, 0x00, 0x64, 0xa6, 0x87, 0xa5, 0x83, 0x06, 0x5f, 0x83, 0x57, 0x57, 0x2c, 0x55, 0xd7, - 0xec, 0x7e, 0xc8, 0xf8, 0xeb, 0x9b, 0xf4, 0x8a, 0xde, 0xc9, 0xc8, 0x7c, 0x73, 0xaf, 0x77, 0xd7, 0xea, 0xfa, 0x85, - 0xf5, 0x8c, 0xba, 0x54, 0x2d, 0x4f, 0x7f, 0x6f, 0xf7, 0x7d, 0x71, 0x67, 0xed, 0x4f, 0x41, 0x19, 0xdb, 0x93, 0x7c, - 0xa0, 0x9a, 0x1b, 0xff, 0x02, 0xcd, 0x9b, 0x82, 0x5a, 0x46, 0xa6, 0xbc, 0xad, 0xfd, 0x92, 0x1b, 0xf2, 0xf6, 0x44, - 0xc6, 0x11, 0xe7, 0x8e, 0x21, 0xef, 0x4b, 0xdb, 0xf8, 0xdc, 0xeb, 0x08, 0x14, 0x7e, 0x79, 0x3a, 0xa5, 0x80, 0xd6, - 0x84, 0x4b, 0xc4, 0x11, 0x5a, 0x5e, 0x97, 0x2e, 0x8a, 0x41, 0xe4, 0xe8, 0x03, 0xd8, 0xd2, 0x86, 0xe0, 0xd3, 0x22, - 0xfc, 0x6c, 0x26, 0xd4, 0x93, 0xad, 0x40, 0xad, 0x88, 0x2a, 0x7b, 0x48, 0x56, 0x02, 0xcb, 0x89, 0xe4, 0xa4, 0x27, - 0x75, 0x26, 0x90, 0x60, 0xea, 0x15, 0xef, 0xbb, 0x60, 0xc8, 0x62, 0x97, 0x2b, 0x0c, 0x2c, 0xe2, 0x64, 0xa1, 0x7e, - 0xbd, 0x3c, 0x95, 0x46, 0x0b, 0x0c, 0x01, 0x4c, 0x73, 0x2f, 0x2f, 0x1b, 0x23, 0x9e, 0xfe, 0xee, 0x86, 0x4c, 0x17, - 0x78, 0xf0, 0xcd, 0x8b, 0x9b, 0xd4, 0x52, 0x80, 0x9e, 0x9b, 0xfc, 0x6e, 0xa4, 0x9d, 0xc8, 0x09, 0xa9, 0xcd, 0x19, - 0x0e, 0x01, 0xaa, 0x9a, 0x3d, 0xc4, 0x5c, 0x2a, 0x65, 0x27, 0xae, 0x81, 0x2c, 0xbf, 0x89, 0xc0, 0x97, 0x6f, 0xe7, - 0xd8, 0x3b, 0x15, 0x95, 0xad, 0xd0, 0x0e, 0xa1, 0xa2, 0x36, 0xac, 0xee, 0xe6, 0xe1, 0x31, 0x47, 0xb0, 0xf3, 0x87, - 0x79, 0xdc, 0xd7, 0x0d, 0x8f, 0x10, 0x60, 0x05, 0xc2, 0x27, 0x04, 0x1f, 0x60, 0x88, 0x66, 0xba, 0xb5, 0xef, 0xef, - 0x55, 0x52, 0x55, 0x3c, 0x05, 0x38, 0x3e, 0xc0, 0xf0, 0xce, 0xd4, 0x63, 0xb3, 0x04, 0x9b, 0x79, 0x04, 0x86, 0x90, - 0x9b, 0xe6, 0x54, 0x53, 0x6e, 0x80, 0xf9, 0x2e, 0x62, 0x98, 0xe2, 0x91, 0xee, 0xd1, 0xf0, 0x01, 0xed, 0xc6, 0x9b, - 0x3b, 0x2f, 0xf0, 0xd3, 0x2c, 0x62, 0xd9, 0xf3, 0x64, 0x94, 0xc1, 0x27, 0x22, 0xdf, 0x22, 0x85, 0xcc, 0xfd, 0xc4, - 0x29, 0xac, 0xb6, 0x69, 0x7d, 0x51, 0x88, 0xdc, 0x5c, 0xdd, 0x98, 0x68, 0x0d, 0x5c, 0xa8, 0x4d, 0x54, 0x27, 0xd0, - 0xda, 0x66, 0x7b, 0xb8, 0xea, 0x4c, 0x24, 0x83, 0x27, 0xc2, 0xfc, 0x1b, 0xaf, 0xee, 0x64, 0xeb, 0x90, 0x8b, 0xd3, - 0xa3, 0x30, 0x57, 0x7b, 0x6b, 0xcf, 0x5b, 0xf7, 0x2d, 0x77, 0xd5, 0x9a, 0x3c, 0xa7, 0x45, 0x28, 0xb1, 0x93, 0x0c, - 0xa0, 0x08, 0xee, 0x9b, 0x41, 0xef, 0x3d, 0xd4, 0x89, 0x0c, 0x2e, 0x54, 0x31, 0xe3, 0xcc, 0x38, 0xca, 0xf3, 0x2b, - 0xae, 0x39, 0xb8, 0xfd, 0xbc, 0x71, 0x31, 0x10, 0xa0, 0xd0, 0x01, 0x99, 0xfa, 0x51, 0x99, 0xda, 0x9a, 0x26, 0xc7, - 0x7c, 0x05, 0x0b, 0x44, 0x86, 0x20, 0x00, 0x59, 0x78, 0xda, 0x56, 0xe9, 0x3e, 0x9e, 0x0c, 0x07, 0xca, 0x1b, 0x81, - 0x19, 0x19, 0x74, 0x10, 0xcd, 0x58, 0xdb, 0x99, 0x44, 0x44, 0x98, 0x84, 0x1b, 0x8b, 0x1a, 0xfe, 0xc5, 0x53, 0x52, - 0x3e, 0xe6, 0xa1, 0x87, 0x11, 0xd3, 0x62, 0x5e, 0x51, 0x7c, 0x49, 0x41, 0x3a, 0x97, 0x56, 0xdf, 0xb2, 0x4c, 0xce, - 0xa9, 0x97, 0xa1, 0xd0, 0x45, 0xc2, 0xa8, 0xb0, 0x49, 0x3d, 0x91, 0x01, 0x24, 0x63, 0x95, 0x19, 0xca, 0x15, 0x5e, - 0x8f, 0x2a, 0x79, 0x5c, 0xf2, 0x6f, 0xcc, 0xca, 0xb8, 0x1c, 0x5b, 0xd6, 0x0d, 0xeb, 0x1c, 0x1c, 0xaf, 0x54, 0xcb, - 0xe4, 0x9b, 0xa2, 0x38, 0xf1, 0xe2, 0x23, 0x06, 0xe2, 0xfd, 0xac, 0xde, 0x66, 0x9e, 0x7d, 0x58, 0xee, 0xda, 0xc2, - 0x95, 0x49, 0xc5, 0x20, 0x96, 0x30, 0x11, 0xb4, 0x28, 0x8d, 0xdf, 0x71, 0x30, 0xc5, 0x29, 0x40, 0x1b, 0x0b, 0xdf, - 0x1b, 0x49, 0x55, 0xe5, 0xb0, 0x5c, 0x46, 0x6f, 0xa5, 0xa8, 0xb3, 0x59, 0x5e, 0x46, 0x9b, 0x79, 0x12, 0x10, 0xe0, - 0xea, 0x4c, 0x59, 0xcd, 0x6e, 0x0e, 0x1d, 0x86, 0x33, 0xac, 0x2c, 0xe5, 0x84, 0x29, 0x9a, 0x35, 0x96, 0x12, 0x61, - 0xdc, 0x66, 0xb7, 0x2f, 0x8e, 0xdf, 0xd5, 0x72, 0x67, 0xfa, 0x0d, 0xdc, 0xe5, 0xae, 0x59, 0x40, 0x78, 0xe0, 0x11, - 0x9d, 0x93, 0xcb, 0x80, 0xaf, 0x8c, 0xea, 0x0d, 0x1a, 0xb0, 0x25, 0xeb, 0xa5, 0xf9, 0x58, 0x95, 0x87, 0xbe, 0x8a, - 0x5d, 0xbc, 0xd4, 0x25, 0xb4, 0x3a, 0xd4, 0xfa, 0xb0, 0xb7, 0xff, 0xb4, 0x57, 0xed, 0x34, 0xa0, 0x03, 0x62, 0x5f, - 0xeb, 0xf1, 0x65, 0x97, 0xff, 0xd5, 0x1f, 0xb7, 0x45, 0xa2, 0xed, 0x94, 0xba, 0x81, 0x0a, 0x41, 0xee, 0x40, 0xb0, - 0x95, 0xce, 0x67, 0xe5, 0x38, 0xe8, 0x85, 0x25, 0xa1, 0x16, 0x5e, 0x97, 0x97, 0x4a, 0xf0, 0x60, 0x4a, 0x49, 0xac, - 0x71, 0xaf, 0x37, 0x87, 0x01, 0x7d, 0xb8, 0xc5, 0x5a, 0x4d, 0x4c, 0x7f, 0x42, 0x54, 0x99, 0x48, 0x0f, 0x6c, 0x2f, - 0x9a, 0x98, 0xf0, 0xb0, 0x1f, 0x54, 0xa4, 0x84, 0xea, 0x40, 0xd0, 0x06, 0xca, 0xc4, 0x1c, 0x5f, 0x76, 0x28, 0x79, - 0x2e, 0xb4, 0xc0, 0x27, 0x06, 0xfb, 0x8e, 0xab, 0xb1, 0x50, 0xb1, 0x03, 0xc9, 0x31, 0x65, 0x0e, 0x37, 0xd8, 0x22, - 0xf6, 0x27, 0xd5, 0x40, 0xe9, 0xaf, 0xc6, 0x75, 0xdf, 0x56, 0x01, 0x94, 0xba, 0xe6, 0xc7, 0x7d, 0x8d, 0x42, 0x0f, - 0x16, 0xf1, 0x76, 0x08, 0xcf, 0x64, 0xbb, 0xa6, 0x22, 0xd6, 0x7c, 0x96, 0xec, 0xb9, 0x61, 0xc3, 0xdf, 0x57, 0x04, - 0x32, 0x46, 0x9a, 0x0e, 0x65, 0x6c, 0xc6, 0x2f, 0x65, 0x14, 0x53, 0x84, 0x7d, 0xe1, 0x77, 0x92, 0x10, 0x21, 0x42, - 0xc6, 0x30, 0xcd, 0x11, 0xb4, 0x33, 0x9f, 0x27, 0xb5, 0x40, 0x75, 0x4d, 0x42, 0xdf, 0xd3, 0xc3, 0x8a, 0x78, 0x90, - 0xa3, 0x47, 0x25, 0x00, 0xea, 0xbf, 0xc5, 0xbd, 0x27, 0x59, 0x31, 0x82, 0xb4, 0xe2, 0x44, 0x1a, 0x57, 0xe0, 0x38, - 0xc7, 0x27, 0x2d, 0x24, 0x88, 0x97, 0xea, 0x4e, 0x42, 0x5f, 0xb4, 0x71, 0x6a, 0xf0, 0x02, 0xb9, 0x28, 0x56, 0x2a, - 0x00, 0xb5, 0x5b, 0xf0, 0x66, 0x09, 0x33, 0x66, 0x48, 0x8f, 0xbc, 0x07, 0x6b, 0x1e, 0xf2, 0x52, 0x2e, 0x8f, 0x39, - 0x39, 0x87, 0xa8, 0xb9, 0x28, 0x92, 0x1a, 0x73, 0x05, 0x7d, 0x0d, 0x8a, 0x53, 0xe8, 0x63, 0x4c, 0xac, 0x36, 0x4f, - 0x7d, 0xaa, 0x86, 0xa2, 0xf4, 0x6c, 0x56, 0x17, 0xeb, 0x88, 0x2d, 0xb0, 0x0b, 0xcd, 0x18, 0x82, 0x5f, 0xc9, 0x24, - 0x87, 0x83, 0xb4, 0x4c, 0x04, 0x1d, 0x95, 0x17, 0x43, 0x27, 0x33, 0xda, 0xbb, 0xf4, 0x84, 0x3b, 0x7a, 0x28, 0x39, - 0x7d, 0x81, 0xd2, 0x43, 0x08, 0xd0, 0x5f, 0x8d, 0x68, 0xdc, 0xfe, 0x0a, 0x27, 0xc5, 0x8b, 0x09, 0x1f, 0x24, 0x51, - 0x84, 0x87, 0x70, 0x46, 0x14, 0x32, 0x12, 0xed, 0x43, 0xc1, 0xcc, 0x3b, 0xdb, 0xd6, 0x94, 0xf7, 0x45, 0x9d, 0x3a, - 0xcd, 0xc1, 0xcb, 0xf7, 0xe2, 0xb5, 0x5c, 0x4e, 0x3d, 0x7a, 0xec, 0xcb, 0x96, 0x90, 0x9d, 0x07, 0x00, 0x02, 0xe4, - 0x8b, 0x1d, 0x32, 0x26, 0x68, 0xc3, 0x9a, 0x96, 0x64, 0x4d, 0x3f, 0x5a, 0x84, 0x7e, 0x54, 0x7d, 0x9c, 0x66, 0x99, - 0x90, 0x6a, 0x0b, 0x63, 0x40, 0x84, 0x9e, 0x2a, 0x94, 0x60, 0x45, 0xee, 0x83, 0x97, 0xb8, 0x9a, 0x00, 0xdb, 0xb6, - 0x18, 0x9e, 0xb4, 0x37, 0x43, 0x60, 0x3b, 0x22, 0xa0, 0xd3, 0x0c, 0x89, 0x42, 0x6c, 0xb8, 0x8f, 0xd1, 0x4c, 0x52, - 0xc1, 0x98, 0x26, 0x2a, 0x1f, 0xfc, 0x07, 0xb5, 0x11, 0x37, 0x69, 0xaf, 0xe2, 0x79, 0x84, 0x3d, 0xc7, 0xa1, 0xeb, - 0xc2, 0x65, 0x40, 0x54, 0xd9, 0x72, 0x59, 0x73, 0x3d, 0x5a, 0x9e, 0x91, 0x41, 0x95, 0x48, 0xfd, 0x85, 0x5b, 0x07, - 0x95, 0x06, 0xd4, 0xb3, 0xf8, 0x64, 0xe0, 0xb9, 0x25, 0xb4, 0xdc, 0x9f, 0x23, 0x89, 0x07, 0xe0, 0xd4, 0xa3, 0x39, - 0xc2, 0x4b, 0x77, 0x87, 0x00, 0xf7, 0x56, 0x75, 0xbb, 0x69, 0x09, 0x28, 0x63, 0x27, 0xe1, 0xaa, 0xad, 0x52, 0x92, - 0x5a, 0x83, 0x12, 0xf3, 0xef, 0xf2, 0x4b, 0x3d, 0x76, 0x15, 0x1b, 0x96, 0x21, 0xd0, 0xb5, 0x42, 0xfd, 0xe5, 0x13, - 0xda, 0x49, 0xe1, 0xc6, 0xe1, 0x0d, 0xb2, 0x68, 0xf3, 0x11, 0xb5, 0x60, 0x2e, 0x50, 0x77, 0x5c, 0xd4, 0xbd, 0xf9, - 0x1b, 0xc1, 0x4d, 0x51, 0x53, 0xe8, 0x42, 0xc9, 0x46, 0x8f, 0x37, 0x12, 0x33, 0x40, 0x73, 0xb9, 0xd2, 0x0a, 0xcf, - 0xaa, 0x07, 0x6a, 0xbf, 0x21, 0x71, 0x6b, 0xbd, 0xbe, 0x0d, 0x1b, 0x3d, 0x44, 0xab, 0xc9, 0x82, 0x36, 0x46, 0x92, - 0xc7, 0xcc, 0xa1, 0xb5, 0x22, 0xd3, 0x35, 0x49, 0xb0, 0x2c, 0xa9, 0xf5, 0x6a, 0xd7, 0xf0, 0xf3, 0xb7, 0x3e, 0x40, - 0x58, 0x30, 0xb0, 0x5a, 0x49, 0xef, 0xb0, 0xdd, 0xca, 0xa5, 0x85, 0xab, 0x4d, 0xfe, 0x2c, 0x95, 0x43, 0x40, 0x9b, - 0x2c, 0xbf, 0xc4, 0xa5, 0xa7, 0x28, 0x88, 0xd4, 0x69, 0xab, 0xab, 0x84, 0x84, 0x60, 0xa5, 0x52, 0x3f, 0x1d, 0x98, - 0x90, 0x23, 0x2a, 0x47, 0x64, 0xf7, 0xba, 0x9c, 0xf3, 0x53, 0x03, 0xd2, 0xdd, 0x88, 0x48, 0xc8, 0xe9, 0x8d, 0x01, - 0x5c, 0x16, 0x1a, 0xfb, 0xdb, 0x80, 0x2b, 0x7c, 0x88, 0xe0, 0xb4, 0xef, 0x4a, 0xb9, 0x2e, 0x82, 0xfb, 0xbe, 0x40, - 0x8a, 0xaa, 0x22, 0x82, 0x05, 0xd5, 0x8e, 0x6c, 0xce, 0x8e, 0xfc, 0xc6, 0x8c, 0x02, 0xe7, 0xe6, 0x78, 0xd7, 0x28, - 0x42, 0xe9, 0x62, 0xe7, 0xbe, 0x62, 0x20, 0x4a, 0x12, 0x3e, 0x3b, 0x46, 0x68, 0xad, 0x75, 0x3e, 0xf1, 0x7e, 0xc0, - 0xb3, 0x24, 0x9c, 0x7f, 0x60, 0x93, 0xf7, 0xa5, 0x38, 0x2f, 0xaf, 0x36, 0x75, 0x5b, 0x30, 0x02, 0x50, 0x5f, 0x78, - 0xde, 0x56, 0x1e, 0xdc, 0x60, 0x64, 0x90, 0x27, 0x73, 0x81, 0xf1, 0xcc, 0xd5, 0x60, 0x9e, 0x1f, 0x3b, 0x2a, 0x04, - 0x2c, 0x04, 0xf2, 0x54, 0x53, 0x9b, 0xd6, 0x4a, 0x6c, 0xd1, 0x8e, 0xd9, 0x6f, 0xd9, 0x00, 0x27, 0xc0, 0xe9, 0x70, - 0xbc, 0xb4, 0x0d, 0xde, 0x90, 0x4b, 0x7a, 0x6b, 0x19, 0x05, 0xd9, 0x85, 0x7f, 0x1b, 0xb4, 0x06, 0xe5, 0x15, 0x08, - 0x15, 0x49, 0x1d, 0x1b, 0x25, 0xa5, 0x48, 0x1a, 0xa1, 0x65, 0xb6, 0x05, 0x59, 0x71, 0xb6, 0x47, 0x7c, 0xd5, 0xcc, - 0xe1, 0xa6, 0xc8, 0x6d, 0x91, 0xce, 0x1a, 0xee, 0x8b, 0x40, 0xc5, 0xa6, 0x90, 0x66, 0x5a, 0x23, 0xdb, 0xb8, 0x27, - 0xab, 0xb4, 0x77, 0x1b, 0x51, 0x33, 0x68, 0x44, 0xdf, 0xd2, 0x54, 0xf9, 0x7d, 0x2d, 0xaa, 0x97, 0x62, 0xa0, 0xcc, - 0x21, 0xa6, 0x6b, 0x5a, 0xc1, 0xa4, 0x4a, 0x2d, 0x8e, 0xf3, 0x36, 0x9f, 0x3e, 0x5c, 0x28, 0x87, 0xe4, 0xc0, 0x09, - 0x25, 0x47, 0x0c, 0xd9, 0x0a, 0x43, 0x70, 0x2b, 0x67, 0x13, 0xc9, 0x72, 0x23, 0x72, 0x99, 0x35, 0x46, 0x77, 0xfc, - 0x83, 0x05, 0xa0, 0xd0, 0x17, 0x1b, 0x14, 0xf4, 0x63, 0xad, 0xf5, 0x89, 0x3a, 0x52, 0x6a, 0x52, 0x7c, 0xba, 0x70, - 0x13, 0x95, 0x43, 0xcd, 0xd5, 0xab, 0xa2, 0x01, 0xb5, 0x26, 0x74, 0xc0, 0xf5, 0x08, 0x83, 0x0d, 0x84, 0xd1, 0x1f, - 0x4d, 0x21, 0x2c, 0xf7, 0x55, 0xdc, 0xb4, 0x9b, 0xbc, 0x7b, 0x3a, 0xdb, 0x63, 0xa4, 0x06, 0x15, 0x69, 0x59, 0x71, - 0x0c, 0xa7, 0x07, 0x9c, 0x83, 0xc7, 0x8e, 0x19, 0x36, 0x1b, 0xa7, 0xc7, 0x18, 0x03, 0x2c, 0x59, 0x61, 0xb1, 0x4d, - 0xa5, 0xb5, 0x22, 0x42, 0x6a, 0x9b, 0xd5, 0x4b, 0x9b, 0x3b, 0x45, 0x7e, 0xfb, 0x33, 0x00, 0xcc, 0xab, 0x26, 0xd3, - 0x3a, 0x8a, 0x29, 0x62, 0x94, 0xb4, 0x59, 0x1c, 0x2f, 0xc4, 0xca, 0x8b, 0x8f, 0x05, 0xee, 0x8f, 0x54, 0xb9, 0xb2, - 0xec, 0xb8, 0x3a, 0x93, 0xfb, 0xe1, 0xe6, 0x87, 0xcc, 0x49, 0xc4, 0x03, 0x16, 0xfa, 0x8c, 0xd9, 0x70, 0x75, 0xe2, - 0x1d, 0xa9, 0xc3, 0x2c, 0x26, 0xf7, 0xba, 0x78, 0xcb, 0xc7, 0xb9, 0x0b, 0xa8, 0xec, 0x41, 0xec, 0xb6, 0x2a, 0x63, - 0xbd, 0xce, 0xc8, 0x20, 0xe1, 0x5b, 0x2a, 0xf6, 0x4a, 0xc6, 0x4e, 0x7c, 0x06, 0x99, 0x1e, 0x2c, 0xc3, 0xc2, 0x53, - 0x46, 0x72, 0xfb, 0x4c, 0x15, 0xb5, 0xeb, 0x29, 0x95, 0xeb, 0xa2, 0x3b, 0xaf, 0xb9, 0xb7, 0x15, 0xee, 0xd4, 0xcc, - 0xa4, 0x13, 0xaf, 0x0b, 0x50, 0xe7, 0x83, 0x97, 0x16, 0xe9, 0x9c, 0x37, 0xb0, 0x6a, 0x85, 0xc2, 0x75, 0xa9, 0x46, - 0x9f, 0x5d, 0xee, 0xa3, 0x2d, 0x8e, 0x4d, 0x77, 0x7e, 0x5d, 0xf6, 0x68, 0xf2, 0x59, 0x87, 0x40, 0xec, 0x29, 0x22, - 0x3e, 0xa5, 0xc1, 0xad, 0x75, 0x98, 0x69, 0xab, 0xad, 0x0c, 0x54, 0x9b, 0xa4, 0x16, 0xf8, 0x49, 0x9b, 0xd2, 0xec, - 0x70, 0x6a, 0x79, 0xd7, 0x20, 0x96, 0xf8, 0x05, 0x0e, 0xab, 0x62, 0xf5, 0xec, 0xf1, 0x2d, 0xae, 0xac, 0x0c, 0x73, - 0xbf, 0x1e, 0x55, 0x0e, 0xb3, 0xb9, 0xe2, 0x78, 0x53, 0x1d, 0x91, 0x48, 0x6d, 0x3f, 0xf7, 0xf3, 0x27, 0x43, 0x45, - 0x8f, 0x83, 0x81, 0x38, 0x50, 0x55, 0xe4, 0x4c, 0x89, 0xb0, 0x0a, 0xa7, 0x25, 0x9a, 0x86, 0xc6, 0x3a, 0x14, 0x04, - 0x64, 0xd4, 0xff, 0x81, 0x70, 0x10, 0x99, 0xb7, 0x4e, 0x48, 0xaa, 0x2a, 0x35, 0x2c, 0xd1, 0x5e, 0xec, 0x7b, 0x48, - 0xe1, 0x21, 0x4f, 0xb6, 0x3e, 0x6f, 0xbf, 0xce, 0x91, 0x05, 0x0f, 0x04, 0xa3, 0x4c, 0x12, 0x03, 0x5b, 0x47, 0x97, - 0x7a, 0xd9, 0x8b, 0xbb, 0x4c, 0x40, 0x4f, 0x77, 0x1e, 0x7f, 0x84, 0x43, 0x51, 0xda, 0x9c, 0xbf, 0x6a, 0x49, 0x36, - 0xf3, 0xe8, 0xb6, 0x6a, 0xac, 0x43, 0x24, 0x36, 0x97, 0x1c, 0x2d, 0xe7, 0x45, 0x9e, 0x72, 0x74, 0xf9, 0x00, 0x8c, - 0x85, 0x77, 0xe7, 0x5c, 0x35, 0x17, 0x52, 0x4d, 0x5f, 0x1c, 0x13, 0xb8, 0x0e, 0x8f, 0xd8, 0x4a, 0xdb, 0x06, 0xeb, - 0xc1, 0x72, 0x88, 0xe7, 0xdc, 0x50, 0xae, 0x3f, 0xd4, 0x92, 0x6a, 0x52, 0xcf, 0x60, 0x1a, 0x2b, 0x75, 0x82, 0x26, - 0x65, 0xce, 0x2b, 0x9e, 0x3a, 0x98, 0x3a, 0x74, 0x93, 0x44, 0xf4, 0xd7, 0x91, 0x39, 0x91, 0xa4, 0x49, 0x3f, 0xb6, - 0x8d, 0x0a, 0x08, 0x80, 0x8e, 0x56, 0x08, 0x68, 0xf7, 0xbd, 0x5b, 0x7d, 0x26, 0xc9, 0x87, 0x67, 0x3d, 0x8a, 0xb9, - 0xd6, 0xd1, 0x56, 0xd7, 0xb0, 0x7c, 0x7b, 0x45, 0x18, 0xcd, 0xdb, 0x03, 0xb3, 0xc2, 0xd9, 0x88, 0x14, 0x63, 0xe7, - 0x2d, 0x20, 0x61, 0x1e, 0x22, 0xc7, 0xbb, 0x2e, 0x6a, 0xdc, 0x4a, 0xe7, 0xe8, 0xbc, 0x08, 0x4f, 0x9b, 0x2b, 0x16, - 0x4a, 0xd1, 0x4b, 0xe5, 0xd8, 0x6f, 0xde, 0x99, 0x51, 0x43, 0x5e, 0xf2, 0xb0, 0xf1, 0x7e, 0x94, 0xa7, 0xa7, 0x30, - 0x3a, 0x3f, 0xc4, 0x61, 0xee, 0x48, 0x5f, 0xa9, 0x03, 0xf4, 0x7a, 0x4f, 0x0e, 0xdf, 0xae, 0xef, 0x65, 0x27, 0x38, - 0x5c, 0x18, 0x2e, 0x8a, 0xf3, 0x05, 0xa9, 0x24, 0xe6, 0x28, 0xf5, 0x78, 0x51, 0x4f, 0xf1, 0x81, 0x78, 0xe5, 0x04, - 0xdb, 0x5e, 0xf6, 0xfd, 0xdf, 0x85, 0x33, 0x29, 0xbf, 0xef, 0x2e, 0x81, 0xaf, 0x07, 0x7f, 0xe8, 0xf6, 0x0b, 0x1c, - 0x89, 0xc8, 0x61, 0x1c, 0xee, 0xd8, 0x56, 0x71, 0xbd, 0x6f, 0xc3, 0x16, 0xa9, 0xd7, 0x1f, 0x27, 0x84, 0x5c, 0x37, - 0xe4, 0xa8, 0x3b, 0x28, 0xe2, 0x65, 0x09, 0x4c, 0xdc, 0x14, 0x42, 0x14, 0xe3, 0xbf, 0x5c, 0xcd, 0x53, 0x84, 0x5f, - 0x35, 0xa2, 0xb0, 0x55, 0x53, 0x53, 0x70, 0x57, 0x60, 0x00, 0x56, 0xf2, 0x04, 0x77, 0xa0, 0xe5, 0x43, 0x59, 0x78, - 0x85, 0x8e, 0xd5, 0xa2, 0xac, 0x04, 0x6a, 0x99, 0x21, 0x8f, 0x08, 0x4e, 0xd0, 0x5e, 0x84, 0x59, 0xd7, 0x30, 0x29, - 0xf7, 0x60, 0xf2, 0xb6, 0x6e, 0xe1, 0x75, 0xb7, 0xa9, 0xd3, 0xc3, 0xfb, 0x55, 0x69, 0xb1, 0xab, 0xb2, 0xc7, 0x03, - 0xe4, 0x28, 0x39, 0xbd, 0x03, 0x30, 0xe7, 0x61, 0x12, 0xd8, 0xea, 0xd2, 0x1c, 0xb6, 0x76, 0x97, 0xd0, 0x6f, 0x33, - 0x7c, 0xba, 0x43, 0x66, 0xa3, 0xa4, 0x9d, 0x7d, 0xfe, 0x53, 0x05, 0x8b, 0xa1, 0x37, 0x00, 0x9e, 0xb0, 0xee, 0x64, - 0xb5, 0xb0, 0x81, 0x7b, 0xfc, 0xd3, 0x87, 0xa6, 0x28, 0xa4, 0x25, 0xa6, 0xb1, 0x8b, 0xa3, 0x9a, 0x6c, 0xad, 0xf6, - 0x1a, 0x39, 0xbb, 0x21, 0x71, 0x55, 0x4a, 0x88, 0x2e, 0x47, 0xb4, 0x42, 0xb2, 0x47, 0x14, 0xc1, 0x6a, 0xef, 0x2c, - 0xdd, 0x46, 0x5f, 0xc3, 0x74, 0x05, 0x18, 0x4d, 0xc0, 0xb0, 0x41, 0xa5, 0xbd, 0x13, 0x00, 0x18, 0xa5, 0x55, 0x53, - 0xa7, 0xf4, 0x2e, 0x76, 0xd9, 0xe5, 0xe6, 0x41, 0xa6, 0xd4, 0x13, 0x35, 0x93, 0xdb, 0x03, 0x2a, 0x6b, 0x2d, 0x54, - 0xb2, 0x5f, 0x72, 0xc5, 0xa7, 0x51, 0x89, 0x56, 0xe8, 0x6a, 0x46, 0x07, 0xdd, 0x4c, 0xd1, 0x51, 0x22, 0xb6, 0x4c, - 0x4c, 0xbb, 0xf2, 0x66, 0x98, 0x78, 0xa9, 0xd8, 0x2a, 0x33, 0x22, 0x4d, 0xd9, 0xa2, 0x96, 0x23, 0xe2, 0xfc, 0xa8, - 0xbd, 0x66, 0x55, 0xa7, 0x36, 0xd6, 0x5a, 0x78, 0xba, 0x38, 0xc4, 0xe4, 0xea, 0x43, 0xb4, 0xdd, 0x07, 0x29, 0x38, - 0xd3, 0xa6, 0x8d, 0x2b, 0xb5, 0xcd, 0xbe, 0x88, 0x32, 0x5e, 0x91, 0x71, 0x11, 0xb3, 0xd9, 0xed, 0x93, 0xa5, 0x1d, - 0x26, 0xca, 0xe3, 0x98, 0x4c, 0x46, 0x0e, 0x54, 0xd2, 0x06, 0xaa, 0x25, 0xf7, 0x92, 0x15, 0x17, 0x71, 0xf1, 0xdf, - 0x68, 0xd9, 0xe6, 0xd9, 0xc2, 0xc0, 0x82, 0x16, 0x66, 0x89, 0x02, 0xb3, 0x54, 0x4a, 0x07, 0x25, 0x1c, 0x45, 0x64, - 0x27, 0x09, 0xd3, 0xcb, 0x92, 0x36, 0xf8, 0xa0, 0x91, 0xee, 0x4e, 0x26, 0x0d, 0x09, 0x97, 0x6b, 0x9c, 0xb5, 0x2d, - 0x26, 0x32, 0xe2, 0xa9, 0x6f, 0x4a, 0x26, 0xc2, 0x48, 0x3c, 0xc8, 0x94, 0x98, 0x0b, 0xcf, 0x06, 0x52, 0xe2, 0x8b, - 0x9c, 0x7e, 0xae, 0x17, 0xb3, 0xd1, 0x22, 0x8d, 0xfe, 0xa1, 0xe7, 0x97, 0xc5, 0xce, 0x6e, 0x47, 0x8c, 0x7a, 0x7b, - 0x5c, 0x79, 0x56, 0x53, 0x6b, 0xd7, 0x8c, 0x1c, 0x33, 0x06, 0x34, 0x52, 0x08, 0x14, 0xd2, 0x27, 0x23, 0x9c, 0x16, - 0x97, 0x03, 0x1b, 0x36, 0xbe, 0x53, 0x8e, 0x67, 0x0a, 0xb7, 0x17, 0x43, 0xc3, 0x73, 0x87, 0x44, 0x10, 0xa1, 0xf1, - 0x06, 0x67, 0xce, 0x50, 0xff, 0xe1, 0xe9, 0xbc, 0x35, 0xd7, 0xfd, 0x0f, 0x8a, 0x9e, 0xa5, 0x45, 0x44, 0x80, 0xf3, - 0x45, 0x45, 0x9a, 0xdb, 0x7b, 0x27, 0x8b, 0xac, 0xc7, 0x37, 0xcd, 0xfa, 0x15, 0x01, 0xdc, 0x49, 0x02, 0x42, 0x80, - 0x86, 0xd7, 0xf5, 0x7c, 0x38, 0x4b, 0x58, 0x1e, 0x60, 0xba, 0xab, 0xe0, 0xef, 0xc4, 0x4d, 0xce, 0x4b, 0x13, 0xfa, - 0xb1, 0xa8, 0xe0, 0x83, 0x9d, 0x2c, 0x10, 0x6e, 0x01, 0x96, 0x10, 0x04, 0x82, 0x92, 0x99, 0x29, 0xa6, 0x12, 0xfa, - 0x0b, 0x29, 0x21, 0x43, 0x02, 0x4c, 0x47, 0xe3, 0x82, 0x1b, 0x24, 0xd5, 0x46, 0x3c, 0xad, 0x62, 0x36, 0x9c, 0x34, - 0x0c, 0x88, 0xf5, 0xc7, 0x30, 0xd8, 0x2a, 0x26, 0xc9, 0xb0, 0xbf, 0xb3, 0x37, 0x9e, 0x0c, 0xa7, 0x4b, 0x14, 0x72, - 0xb1, 0xcf, 0x98, 0x3c, 0xa1, 0xe9, 0x17, 0x85, 0xa8, 0x2f, 0xeb, 0x82, 0xd3, 0x7b, 0x76, 0x74, 0x07, 0x4f, 0xae, - 0x32, 0xd2, 0xdb, 0x38, 0xb0, 0xdc, 0x42, 0x22, 0xc0, 0xbc, 0xdf, 0x03, 0xcd, 0x48, 0x32, 0x64, 0x28, 0x03, 0xcc, - 0x35, 0x66, 0x4f, 0x0d, 0x4d, 0x0f, 0x65, 0x47, 0x72, 0x6d, 0x12, 0xac, 0x1e, 0xe6, 0xbe, 0xbc, 0xb2, 0x6e, 0x73, - 0xbd, 0x03, 0xb9, 0x6e, 0x6b, 0x08, 0xd8, 0xe5, 0x88, 0x34, 0xa7, 0x26, 0xb7, 0x09, 0xd5, 0x03, 0x14, 0x48, 0x35, - 0xd5, 0xb4, 0x0e, 0x0e, 0x37, 0x7c, 0xd0, 0x01, 0xe1, 0x26, 0xc4, 0x46, 0xe5, 0x11, 0x7a, 0xad, 0xc6, 0x3e, 0xd1, - 0xd7, 0x92, 0x6b, 0x9a, 0x6f, 0x90, 0x3a, 0x72, 0xa9, 0xea, 0x3c, 0x4e, 0xd4, 0xb5, 0xb6, 0xda, 0x82, 0x2d, 0xc2, - 0x00, 0x8b, 0x55, 0x0c, 0x87, 0xe8, 0x54, 0xa8, 0x68, 0x89, 0x7b, 0x1b, 0x73, 0xd5, 0xcb, 0x9d, 0xb7, 0x55, 0x97, - 0x7a, 0xa7, 0x06, 0x8d, 0xc8, 0xf4, 0x50, 0x01, 0x0e, 0x84, 0x8c, 0xb5, 0x7d, 0xb0, 0x8c, 0xe3, 0x8c, 0x54, 0x65, - 0xd8, 0x08, 0x46, 0xd3, 0x01, 0xca, 0x5a, 0xf5, 0x38, 0x9c, 0x03, 0x62, 0x79, 0x48, 0x6e, 0x9a, 0xcc, 0x10, 0xd9, - 0x22, 0x9b, 0x5f, 0x6a, 0xf2, 0xe4, 0x0a, 0x1d, 0x0d, 0xfb, 0x1e, 0xd0, 0xae, 0xee, 0xc0, 0x40, 0x76, 0xf8, 0xaa, - 0x93, 0xce, 0x72, 0x09, 0xb4, 0x39, 0x86, 0xce, 0x85, 0xc5, 0x29, 0x9f, 0xa7, 0x23, 0x1b, 0xee, 0x1d, 0xe0, 0x45, - 0x47, 0xd7, 0x0b, 0xf0, 0xdb, 0xc1, 0xc5, 0x1d, 0x63, 0x0f, 0x6e, 0xca, 0xa3, 0x2c, 0x3b, 0x95, 0x30, 0x95, 0x47, - 0x13, 0x17, 0xeb, 0x9c, 0x0b, 0x5d, 0xce, 0xe6, 0x75, 0xba, 0xf5, 0x27, 0x2a, 0x86, 0x9b, 0xb5, 0x73, 0x06, 0xcf, - 0x55, 0x4e, 0x87, 0x24, 0x62, 0x49, 0x8b, 0x73, 0xf4, 0x85, 0x44, 0x9e, 0xd6, 0xf9, 0xfd, 0x42, 0x81, 0xce, 0xa9, - 0x83, 0x6a, 0x1d, 0xe3, 0xcc, 0x4e, 0x0f, 0x3a, 0xef, 0x95, 0xc6, 0xa2, 0xb1, 0x4a, 0x59, 0xf1, 0x1f, 0x38, 0xb7, - 0xf4, 0xf6, 0x84, 0xb0, 0x49, 0x2a, 0xa4, 0x50, 0x96, 0x09, 0xb7, 0x3d, 0x0e, 0x34, 0x6d, 0xe7, 0x44, 0x76, 0x5b, - 0xdf, 0xbe, 0x93, 0x24, 0x22, 0x71, 0xdb, 0x0b, 0xa2, 0xf0, 0x0c, 0xd0, 0x18, 0x92, 0xb3, 0xe7, 0x9d, 0x75, 0xf9, - 0xd2, 0xcb, 0x72, 0xbc, 0xc2, 0xde, 0x15, 0x83, 0xb1, 0xb0, 0x42, 0x0b, 0x0b, 0x37, 0x0d, 0xd4, 0xb1, 0x93, 0x24, - 0x76, 0x59, 0x12, 0x3f, 0xb6, 0xfc, 0x33, 0x69, 0x6e, 0x44, 0x9e, 0x8a, 0x8e, 0x75, 0xc8, 0x3e, 0x73, 0xaa, 0x54, - 0xf7, 0x5a, 0xe5, 0x41, 0x39, 0xe6, 0xa9, 0x1a, 0x31, 0x67, 0x6e, 0x33, 0x45, 0x3e, 0x92, 0x3e, 0x6f, 0xae, 0x67, - 0x94, 0x28, 0x10, 0xa9, 0x0b, 0xbd, 0xca, 0x9c, 0xab, 0xd0, 0x91, 0x42, 0x4a, 0xb7, 0x46, 0xb3, 0x89, 0x39, 0x0e, - 0x67, 0x3f, 0x55, 0xd9, 0x13, 0x7c, 0xed, 0x3d, 0x6f, 0xed, 0xc3, 0x66, 0x83, 0xeb, 0x50, 0xf3, 0x21, 0x3d, 0x60, - 0xa6, 0x99, 0x3b, 0x53, 0x20, 0x0b, 0xdb, 0xaf, 0xec, 0x48, 0x94, 0x32, 0xfd, 0x63, 0xa3, 0x75, 0x7d, 0xd9, 0x47, - 0x75, 0x4c, 0xfe, 0xfd, 0x2d, 0x5d, 0xc3, 0x55, 0x07, 0x45, 0x8e, 0xe1, 0x58, 0xd1, 0x6e, 0xa5, 0x3b, 0x00, 0xe1, - 0x35, 0x3b, 0x8c, 0xdc, 0x72, 0x36, 0x45, 0xbd, 0x55, 0x57, 0xc1, 0x02, 0x6a, 0xd4, 0x91, 0x94, 0xbd, 0x51, 0x58, - 0x44, 0xfd, 0x9a, 0x5d, 0x8b, 0x2b, 0x8a, 0x6e, 0x59, 0xe3, 0x7e, 0xc8, 0xec, 0xa8, 0x3f, 0xe2, 0x5a, 0xb9, 0xc3, - 0x0c, 0xd9, 0xe1, 0x1a, 0x53, 0x48, 0xea, 0x8d, 0xc6, 0xcd, 0xb6, 0xd5, 0xf3, 0x4c, 0xc3, 0xb8, 0x6d, 0xcd, 0xd2, - 0x26, 0x76, 0x50, 0x0d, 0xb7, 0x75, 0xc1, 0x54, 0xb5, 0x5d, 0xf8, 0xfa, 0xd5, 0x6e, 0x25, 0xb2, 0x26, 0xb4, 0xe1, - 0x68, 0x6b, 0x60, 0x9a, 0x16, 0xf9, 0x5c, 0xf4, 0xec, 0x6a, 0xb0, 0xc5, 0xbe, 0x0b, 0xd9, 0xbc, 0xfb, 0x6b, 0x95, - 0x84, 0x2a, 0xb9, 0x72, 0x1f, 0x97, 0xe4, 0x27, 0x9d, 0xac, 0xc2, 0x33, 0xb5, 0x8d, 0xfc, 0x0e, 0x27, 0xda, 0x87, - 0x95, 0xe6, 0x69, 0x25, 0xb3, 0x10, 0x10, 0x85, 0xae, 0xf0, 0x2a, 0x04, 0xba, 0x05, 0x0b, 0xff, 0x07, 0x3a, 0x76, - 0x65, 0x5c, 0x0a, 0xe9, 0x8d, 0xca, 0x39, 0x74, 0x43, 0x42, 0x3e, 0xb4, 0xb0, 0x9c, 0x9c, 0x97, 0x1a, 0x74, 0xb5, - 0x35, 0x74, 0x64, 0x79, 0x20, 0x02, 0xfc, 0x44, 0x0e, 0x79, 0xa6, 0x26, 0xd8, 0xfd, 0x24, 0x70, 0x96, 0x66, 0xb3, - 0x08, 0xbf, 0x18, 0x70, 0x86, 0xa4, 0xf6, 0x9e, 0x7e, 0xf9, 0x14, 0x68, 0x0f, 0xbf, 0x5c, 0x68, 0x7d, 0x72, 0x66, - 0xce, 0x51, 0x4b, 0xc7, 0x0d, 0x1c, 0xc2, 0x45, 0x69, 0xc0, 0xf7, 0x48, 0x35, 0x86, 0x29, 0x22, 0x4c, 0x0e, 0xe0, - 0x5c, 0xb9, 0x6d, 0x78, 0x56, 0x6e, 0x02, 0x33, 0x1d, 0x29, 0xa5, 0x15, 0xc7, 0xa8, 0xfb, 0xb6, 0xf7, 0xa3, 0x24, - 0xe9, 0xcd, 0xc7, 0xcb, 0xac, 0x50, 0xfa, 0x9e, 0x99, 0x85, 0xae, 0xe2, 0x77, 0x26, 0xb9, 0xab, 0x4b, 0xe8, 0xa4, - 0x5a, 0xce, 0x80, 0x51, 0xae, 0x56, 0x58, 0xee, 0x84, 0x40, 0x0e, 0x9b, 0xfb, 0xe9, 0x66, 0x90, 0x26, 0x5b, 0x51, - 0x95, 0x18, 0x23, 0x52, 0x68, 0xbf, 0xd9, 0x9d, 0xfb, 0xa3, 0xd5, 0x0c, 0x3a, 0xea, 0x3b, 0x66, 0x5c, 0xcd, 0xb7, - 0x62, 0xbb, 0xd8, 0xb0, 0x83, 0x69, 0x14, 0x75, 0x98, 0xe6, 0x01, 0x42, 0xf7, 0x2c, 0x1d, 0xa8, 0x5f, 0x10, 0x9f, - 0xf2, 0x76, 0x55, 0x6d, 0x1d, 0xe4, 0x62, 0xa6, 0xa2, 0x7c, 0x8a, 0x1a, 0x14, 0xb0, 0x68, 0xdd, 0x2e, 0x4d, 0xc0, - 0x14, 0x59, 0x48, 0xb7, 0x90, 0x82, 0x28, 0x59, 0x08, 0x66, 0x50, 0xf1, 0x99, 0xbf, 0x4c, 0x7c, 0xad, 0x8f, 0x16, - 0x3c, 0xa5, 0x27, 0x6c, 0x15, 0x72, 0x75, 0xc7, 0x68, 0x31, 0xab, 0x4e, 0x3b, 0x4e, 0x13, 0x87, 0x0e, 0x35, 0xea, - 0x88, 0xd8, 0x75, 0x7c, 0xf0, 0x54, 0x32, 0x79, 0x83, 0xec, 0x2f, 0x27, 0x01, 0x3f, 0xd6, 0xb3, 0x5f, 0x32, 0x7b, - 0x88, 0x55, 0x69, 0xc6, 0xe3, 0x85, 0xb2, 0x47, 0xe5, 0xa8, 0xa8, 0x35, 0xf6, 0x73, 0x17, 0xa7, 0xb5, 0x51, 0x49, - 0x21, 0x77, 0x1e, 0x2e, 0xe4, 0x2b, 0xa7, 0x70, 0xee, 0x46, 0x25, 0xa2, 0x3c, 0x80, 0x99, 0xb0, 0x39, 0x71, 0xa3, - 0xe2, 0x16, 0x50, 0x39, 0xd3, 0x93, 0x26, 0x31, 0x9d, 0x95, 0x88, 0x31, 0xa3, 0x4b, 0xb8, 0x1e, 0x87, 0x68, 0x0c, - 0xcd, 0x30, 0xa7, 0xf7, 0x31, 0x7a, 0x82, 0x1c, 0x50, 0x0f, 0xed, 0x5a, 0x43, 0x88, 0x99, 0x54, 0xf8, 0x56, 0xad, - 0x88, 0x2d, 0xb3, 0x4f, 0x04, 0xb5, 0x6d, 0x2e, 0xf3, 0x88, 0x28, 0x6f, 0x29, 0x7c, 0x9f, 0xfb, 0xcb, 0x77, 0x8c, - 0x57, 0x72, 0xe8, 0x9d, 0x8e, 0x92, 0x9f, 0xc3, 0xfc, 0xec, 0x37, 0x0e, 0x60, 0x01, 0x11, 0xe7, 0x92, 0x9c, 0x7a, - 0x4a, 0x96, 0xe6, 0x3a, 0xeb, 0x75, 0x13, 0xc1, 0x2c, 0x99, 0x06, 0x4c, 0xac, 0x65, 0x16, 0x40, 0x07, 0x52, 0x09, - 0x9c, 0x15, 0x95, 0x75, 0x34, 0x93, 0x47, 0x0b, 0xbd, 0x37, 0xf1, 0xf0, 0x45, 0x29, 0xc6, 0x02, 0xfc, 0xb1, 0xa5, - 0xc6, 0xa2, 0x4c, 0xdf, 0xbc, 0x08, 0x54, 0xcd, 0x5a, 0x1e, 0x87, 0x74, 0xe9, 0xf5, 0x8a, 0x9a, 0x55, 0x49, 0x6b, - 0xa9, 0x2e, 0xd0, 0x76, 0x40, 0x8e, 0x51, 0x8b, 0xea, 0x0c, 0xd2, 0x50, 0xb4, 0x07, 0x4a, 0x5f, 0xc3, 0x84, 0x1e, - 0xf0, 0x4b, 0x35, 0x90, 0xd9, 0xe0, 0x9d, 0x4d, 0xb7, 0xb8, 0x98, 0x1c, 0x39, 0xeb, 0x06, 0x10, 0x70, 0xbb, 0xde, - 0x96, 0x9a, 0x08, 0xa9, 0x70, 0x83, 0x71, 0x59, 0x24, 0xea, 0x2f, 0x9a, 0xc3, 0xda, 0x15, 0x92, 0x3a, 0xc4, 0x3a, - 0xb4, 0x30, 0x01, 0xad, 0x19, 0x17, 0x1b, 0x5a, 0x94, 0x9d, 0xc8, 0x81, 0xb5, 0x59, 0x24, 0x19, 0x87, 0x3d, 0x9a, - 0x69, 0x33, 0x91, 0x6b, 0x09, 0x9e, 0x4a, 0x44, 0x6f, 0xd1, 0xf4, 0xeb, 0x07, 0x15, 0x36, 0x37, 0x99, 0x54, 0xca, - 0x4c, 0x8f, 0x86, 0x40, 0xbb, 0x76, 0x07, 0x7c, 0x87, 0x0a, 0xfe, 0x12, 0x3e, 0x18, 0x45, 0xf7, 0xfb, 0xec, 0x69, - 0x07, 0x7c, 0x08, 0xa7, 0x4e, 0xfb, 0x45, 0x80, 0x75, 0x0e, 0x94, 0x62, 0x5d, 0x98, 0xe3, 0x8c, 0xa3, 0x76, 0x35, - 0xa3, 0x8d, 0xfd, 0xc4, 0x18, 0x02, 0x85, 0xc3, 0xb7, 0x3d, 0x5a, 0x79, 0xd5, 0xcc, 0xd6, 0x4c, 0x2f, 0x69, 0x47, - 0x3e, 0xa2, 0x46, 0x30, 0x09, 0x22, 0x69, 0x99, 0x40, 0x68, 0xc6, 0xe8, 0x2d, 0x5c, 0xc1, 0xda, 0x9c, 0x01, 0x2d, - 0x75, 0xbd, 0x50, 0xe8, 0x81, 0xa7, 0xe7, 0x4c, 0x4c, 0x0a, 0xf3, 0x01, 0x2e, 0x69, 0xff, 0xde, 0x1c, 0x66, 0x0d, - 0xd5, 0x6a, 0x6d, 0xb7, 0x65, 0x7d, 0x97, 0x28, 0x10, 0xb6, 0x1f, 0xda, 0x45, 0xf7, 0x23, 0x3f, 0xbb, 0x16, 0xa0, - 0xce, 0x62, 0xdb, 0x35, 0x9e, 0xf4, 0xd7, 0x5e, 0xb7, 0x04, 0x1f, 0xfb, 0x2b, 0x0d, 0x9f, 0x54, 0x0c, 0xcb, 0x92, - 0x09, 0xd3, 0x95, 0xe5, 0x18, 0x67, 0xa5, 0xb8, 0xcf, 0xcb, 0x98, 0x74, 0x77, 0x28, 0x31, 0x89, 0xaf, 0x3b, 0x1b, - 0xd0, 0xb7, 0x8c, 0xe8, 0x65, 0xfd, 0x56, 0xcf, 0xb0, 0xd7, 0x25, 0x80, 0x98, 0x7a, 0x45, 0xc5, 0x78, 0x98, 0xe8, - 0x8b, 0x87, 0xa0, 0x30, 0x7e, 0x94, 0xb9, 0x18, 0x7c, 0x52, 0x6f, 0x5b, 0x48, 0x84, 0x9f, 0xc6, 0xa3, 0xb8, 0x98, - 0xb5, 0x68, 0xd8, 0x76, 0x3d, 0x29, 0x0e, 0x84, 0x84, 0xd6, 0xcc, 0xa7, 0x49, 0x5a, 0x73, 0x29, 0x0c, 0xbf, 0x59, - 0x88, 0x8d, 0x66, 0xe3, 0x28, 0x5a, 0x0b, 0x60, 0x74, 0x55, 0x73, 0xc5, 0x62, 0xe0, 0x61, 0xc1, 0x43, 0xf9, 0xd2, - 0x12, 0x96, 0x3d, 0x7f, 0x9f, 0x4e, 0xe4, 0x9b, 0xbb, 0x9c, 0x6e, 0xbf, 0x77, 0x9d, 0xbd, 0xb9, 0x4b, 0x27, 0xca, - 0xea, 0x17, 0x1d, 0x95, 0xa8, 0xc6, 0xfa, 0xd8, 0xfa, 0x70, 0x97, 0x5b, 0xfd, 0x44, 0x72, 0xda, 0xd9, 0x0e, 0x18, - 0xb7, 0x14, 0xb0, 0x65, 0xda, 0x1e, 0x36, 0xe5, 0x3f, 0xde, 0xba, 0x38, 0xd2, 0x28, 0x48, 0x7c, 0xc2, 0x9c, 0x21, - 0x49, 0xf1, 0xd8, 0x64, 0x00, 0xa3, 0x96, 0x01, 0xf5, 0x08, 0xf6, 0x75, 0x63, 0x47, 0xbe, 0xb9, 0x8c, 0x71, 0xa9, - 0x4e, 0xbb, 0x0e, 0x64, 0xda, 0xe5, 0x21, 0xb0, 0x71, 0x9b, 0xbb, 0x1c, 0x28, 0x12, 0x07, 0x2a, 0x62, 0xa6, 0xfd, - 0x22, 0xf5, 0xa7, 0x1b, 0xc4, 0x46, 0xed, 0xc0, 0xf5, 0x39, 0xd8, 0x14, 0xfb, 0x64, 0xb1, 0xdf, 0xca, 0x3b, 0x3b, - 0xec, 0x8d, 0xf4, 0x47, 0x9c, 0x9b, 0xcf, 0x38, 0x30, 0xa2, 0x4a, 0x73, 0x31, 0x2b, 0x91, 0x2a, 0xb2, 0xa7, 0x95, - 0xef, 0x2f, 0xa4, 0x49, 0xe0, 0xdc, 0xad, 0x0f, 0x3d, 0x9c, 0xcc, 0x9e, 0x08, 0x93, 0x39, 0xe4, 0x3b, 0x78, 0x49, - 0x89, 0xa6, 0x0b, 0x8d, 0xb6, 0x5b, 0x07, 0x04, 0x76, 0x02, 0xe6, 0x69, 0x89, 0xbc, 0x4e, 0xc9, 0x4b, 0x7e, 0xff, - 0xf6, 0xcf, 0xd2, 0xb2, 0x80, 0xe1, 0xc8, 0x53, 0x5c, 0xa5, 0x00, 0x11, 0xc7, 0x71, 0xfe, 0x6a, 0xdd, 0x67, 0x24, - 0xc6, 0xfa, 0xf3, 0xbb, 0x1f, 0xec, 0x3e, 0x41, 0xae, 0xa4, 0xa1, 0xf0, 0xcc, 0xcd, 0x91, 0x9d, 0x83, 0xec, 0xca, - 0xb8, 0x62, 0xb7, 0x41, 0x3f, 0x89, 0x2c, 0x2a, 0xd1, 0x4c, 0xeb, 0xcd, 0x29, 0x16, 0x49, 0x49, 0x2b, 0x2c, 0x6a, - 0xc9, 0x17, 0x0c, 0xe5, 0x30, 0x59, 0x96, 0xb6, 0x9d, 0x39, 0x0e, 0xc5, 0x5a, 0x96, 0x00, 0xd9, 0xc5, 0x12, 0x9c, - 0x2b, 0x8a, 0x5c, 0x86, 0x95, 0x35, 0xb7, 0x02, 0xe3, 0xc0, 0x14, 0x7e, 0xf2, 0x8f, 0x12, 0xed, 0xef, 0x64, 0x58, - 0xb2, 0x8b, 0x3f, 0xa7, 0x2b, 0xf4, 0xda, 0xb9, 0x17, 0xcc, 0x60, 0x32, 0x44, 0xef, 0xb1, 0x84, 0x79, 0xb9, 0x13, - 0xaf, 0x4a, 0x96, 0xa5, 0x71, 0xe0, 0xa0, 0x59, 0x37, 0x6b, 0x75, 0xdf, 0x22, 0x48, 0xcb, 0x86, 0xab, 0x46, 0xac, - 0xb4, 0x12, 0x2a, 0xa1, 0x29, 0xe8, 0x88, 0x92, 0xbc, 0x44, 0x98, 0x19, 0x80, 0xb2, 0x93, 0x88, 0xca, 0x08, 0x82, - 0x63, 0x58, 0xb1, 0x98, 0x69, 0x5c, 0xd9, 0x80, 0xd5, 0xa9, 0xf1, 0x51, 0x1e, 0xba, 0x5e, 0xc0, 0x50, 0x7d, 0xed, - 0x4d, 0xc6, 0x39, 0xc6, 0xbc, 0xd6, 0x4c, 0x73, 0x72, 0xec, 0x7f, 0xdc, 0x55, 0x13, 0x24, 0x2d, 0xbe, 0x1f, 0xed, - 0x67, 0x0c, 0x4d, 0x33, 0x20, 0x96, 0x3d, 0x7c, 0xc2, 0x56, 0x07, 0x6c, 0xd2, 0x75, 0xd8, 0x48, 0x12, 0x25, 0xe8, - 0x4d, 0x9c, 0x66, 0xfb, 0x26, 0x80, 0xa1, 0xba, 0x34, 0xc8, 0x9e, 0x47, 0x46, 0xbc, 0x35, 0x96, 0x03, 0x4b, 0xbe, - 0x02, 0xba, 0xa0, 0x3c, 0xf3, 0x08, 0xce, 0xb6, 0x73, 0x20, 0x0a, 0x63, 0x2d, 0x8a, 0x4b, 0x9c, 0xf0, 0x3b, 0x92, - 0x43, 0x59, 0x32, 0x43, 0x61, 0xca, 0xe7, 0xe0, 0x5c, 0x99, 0x0f, 0x1f, 0xfe, 0x90, 0x7f, 0xfc, 0x4c, 0x57, 0x97, - 0x22, 0xf6, 0xf9, 0x71, 0x8e, 0xcf, 0xbf, 0x4d, 0x7a, 0xca, 0xed, 0x2c, 0xfc, 0x06, 0xde, 0x59, 0x42, 0xce, 0xbb, - 0x1f, 0x7e, 0xd4, 0x2d, 0x0e, 0x8a, 0x85, 0xce, 0x62, 0x8b, 0x5a, 0x70, 0xfe, 0xf9, 0x79, 0x31, 0x17, 0x55, 0x1e, - 0x13, 0x38, 0x53, 0x49, 0x59, 0xfd, 0xa6, 0x48, 0x81, 0xb4, 0x8d, 0x4a, 0xc2, 0xc6, 0xff, 0x18, 0x45, 0xf1, 0xff, - 0xa3, 0x0c, 0x85, 0x86, 0xac, 0xfd, 0xf1, 0x96, 0x45, 0x65, 0x83, 0xcd, 0xff, 0x98, 0x68, 0xad, 0x56, 0x9f, 0x09, - 0x50, 0x49, 0x5b, 0x49, 0xa5, 0x0f, 0x0a, 0x3c, 0xd3, 0xd1, 0xe4, 0x0c, 0x35, 0xc8, 0x88, 0x27, 0x8c, 0x33, 0x60, - 0x68, 0x9b, 0x75, 0xc9, 0xbb, 0x6d, 0x13, 0x7f, 0x8d, 0xc2, 0x9b, 0x32, 0xb5, 0xd1, 0x18, 0x24, 0xa7, 0x0a, 0x90, - 0xe6, 0x38, 0x5b, 0x85, 0xae, 0x68, 0xc3, 0x39, 0x37, 0x6b, 0x2d, 0x38, 0x1b, 0xc6, 0x56, 0xc3, 0x97, 0xbf, 0x20, - 0x41, 0x60, 0xd7, 0xa4, 0x0e, 0xaa, 0xb2, 0xe6, 0xc5, 0x4d, 0xf8, 0x27, 0x6c, 0x2f, 0x31, 0x98, 0xc9, 0x4b, 0x9a, - 0x77, 0xa6, 0x23, 0xa4, 0x79, 0x84, 0x9c, 0xd9, 0xfc, 0x8f, 0x62, 0x26, 0xcb, 0x43, 0x19, 0xcd, 0x7c, 0x98, 0x18, - 0xff, 0xe6, 0x49, 0x02, 0xfb, 0x99, 0xf3, 0x61, 0x14, 0x99, 0x58, 0x1e, 0xdb, 0xc6, 0x0b, 0x72, 0x1f, 0x43, 0x37, - 0x5a, 0xac, 0xb2, 0x2c, 0x63, 0x5f, 0x29, 0xb3, 0xb4, 0xb4, 0xe0, 0xf0, 0x74, 0x03, 0xa2, 0x0a, 0x9d, 0x0d, 0x21, - 0xcf, 0xa5, 0x7f, 0x59, 0xa5, 0xc2, 0xf4, 0xa1, 0xcc, 0x58, 0xeb, 0x2d, 0x10, 0x7b, 0x3d, 0x51, 0x7f, 0x72, 0x87, - 0x44, 0x9b, 0xdc, 0x68, 0xf9, 0xe0, 0x14, 0x56, 0x93, 0x74, 0x1e, 0x99, 0x88, 0x47, 0xf8, 0xce, 0xb6, 0x9f, 0xb7, - 0x4a, 0x7a, 0x7e, 0xf0, 0x11, 0x76, 0xbb, 0x34, 0xf6, 0x5e, 0xf2, 0x3b, 0xf9, 0x39, 0xfa, 0x30, 0xb8, 0x23, 0x27, - 0x25, 0xb5, 0xfd, 0xa1, 0x8f, 0x71, 0x1d, 0x28, 0xbb, 0xff, 0x41, 0xe3, 0x39, 0x64, 0x51, 0xf1, 0x68, 0x92, 0xce, - 0x30, 0x07, 0x4b, 0xfd, 0x30, 0x73, 0xe1, 0x6f, 0xd2, 0x04, 0x67, 0xd1, 0x8d, 0x5e, 0x1e, 0x4c, 0xeb, 0xc9, 0x3f, - 0x22, 0x2b, 0x7f, 0x9a, 0x65, 0x93, 0xc3, 0x69, 0xb8, 0xe0, 0x47, 0x32, 0xfa, 0xed, 0xac, 0x6e, 0x4f, 0x96, 0xad, - 0x5b, 0xed, 0x21, 0x60, 0xfa, 0x91, 0x86, 0x48, 0xde, 0x2c, 0x53, 0x85, 0x81, 0xa8, 0x18, 0x5c, 0xd0, 0x1a, 0x74, - 0x29, 0x35, 0xb5, 0x55, 0xe0, 0x8c, 0x4e, 0x04, 0x1d, 0x54, 0x70, 0xb4, 0x5c, 0xf9, 0xea, 0x07, 0x0d, 0x8b, 0x93, - 0x8a, 0xed, 0xb6, 0x28, 0x92, 0x3d, 0x83, 0xe3, 0x68, 0x11, 0x15, 0x99, 0xf1, 0xef, 0x32, 0x5a, 0x29, 0xa5, 0x54, - 0x82, 0xe0, 0x8e, 0xbe, 0xd0, 0xc5, 0x65, 0x94, 0x86, 0xfc, 0x90, 0x4a, 0xb6, 0xd0, 0x80, 0x4a, 0x29, 0x0e, 0x54, - 0x8d, 0xcb, 0x44, 0xb8, 0x32, 0x36, 0x17, 0x8d, 0x2b, 0x5a, 0x15, 0xaf, 0x62, 0x87, 0xf4, 0xfa, 0x3a, 0x51, 0x85, - 0x21, 0xcb, 0xc0, 0xe1, 0xe1, 0x1c, 0x65, 0xcd, 0x93, 0x6d, 0x28, 0xc9, 0x33, 0x15, 0x73, 0x30, 0xe3, 0x5c, 0x3d, - 0xa9, 0xc6, 0x06, 0x34, 0x54, 0x54, 0x67, 0x31, 0x9d, 0xad, 0x0e, 0xa8, 0x53, 0x02, 0x02, 0xb3, 0xf0, 0x18, 0x8f, - 0xa3, 0x10, 0x77, 0xa5, 0x0c, 0xbb, 0x70, 0x9b, 0x25, 0x58, 0x8f, 0x93, 0xe1, 0x70, 0x47, 0x1b, 0x3b, 0x17, 0x75, - 0xf8, 0x51, 0xb0, 0x4b, 0x31, 0x68, 0x48, 0x23, 0x24, 0xb9, 0xd8, 0x39, 0x75, 0x2c, 0x9a, 0x70, 0x27, 0x0b, 0x02, - 0x20, 0xf2, 0x30, 0xf5, 0xc1, 0xe2, 0xf2, 0xa8, 0xb3, 0x80, 0x89, 0x79, 0xae, 0xec, 0xa2, 0xbc, 0x81, 0xaf, 0xd6, - 0xa1, 0x1c, 0x62, 0x99, 0xc4, 0x58, 0x69, 0x33, 0xfe, 0x77, 0x59, 0x1e, 0xa5, 0x37, 0x96, 0xd5, 0xb4, 0x45, 0xf5, - 0xa0, 0xd1, 0x1d, 0xae, 0x1d, 0x31, 0x36, 0x96, 0x59, 0x27, 0x86, 0x05, 0xf3, 0xdf, 0x67, 0x86, 0xc5, 0x46, 0x55, - 0xcb, 0x37, 0x39, 0xdf, 0xbd, 0xe3, 0x53, 0x08, 0x66, 0x51, 0xc3, 0x03, 0xee, 0x1e, 0x96, 0x31, 0x2c, 0x16, 0x04, - 0xb3, 0xec, 0xc1, 0xc3, 0xba, 0x1a, 0x82, 0x34, 0xe3, 0x51, 0x92, 0x40, 0x37, 0x62, 0x28, 0x46, 0x72, 0x46, 0xc0, - 0x26, 0x29, 0xc4, 0xe0, 0x15, 0xb0, 0x3f, 0xd6, 0x25, 0xa4, 0x82, 0x23, 0x3c, 0x20, 0x1c, 0xaa, 0xb8, 0xfc, 0xb0, - 0x88, 0x61, 0x20, 0x86, 0x54, 0xbc, 0x98, 0x95, 0x4f, 0x0b, 0x80, 0x91, 0x35, 0xaa, 0x78, 0x48, 0x86, 0xc8, 0xc8, - 0x9b, 0x16, 0x19, 0x75, 0xf0, 0xc6, 0xf0, 0x1b, 0x11, 0x03, 0x5e, 0xc9, 0x19, 0xe4, 0x31, 0x27, 0x2b, 0x73, 0xf9, - 0x32, 0x77, 0xe9, 0xb7, 0xe3, 0xa9, 0x1c, 0xb7, 0xcd, 0x3c, 0xb4, 0xe9, 0x65, 0xac, 0x73, 0x51, 0x71, 0x70, 0xbd, - 0xcc, 0x31, 0xa2, 0xa7, 0xf9, 0xc2, 0xb5, 0x45, 0xf6, 0xb4, 0x86, 0xeb, 0xd7, 0x2a, 0xfb, 0xf0, 0x09, 0x19, 0x5b, - 0x40, 0x86, 0x87, 0x9d, 0xba, 0x6d, 0x64, 0x0c, 0x23, 0xf0, 0xdf, 0xc6, 0xf7, 0x13, 0xe7, 0x98, 0x2e, 0x73, 0x41, - 0x72, 0x98, 0x17, 0xf8, 0xb6, 0x30, 0xfe, 0x92, 0x73, 0x1c, 0x8d, 0xc9, 0xba, 0x87, 0x1a, 0xdd, 0xbd, 0xb4, 0xe1, - 0x0b, 0x26, 0xe8, 0xfc, 0x12, 0x1d, 0x5f, 0x93, 0x06, 0xcb, 0x7d, 0xde, 0xd7, 0x33, 0x64, 0x1a, 0x0f, 0x63, 0x4c, - 0xc8, 0x35, 0x9e, 0x33, 0xd1, 0x8d, 0x7a, 0xcf, 0x96, 0xb1, 0x96, 0xd8, 0x2a, 0xa2, 0xcd, 0x36, 0x58, 0x39, 0xaa, - 0xfe, 0xf5, 0x5d, 0x24, 0x82, 0x11, 0x35, 0xed, 0xd3, 0x5a, 0xdf, 0xa8, 0x3c, 0xf3, 0xdb, 0x99, 0x77, 0x1b, 0x56, - 0x86, 0x19, 0x04, 0x33, 0xbe, 0x62, 0xce, 0xd0, 0xcf, 0x23, 0x73, 0x0f, 0xbc, 0xdd, 0x4b, 0xef, 0xc6, 0x9a, 0x35, - 0xfa, 0x61, 0xba, 0x53, 0x92, 0x59, 0x60, 0x3b, 0xfe, 0x4d, 0xd0, 0x53, 0x21, 0xf5, 0xa3, 0x3a, 0xb0, 0xf8, 0x9a, - 0x93, 0x98, 0x90, 0x0c, 0x39, 0x58, 0x90, 0xab, 0xe6, 0xbd, 0xa7, 0xdb, 0xb6, 0x8c, 0x0a, 0x71, 0xe9, 0x74, 0xf5, - 0xe5, 0xf5, 0xda, 0x0b, 0xb4, 0xa3, 0xfa, 0xd1, 0xc6, 0xcb, 0x78, 0xf1, 0x78, 0x03, 0x77, 0x22, 0x7e, 0x43, 0x6e, - 0x68, 0x8c, 0xaf, 0xc2, 0xa3, 0xb5, 0xda, 0x2b, 0xae, 0xbd, 0x69, 0xee, 0xf1, 0x8b, 0xb9, 0x56, 0x67, 0x4e, 0xb5, - 0x57, 0x66, 0x5c, 0x99, 0xb8, 0x58, 0x51, 0x92, 0x0f, 0x5f, 0x10, 0x5c, 0xc7, 0xcf, 0xd6, 0x41, 0xb8, 0xeb, 0xf1, - 0x9d, 0x1c, 0x2c, 0xc5, 0xc0, 0x74, 0x03, 0xd7, 0x81, 0x18, 0xc3, 0xd8, 0x22, 0x01, 0x92, 0xfa, 0xa1, 0x6c, 0xc5, - 0x28, 0x18, 0xbf, 0x3e, 0x5e, 0xb6, 0xea, 0x1d, 0xff, 0x61, 0x09, 0xe0, 0xd8, 0x46, 0x38, 0x02, 0xcd, 0xac, 0x38, - 0xe5, 0x52, 0x5c, 0xe8, 0x23, 0x38, 0xb3, 0x29, 0xfb, 0x80, 0xe3, 0x90, 0x4d, 0x30, 0xed, 0x8f, 0x86, 0xca, 0xef, - 0x9f, 0xc8, 0x8f, 0x6b, 0x77, 0xbf, 0x57, 0xd7, 0x16, 0x9e, 0xfe, 0x73, 0x87, 0xde, 0x49, 0x47, 0x5e, 0x3b, 0x1d, - 0x75, 0xb8, 0x02, 0xd9, 0x71, 0x53, 0x7b, 0x56, 0x57, 0x5f, 0xc9, 0xf1, 0x63, 0x7f, 0x5b, 0x95, 0x6e, 0xe3, 0xfd, - 0xf3, 0xb2, 0xaa, 0xec, 0x6c, 0xaf, 0x5c, 0x17, 0x7f, 0x59, 0x69, 0xf2, 0xda, 0x7f, 0xd1, 0x6f, 0xe7, 0xa4, 0x1f, - 0xe6, 0x9b, 0x45, 0x8b, 0xbc, 0x61, 0x9b, 0x01, 0xfe, 0xf1, 0xa0, 0xf1, 0x50, 0x44, 0xd8, 0xdc, 0x75, 0xbf, 0xb5, - 0xa1, 0x41, 0x31, 0x27, 0xef, 0x04, 0x29, 0x0a, 0x20, 0x71, 0xc7, 0x5a, 0x01, 0x38, 0x06, 0x86, 0xc1, 0x73, 0xef, - 0x53, 0xeb, 0xc6, 0x14, 0x75, 0xb9, 0x7a, 0xa2, 0xb1, 0x9b, 0xad, 0x37, 0xf4, 0x35, 0x6e, 0xf4, 0x1f, 0x91, 0x0b, - 0x11, 0x18, 0x3c, 0x3f, 0x80, 0xfb, 0xc7, 0x29, 0x7b, 0xd1, 0x62, 0x52, 0x79, 0xc3, 0xe7, 0xf6, 0xf5, 0xe1, 0xb5, - 0x7c, 0x9a, 0xcd, 0x05, 0x12, 0xbd, 0x3e, 0x36, 0xb5, 0xd0, 0x14, 0xb9, 0x96, 0x3b, 0xd9, 0xc5, 0xd1, 0x34, 0xc4, - 0x68, 0x01, 0x50, 0x28, 0x03, 0xc6, 0x4f, 0xb0, 0x86, 0x3a, 0xe3, 0x9f, 0xcf, 0xa7, 0x3c, 0xa7, 0xfb, 0xcd, 0x5b, - 0x33, 0xbd, 0xa5, 0x39, 0xe0, 0xdb, 0x90, 0xff, 0xdb, 0x3f, 0xd1, 0xad, 0x63, 0xac, 0xf6, 0x98, 0x1d, 0x5c, 0x9b, - 0x6b, 0x59, 0xf4, 0x6f, 0x6b, 0xe2, 0xca, 0xeb, 0xd1, 0x0f, 0xf8, 0x75, 0xee, 0x0b, 0x81, 0xd1, 0x14, 0x9e, 0xb1, - 0x98, 0xb4, 0x55, 0xae, 0xef, 0x7a, 0xc2, 0x6c, 0x1b, 0x9d, 0x22, 0x35, 0x04, 0xd7, 0xfb, 0x18, 0x57, 0x1b, 0x4f, - 0xa2, 0xb2, 0xda, 0xbe, 0x79, 0x2a, 0xc0, 0x85, 0xc6, 0xf2, 0x4f, 0xd4, 0x79, 0xbb, 0x47, 0x6d, 0x72, 0xda, 0x3f, - 0x69, 0xed, 0x9e, 0x4b, 0x0f, 0x1d, 0xe9, 0xb1, 0xe9, 0x53, 0x6b, 0xde, 0x10, 0xec, 0x5b, 0xd2, 0x62, 0x2f, 0x00, - 0xdc, 0x01, 0x9e, 0xa8, 0x36, 0xd1, 0xb3, 0xaa, 0x7f, 0xec, 0x01, 0x69, 0x8c, 0xef, 0x31, 0x49, 0x95, 0x1b, 0xb9, - 0x50, 0xb3, 0x48, 0x50, 0x74, 0x1c, 0x1f, 0xdf, 0x31, 0xad, 0xd6, 0xc3, 0xf3, 0x62, 0x55, 0x0a, 0x63, 0xcb, 0xdc, - 0x9b, 0x79, 0x90, 0xd3, 0x54, 0x1f, 0xbc, 0x16, 0xee, 0x1b, 0xba, 0x14, 0x3e, 0x16, 0x8f, 0x5a, 0xed, 0x80, 0x9c, - 0x6c, 0x41, 0x08, 0x47, 0x74, 0xfe, 0x52, 0x32, 0x53, 0x80, 0xd7, 0x81, 0xbb, 0xe2, 0x18, 0x3d, 0xb6, 0xe3, 0x6e, - 0x54, 0xdc, 0xc2, 0x9f, 0x1d, 0x44, 0x11, 0xd6, 0x55, 0xbb, 0x35, 0x61, 0xce, 0xcb, 0x14, 0x46, 0xa9, 0x90, 0x80, - 0x70, 0xb8, 0xcc, 0x0d, 0x50, 0x42, 0x49, 0x40, 0x5b, 0x15, 0xd5, 0x1f, 0xca, 0xdc, 0x76, 0xbb, 0x51, 0x73, 0x1e, - 0x89, 0x87, 0x81, 0x8a, 0xf5, 0x98, 0xd6, 0x5a, 0x92, 0x03, 0x0a, 0x51, 0xb3, 0xc9, 0xf3, 0xf2, 0x8f, 0xf5, 0x48, - 0x2e, 0x05, 0x8f, 0x44, 0x2c, 0xde, 0x96, 0xe4, 0x9b, 0xfc, 0xf1, 0x0c, 0x99, 0xbd, 0xe5, 0xe4, 0x87, 0x39, 0x4c, - 0x27, 0x76, 0x19, 0xf0, 0x04, 0x05, 0xac, 0x51, 0x8f, 0xb6, 0xa2, 0xa7, 0x80, 0x74, 0x98, 0x15, 0x0c, 0x08, 0x4e, - 0xa9, 0x5f, 0xe6, 0x47, 0xbe, 0xd9, 0x96, 0x42, 0x55, 0x89, 0x68, 0x29, 0x0b, 0xbe, 0xdc, 0x9e, 0x6f, 0x26, 0x94, - 0xac, 0xb8, 0xa6, 0xb6, 0x99, 0xad, 0xa2, 0x45, 0x2b, 0x08, 0x7f, 0x5c, 0xcd, 0x8c, 0xa8, 0xbf, 0x90, 0x6e, 0xd6, - 0xb4, 0x7d, 0x80, 0xb4, 0x9a, 0x53, 0x3b, 0x3b, 0x47, 0x73, 0x41, 0x03, 0xf5, 0x18, 0xc1, 0xc6, 0xe2, 0x52, 0x93, - 0x72, 0xd6, 0x39, 0xaf, 0xc6, 0x1b, 0x86, 0xdb, 0x4d, 0x52, 0x2f, 0x8a, 0x1b, 0x57, 0x37, 0x3a, 0xe9, 0x4b, 0xd0, - 0xc1, 0xa0, 0x03, 0x86, 0x94, 0x5a, 0x85, 0x8a, 0xec, 0xce, 0x62, 0x5d, 0x38, 0x4d, 0x48, 0x3a, 0x5d, 0xf1, 0x72, - 0x52, 0xbc, 0x67, 0x84, 0x38, 0xfa, 0x01, 0x29, 0x93, 0x47, 0xa8, 0x49, 0x5e, 0xfb, 0x80, 0x21, 0xf3, 0x67, 0xd4, - 0xe2, 0xb0, 0xa1, 0x0d, 0xa2, 0x7f, 0x10, 0x38, 0x1e, 0x47, 0x90, 0x0a, 0xd6, 0x53, 0x32, 0xba, 0x04, 0x48, 0x7a, - 0x09, 0x9f, 0x1e, 0xb1, 0x60, 0x6a, 0xee, 0x94, 0x82, 0xe2, 0xc9, 0x00, 0x43, 0x5b, 0x69, 0x54, 0x96, 0x54, 0x4e, - 0xf4, 0x40, 0x03, 0xef, 0x29, 0x14, 0x10, 0x46, 0x9c, 0x3d, 0xf6, 0x39, 0x2f, 0x62, 0x50, 0xec, 0xad, 0x41, 0xe8, - 0x3e, 0x03, 0xd8, 0xc8, 0x33, 0x0c, 0x16, 0x79, 0x5e, 0x21, 0x47, 0x65, 0x2f, 0xab, 0xb9, 0xff, 0x72, 0x46, 0xd9, - 0xc0, 0xe0, 0x51, 0x3d, 0xe9, 0xe4, 0x5a, 0xbf, 0x0e, 0x27, 0xc8, 0x59, 0xfa, 0x94, 0xd5, 0xa3, 0x76, 0x6e, 0xca, - 0x98, 0xac, 0x2b, 0xf5, 0x67, 0xee, 0x61, 0x24, 0xdf, 0xca, 0x99, 0x51, 0x16, 0xa9, 0x88, 0x17, 0x7e, 0x00, 0xa5, - 0x9f, 0x67, 0x1d, 0x83, 0xc2, 0x13, 0x0b, 0x0d, 0x81, 0x38, 0xc4, 0x35, 0x36, 0xb8, 0x71, 0x20, 0x18, 0x35, 0x68, - 0x4c, 0x6e, 0x51, 0xad, 0x29, 0x72, 0x2d, 0xd4, 0xa7, 0x06, 0x43, 0x6d, 0x9c, 0x79, 0x66, 0x25, 0x98, 0xd0, 0xf0, - 0x92, 0x4f, 0x95, 0xac, 0xa3, 0xb8, 0xc2, 0x2f, 0x57, 0x80, 0xd9, 0xc0, 0x34, 0x77, 0x1d, 0x60, 0xb0, 0xd2, 0x9c, - 0x9a, 0x91, 0x67, 0xe7, 0x0e, 0xa1, 0xd4, 0x8d, 0x5e, 0xc0, 0x04, 0x30, 0x1c, 0x02, 0xda, 0xa0, 0x97, 0x17, 0x3e, - 0x5c, 0x90, 0xaa, 0x1d, 0x19, 0x70, 0xb4, 0xc8, 0x89, 0xb2, 0x75, 0x88, 0xff, 0x99, 0x48, 0x48, 0xda, 0xec, 0x40, - 0xbc, 0x39, 0x76, 0x53, 0xc7, 0xaa, 0xe7, 0x20, 0xbf, 0xba, 0xc1, 0x5e, 0x2b, 0xae, 0x4c, 0x93, 0x1a, 0x7a, 0x35, - 0x1a, 0x87, 0x82, 0xb4, 0xbc, 0x98, 0xdd, 0x78, 0xd2, 0x24, 0xba, 0x2c, 0xdd, 0x34, 0xe8, 0x21, 0xbc, 0x33, 0x0f, - 0xf9, 0x1d, 0xef, 0xeb, 0xc9, 0xfe, 0x81, 0xa2, 0x43, 0x60, 0xb7, 0x21, 0xd7, 0x15, 0x5f, 0x3f, 0x21, 0xc5, 0x32, - 0xda, 0xa7, 0x5d, 0x5b, 0xf3, 0xd4, 0xe2, 0x04, 0x86, 0xbd, 0x9c, 0x8d, 0x0b, 0x6e, 0x55, 0x84, 0x61, 0x6a, 0x28, - 0x25, 0x97, 0x9d, 0x6e, 0x49, 0x4e, 0xae, 0x05, 0x1a, 0x83, 0x40, 0x71, 0x1e, 0xf7, 0x9f, 0xd3, 0x97, 0x12, 0x6c, - 0xc7, 0x0e, 0x46, 0x27, 0xe9, 0x3d, 0xad, 0x93, 0xa3, 0xa2, 0xb0, 0xdd, 0x29, 0xdd, 0x38, 0xf0, 0xc7, 0x89, 0x2a, - 0xb5, 0x25, 0x06, 0x9e, 0xef, 0xae, 0x4d, 0x42, 0x5b, 0x73, 0x0e, 0xb0, 0x12, 0x00, 0x28, 0x2f, 0x06, 0x55, 0xbb, - 0x78, 0xe0, 0xa6, 0xa5, 0x6d, 0x70, 0xd3, 0x40, 0x8d, 0x44, 0x04, 0x51, 0x40, 0xc2, 0xd4, 0x3f, 0x37, 0x25, 0x3b, - 0xf1, 0x8e, 0x79, 0x27, 0x0a, 0x15, 0x92, 0x06, 0xda, 0x79, 0xf5, 0xf0, 0xe8, 0x63, 0x42, 0x58, 0x63, 0x9c, 0x18, - 0xdb, 0x80, 0x7d, 0xd7, 0x5a, 0xd1, 0x5c, 0x17, 0xf4, 0xb6, 0xee, 0x14, 0xd5, 0x1c, 0xa0, 0x93, 0x70, 0x3b, 0x0f, - 0x3e, 0x42, 0x46, 0x95, 0xbc, 0xdf, 0xe5, 0xc8, 0xe3, 0x12, 0x04, 0x39, 0xdf, 0x36, 0x54, 0x1e, 0x83, 0x07, 0x51, - 0xe0, 0x83, 0x2a, 0x06, 0x53, 0xe5, 0xc4, 0x4d, 0x20, 0xd5, 0x08, 0x32, 0xaf, 0x22, 0xc4, 0x2b, 0x3a, 0xf9, 0x7d, - 0x8f, 0x40, 0xac, 0x12, 0x9e, 0x24, 0xf3, 0x2a, 0xf9, 0x34, 0x23, 0xae, 0x6a, 0x83, 0x61, 0x66, 0xd8, 0x6e, 0xc9, - 0x69, 0x8c, 0x88, 0xa7, 0xe3, 0x66, 0x6f, 0xe2, 0xb9, 0x11, 0xd8, 0xc2, 0x51, 0xc4, 0xf2, 0x35, 0xd1, 0x19, 0x98, - 0x21, 0x55, 0x85, 0xd8, 0x5c, 0xf2, 0x19, 0x91, 0x8d, 0xc2, 0xf5, 0xb5, 0xf8, 0xbb, 0x4a, 0x29, 0x11, 0x19, 0xea, - 0x6b, 0xf5, 0x4f, 0xd1, 0x08, 0xdb, 0xa9, 0x62, 0xf4, 0xfa, 0xf1, 0x81, 0x7a, 0x04, 0x3a, 0x53, 0xaa, 0x8d, 0x52, - 0x2d, 0x98, 0xd7, 0x5a, 0xa1, 0x61, 0xa1, 0x75, 0xd4, 0xa7, 0x26, 0xc3, 0xe2, 0xc7, 0xab, 0xdc, 0x2e, 0x06, 0x80, - 0x4e, 0x02, 0x94, 0xec, 0x0f, 0x2d, 0xf5, 0xcd, 0x2a, 0x4b, 0x12, 0x80, 0xcc, 0x01, 0xd8, 0xe3, 0x38, 0xa9, 0x59, - 0x91, 0x1d, 0xfd, 0x42, 0x54, 0x4e, 0xd8, 0xe1, 0x8b, 0xa5, 0x69, 0xfe, 0x00, 0x57, 0x09, 0xcc, 0x08, 0x31, 0x19, - 0xd7, 0x51, 0x67, 0x83, 0xd0, 0x02, 0xa0, 0x5b, 0xda, 0xa9, 0x87, 0xe4, 0xed, 0x7a, 0x0c, 0x52, 0xf6, 0x01, 0xea, - 0xbc, 0xab, 0xe1, 0xfa, 0x08, 0xc3, 0x9c, 0x1d, 0x24, 0xa0, 0x9d, 0xaa, 0xd0, 0x9f, 0x9a, 0xa6, 0x72, 0x44, 0xaf, - 0xa7, 0x4d, 0x07, 0x95, 0xbb, 0x89, 0x2a, 0x13, 0xe0, 0xe0, 0x4d, 0x3c, 0x49, 0xe2, 0xb0, 0xdb, 0x4b, 0x4d, 0x53, - 0x3f, 0x99, 0xb8, 0xb2, 0x5a, 0x4d, 0xf9, 0x76, 0x6e, 0x95, 0xd0, 0xd2, 0xe3, 0x42, 0x88, 0x79, 0xbc, 0xe7, 0x81, - 0xeb, 0x65, 0xdf, 0xc8, 0x1a, 0x2c, 0xee, 0x9b, 0x95, 0x51, 0x95, 0xd3, 0x11, 0x1a, 0x93, 0x62, 0x9e, 0xfc, 0x05, - 0x88, 0xd1, 0xdd, 0x4e, 0xd3, 0xeb, 0x10, 0x42, 0x44, 0x37, 0xfb, 0xf6, 0x9e, 0x1a, 0xeb, 0x88, 0x3d, 0x21, 0x2c, - 0x73, 0xc3, 0x4b, 0x74, 0x0c, 0xdc, 0xf6, 0xae, 0x2c, 0xc9, 0x74, 0xf9, 0xdc, 0x17, 0x20, 0x7c, 0x1d, 0x30, 0x43, - 0x0a, 0x54, 0x4a, 0xec, 0x83, 0xcd, 0xf7, 0x91, 0xd0, 0x3c, 0x3d, 0x17, 0xb6, 0x11, 0x7a, 0xbe, 0xec, 0xb3, 0xf5, - 0x5b, 0x38, 0x62, 0x6b, 0xab, 0x60, 0x0f, 0x7b, 0xb9, 0x6e, 0x91, 0xd1, 0x3c, 0xf8, 0x85, 0xe9, 0x2c, 0x0b, 0x89, - 0x57, 0x1b, 0xf5, 0x0d, 0xeb, 0x1d, 0x5b, 0xfa, 0x4c, 0x66, 0x4d, 0x3c, 0x4c, 0xd6, 0xd3, 0xc8, 0xc3, 0xc9, 0xa9, - 0x3c, 0xc7, 0xe6, 0xa9, 0xb0, 0xc0, 0x1b, 0xba, 0x7a, 0x7a, 0xcb, 0xb8, 0xf7, 0xa6, 0x21, 0x79, 0x89, 0xcf, 0xce, - 0xa2, 0x05, 0xa0, 0x98, 0xa8, 0x9c, 0x5e, 0xbb, 0xc0, 0x09, 0xf6, 0x7a, 0x51, 0x41, 0x83, 0x63, 0xe4, 0xd8, 0x96, - 0xe0, 0xe9, 0x70, 0x26, 0x67, 0x9d, 0x0b, 0x48, 0x5f, 0x33, 0xa9, 0x39, 0x0b, 0x73, 0x4e, 0x4a, 0x11, 0xf8, 0xe8, - 0x51, 0x9c, 0xa3, 0x79, 0xba, 0x01, 0x04, 0x86, 0x8a, 0xf7, 0x5d, 0x60, 0x8f, 0x37, 0x1c, 0xa9, 0x8b, 0x1c, 0xac, - 0xe4, 0x3d, 0x31, 0xcc, 0x0a, 0xfd, 0xeb, 0xe7, 0x07, 0x2b, 0x85, 0x8a, 0x5c, 0x8e, 0x51, 0x88, 0x62, 0xf7, 0x8c, - 0x08, 0xcc, 0x4d, 0xa5, 0x3a, 0x08, 0xd4, 0xf2, 0x0f, 0xb6, 0x5f, 0x08, 0x57, 0x4a, 0x70, 0xeb, 0x41, 0x5d, 0x5a, - 0x42, 0xc6, 0x1e, 0xce, 0xea, 0x2d, 0xd2, 0x58, 0x40, 0xb0, 0xc7, 0x5c, 0x6b, 0x7a, 0x98, 0x03, 0xc9, 0xac, 0x06, - 0x18, 0x6d, 0x89, 0x20, 0xf5, 0x82, 0xc1, 0x2e, 0x15, 0xdd, 0xd7, 0x05, 0x45, 0xba, 0xcb, 0xa8, 0x31, 0x95, 0x56, - 0x72, 0x7c, 0x1e, 0x62, 0x7f, 0xad, 0xa9, 0x5a, 0xea, 0xab, 0xec, 0x6b, 0x72, 0xba, 0xbb, 0x5f, 0x6c, 0xfc, 0x48, - 0xf8, 0x79, 0xae, 0x98, 0x41, 0x95, 0x8c, 0xa3, 0x5d, 0xc2, 0xa4, 0xa1, 0x7a, 0xa5, 0x38, 0x6e, 0x2c, 0x37, 0x9e, - 0x6e, 0x5f, 0x74, 0xc6, 0x56, 0xe9, 0xbf, 0xbb, 0x05, 0x3e, 0x27, 0xdd, 0x6b, 0x32, 0x4f, 0x49, 0x6c, 0xf0, 0x43, - 0xf7, 0x20, 0x9d, 0x28, 0xcf, 0xfd, 0xcb, 0xf3, 0xe3, 0x39, 0x29, 0x62, 0xdb, 0x56, 0xe4, 0x95, 0x15, 0xa0, 0x1c, - 0xd2, 0x6e, 0x02, 0xea, 0x4b, 0x37, 0xea, 0x4d, 0xe4, 0x8d, 0x0d, 0xbc, 0x84, 0xd4, 0x1a, 0x28, 0x76, 0x61, 0xec, - 0xab, 0xd3, 0x51, 0x48, 0x93, 0x33, 0xd9, 0x43, 0x42, 0x31, 0x61, 0x80, 0xfe, 0x69, 0x71, 0x34, 0xa3, 0x82, 0xd6, - 0xfb, 0xbd, 0xa8, 0x8e, 0x65, 0xe7, 0x1a, 0x08, 0x99, 0xd9, 0x68, 0x96, 0xbf, 0xc8, 0xf0, 0xc6, 0x21, 0xf2, 0x55, - 0x66, 0x3a, 0x3a, 0xf0, 0xd7, 0x94, 0x3b, 0xa9, 0xc3, 0xc6, 0x55, 0x76, 0x24, 0x81, 0xff, 0x2e, 0x73, 0x22, 0x14, - 0xbe, 0x99, 0x2d, 0x0f, 0xe4, 0x6b, 0x5d, 0xf9, 0x5f, 0x33, 0xea, 0xb3, 0xc2, 0x1d, 0x6d, 0xcb, 0xd5, 0x8c, 0xc3, - 0xd9, 0x70, 0x20, 0xf3, 0xf1, 0x81, 0x0b, 0x5e, 0x79, 0xaa, 0xca, 0x7e, 0x13, 0x0e, 0xc9, 0x03, 0x7b, 0x36, 0x39, - 0x4a, 0x4b, 0x47, 0xed, 0x7f, 0xe5, 0xb4, 0xe8, 0x50, 0x34, 0x2c, 0x5a, 0x17, 0x05, 0xa2, 0x56, 0x1b, 0xcb, 0xcb, - 0x3c, 0x22, 0x41, 0xed, 0x8b, 0xc5, 0x43, 0x7b, 0xe0, 0xa3, 0x29, 0x46, 0xbe, 0xcf, 0x58, 0x07, 0x12, 0x7d, 0x7f, - 0x44, 0x90, 0x32, 0x50, 0x3a, 0x74, 0x06, 0xa5, 0x89, 0x29, 0x1e, 0x93, 0x3c, 0x67, 0xb1, 0xc2, 0x5e, 0xf2, 0x3a, - 0x2a, 0x07, 0x2b, 0x92, 0x7f, 0x8e, 0x08, 0x70, 0x14, 0x0c, 0x1e, 0x45, 0x9e, 0xfa, 0x25, 0xb8, 0xe5, 0xbe, 0x3f, - 0x60, 0x44, 0x56, 0xd2, 0x58, 0x5b, 0x8c, 0x5e, 0x88, 0x91, 0xf9, 0x08, 0x8e, 0xc7, 0xef, 0x9b, 0xa3, 0x14, 0x94, - 0xbe, 0xb4, 0x2b, 0x50, 0xdc, 0x04, 0xba, 0xb4, 0x9b, 0x1a, 0xa7, 0x81, 0x9c, 0xc8, 0xb4, 0xb5, 0x1d, 0xf7, 0xdd, - 0xd9, 0xb1, 0xa0, 0x2d, 0x41, 0xc6, 0x74, 0x17, 0x9a, 0x39, 0x0a, 0x0c, 0xef, 0xb7, 0x1a, 0x47, 0xc0, 0x80, 0x5d, - 0x63, 0x3d, 0xfc, 0x52, 0x4c, 0xfb, 0x54, 0xe9, 0x87, 0x2b, 0x9c, 0xb3, 0x4b, 0x3a, 0xbd, 0xf9, 0xfd, 0x40, 0x06, - 0xc4, 0xc5, 0x1b, 0x31, 0xf5, 0x05, 0xcc, 0x2f, 0x83, 0x02, 0x10, 0xa6, 0x12, 0x58, 0xfa, 0xbf, 0x98, 0x0b, 0xbc, - 0x13, 0x87, 0x35, 0x83, 0x03, 0x83, 0x88, 0x8f, 0x3b, 0xb8, 0xc5, 0x5f, 0x87, 0xff, 0x68, 0x80, 0xba, 0x72, 0xf7, - 0x19, 0x65, 0xcd, 0xf7, 0x49, 0x29, 0x32, 0x7d, 0xf9, 0xee, 0x65, 0x2b, 0xd4, 0x41, 0x8e, 0x6d, 0x6e, 0x55, 0xf3, - 0xda, 0xe2, 0xf7, 0xd3, 0x58, 0xcd, 0x4d, 0x7e, 0xd3, 0xdb, 0x55, 0x57, 0x4f, 0x8d, 0x1a, 0xf5, 0x84, 0x60, 0xf4, - 0xe6, 0x66, 0xd8, 0xad, 0xf1, 0xcb, 0x59, 0x09, 0x68, 0x64, 0xb3, 0x57, 0xbf, 0x47, 0x41, 0xae, 0xaf, 0xf5, 0xf3, - 0xbc, 0xac, 0x32, 0x2e, 0xbe, 0x09, 0xc0, 0x53, 0xe3, 0x43, 0xa2, 0x4a, 0xb5, 0x2c, 0x0d, 0x51, 0x93, 0x00, 0x82, - 0xc3, 0x1f, 0x74, 0x0b, 0x2e, 0xed, 0x57, 0x72, 0x9b, 0x55, 0x79, 0x6d, 0x45, 0xd0, 0x81, 0x45, 0x9f, 0xae, 0x0c, - 0x76, 0x30, 0xe0, 0xe1, 0x14, 0xfd, 0x43, 0xf1, 0x87, 0x89, 0xed, 0x9d, 0x6d, 0x4a, 0x28, 0x1f, 0x9a, 0xb9, 0x17, - 0x77, 0xf6, 0x4c, 0x11, 0xcd, 0x22, 0xd4, 0xac, 0x82, 0x19, 0x2c, 0x1b, 0x6a, 0xe7, 0x1a, 0x12, 0xf6, 0x08, 0x52, - 0x4c, 0xc1, 0xb8, 0xd1, 0xde, 0x90, 0xee, 0x88, 0x6b, 0x06, 0xe5, 0xb0, 0x50, 0x94, 0xf9, 0xcd, 0x08, 0xc9, 0x38, - 0xa7, 0xe9, 0x0d, 0x0a, 0xfc, 0x30, 0xe0, 0x73, 0x79, 0xb0, 0x20, 0xcf, 0x1f, 0x55, 0xe9, 0x74, 0x14, 0xfb, 0x56, - 0x12, 0x31, 0x63, 0xda, 0x81, 0x2d, 0xaf, 0xf7, 0xca, 0x74, 0x61, 0xb3, 0x4f, 0x3a, 0xd6, 0x1d, 0xe2, 0xed, 0x29, - 0x71, 0x1d, 0xc4, 0x9e, 0xa6, 0x1c, 0x36, 0x79, 0x3d, 0x99, 0xe3, 0x68, 0x51, 0x76, 0x5d, 0xac, 0xa6, 0x33, 0x14, - 0x7a, 0x0b, 0xc9, 0x46, 0x1b, 0x7a, 0xfe, 0xa4, 0x72, 0x8c, 0xf3, 0xc3, 0xe5, 0x24, 0x86, 0xe9, 0x4b, 0xa9, 0x21, - 0x5a, 0xb7, 0x94, 0xee, 0xb1, 0xbe, 0x63, 0x05, 0x5b, 0xb3, 0xf7, 0x8f, 0x44, 0x96, 0x26, 0x96, 0xa9, 0xd4, 0x96, - 0x9d, 0xba, 0x71, 0xef, 0x59, 0x7b, 0xfc, 0xde, 0x62, 0xb1, 0x46, 0xea, 0x6c, 0xb4, 0x31, 0xcd, 0x40, 0x3e, 0x1a, - 0xda, 0x07, 0x5f, 0x30, 0x65, 0x0b, 0xfd, 0x70, 0xde, 0x6d, 0xd0, 0x16, 0xe3, 0x33, 0x86, 0xa6, 0xd9, 0x9d, 0x0f, - 0xbc, 0xfa, 0x2c, 0x8b, 0x2e, 0x17, 0x1d, 0x4f, 0x73, 0x8c, 0x18, 0x75, 0xff, 0x5f, 0x1e, 0xf6, 0x52, 0x86, 0xbb, - 0x3c, 0x21, 0xc3, 0x4e, 0xee, 0xdb, 0x29, 0xab, 0x80, 0x7c, 0x8c, 0xad, 0xf4, 0xbc, 0x72, 0x30, 0xa2, 0xd2, 0x51, - 0x9c, 0xe9, 0x3f, 0x7c, 0xe5, 0xd7, 0x32, 0x8d, 0xda, 0xf4, 0xa3, 0xcb, 0x92, 0xbf, 0xb2, 0x1a, 0x88, 0x36, 0x4f, - 0x88, 0x4c, 0xfe, 0x4f, 0x24, 0x25, 0x47, 0x06, 0xe2, 0xd1, 0x01, 0x14, 0x30, 0x53, 0x27, 0x93, 0xd3, 0x62, 0x70, - 0x02, 0x22, 0x4b, 0x34, 0x87, 0x73, 0x00, 0x93, 0xb4, 0x04, 0x13, 0x1e, 0xd7, 0x6a, 0xdf, 0x63, 0xc6, 0x01, 0x7f, - 0x99, 0x47, 0x73, 0x70, 0xf7, 0x01, 0x2d, 0x9a, 0x80, 0x64, 0x24, 0x61, 0x58, 0x6b, 0xdb, 0x79, 0x38, 0xd9, 0x4e, - 0xf0, 0xac, 0x7a, 0x7d, 0xc0, 0x8f, 0xb5, 0x82, 0xcb, 0x9d, 0x28, 0x45, 0x75, 0x1f, 0x7c, 0xd9, 0xea, 0xcd, 0x21, - 0xd4, 0x59, 0x0f, 0xf5, 0xcc, 0x40, 0x71, 0xdb, 0xce, 0x66, 0x54, 0xf3, 0x05, 0xff, 0xf8, 0xcb, 0xc2, 0x50, 0x2c, - 0x9a, 0x35, 0x64, 0xc0, 0x00, 0xdc, 0xc6, 0x9c, 0xef, 0x75, 0xfc, 0x97, 0x4f, 0x90, 0xb0, 0x17, 0x11, 0xf6, 0x26, - 0xc5, 0x28, 0xe1, 0x97, 0x13, 0x06, 0x04, 0xf1, 0xda, 0x13, 0x25, 0x88, 0xf4, 0xa0, 0x3e, 0x99, 0x66, 0x5c, 0x66, - 0x33, 0x48, 0xd6, 0xb0, 0x08, 0xba, 0xdd, 0x35, 0xeb, 0x32, 0xe3, 0x4f, 0x7e, 0xc8, 0x70, 0x0d, 0xf4, 0x4f, 0x26, - 0x4a, 0x3a, 0x37, 0x24, 0xa8, 0xf8, 0x20, 0x5e, 0xe6, 0x50, 0x79, 0xde, 0x33, 0xe4, 0xe9, 0xf9, 0x47, 0x7f, 0xdf, - 0xcc, 0x1c, 0xca, 0x53, 0xd6, 0xe4, 0xef, 0x9e, 0xea, 0xfe, 0xa7, 0xc8, 0x2b, 0x3a, 0xf3, 0xd5, 0xac, 0xb3, 0xe2, - 0x3a, 0xe3, 0xec, 0x88, 0x54, 0x70, 0x6a, 0x45, 0xeb, 0x1d, 0x0f, 0xb1, 0x69, 0xfc, 0xb5, 0x40, 0xea, 0xec, 0x91, - 0xb9, 0x67, 0x07, 0x15, 0xa3, 0x25, 0x14, 0x58, 0x2f, 0xa2, 0x06, 0xbe, 0x1d, 0xb5, 0x19, 0x33, 0x7d, 0x4e, 0x0a, - 0xb4, 0x68, 0x09, 0x36, 0x6d, 0x17, 0xa3, 0x26, 0x5e, 0x96, 0xcc, 0x15, 0x27, 0xfc, 0xe9, 0x32, 0x53, 0xec, 0x87, - 0x8c, 0xd4, 0xc1, 0x9e, 0x17, 0x2b, 0x96, 0x2c, 0x97, 0x4f, 0xd7, 0x0f, 0xc9, 0x2e, 0xf7, 0x1e, 0x11, 0x33, 0x5e, - 0x3f, 0x5e, 0xb2, 0x4b, 0x09, 0x28, 0x91, 0x91, 0x0d, 0xe3, 0x36, 0x12, 0x6a, 0x14, 0x15, 0xa3, 0x2b, 0x50, 0x72, - 0xac, 0x53, 0x11, 0x00, 0xf0, 0xc7, 0xf4, 0x52, 0xd8, 0xc0, 0x83, 0xd3, 0x89, 0x02, 0x94, 0x91, 0xa7, 0xef, 0x4c, - 0xc6, 0x82, 0xe8, 0xa8, 0x99, 0xc3, 0xef, 0x84, 0xb1, 0x7a, 0xe6, 0xde, 0xeb, 0xa3, 0x48, 0xb0, 0x7b, 0xd9, 0x08, - 0x03, 0x89, 0x65, 0xd9, 0x64, 0x1c, 0xb6, 0x6e, 0x2b, 0xfc, 0xb4, 0x58, 0x81, 0x34, 0x05, 0x68, 0xde, 0xd3, 0x46, - 0xc0, 0x69, 0x18, 0xb3, 0x2f, 0x13, 0x48, 0xa9, 0x82, 0xb1, 0xfc, 0xa4, 0x64, 0xc3, 0xb3, 0x49, 0xde, 0xfd, 0xc4, - 0xd3, 0x5c, 0x20, 0xe4, 0xc5, 0x02, 0xdb, 0x9a, 0xa9, 0x13, 0xbf, 0x19, 0xe5, 0x66, 0x3f, 0x56, 0xcd, 0xa2, 0x0d, - 0x47, 0x1e, 0x95, 0xe5, 0xa6, 0x1b, 0xdd, 0xda, 0x2d, 0x58, 0xb5, 0x10, 0xa9, 0xe6, 0x78, 0x19, 0x80, 0x8d, 0xe8, - 0x97, 0x94, 0x61, 0xf5, 0x83, 0x4e, 0x81, 0x64, 0x61, 0xc8, 0xb6, 0xcd, 0x92, 0x32, 0x18, 0x82, 0xf2, 0xa8, 0x9a, - 0x02, 0xac, 0x91, 0xf2, 0x4d, 0x0a, 0xa3, 0xc9, 0xbf, 0x6a, 0x8b, 0xfe, 0x93, 0xff, 0x29, 0xd6, 0x7b, 0x26, 0x88, - 0x64, 0x7b, 0x38, 0x9f, 0x9d, 0xa6, 0x05, 0x33, 0x68, 0x14, 0x84, 0xf6, 0x60, 0x4a, 0xcd, 0x49, 0x24, 0x06, 0x25, - 0x17, 0x22, 0xfb, 0x93, 0xea, 0x2d, 0xc7, 0x47, 0x1e, 0xb2, 0xaf, 0x6f, 0x92, 0x26, 0x9d, 0x56, 0xa7, 0xca, 0x08, - 0xee, 0x0a, 0x9c, 0xa0, 0x04, 0xb3, 0x01, 0xfd, 0x93, 0x9f, 0x3f, 0x85, 0x24, 0xfa, 0xd0, 0x05, 0x84, 0x52, 0x67, - 0xcf, 0x88, 0xdc, 0x2c, 0x3c, 0xa2, 0x55, 0x88, 0x62, 0x5c, 0x20, 0x07, 0xc8, 0xfc, 0xb7, 0x91, 0x05, 0xbd, 0x86, - 0xfd, 0x42, 0x37, 0xa2, 0x7d, 0x08, 0x8b, 0x11, 0x9b, 0x2b, 0xde, 0xe8, 0x3d, 0x90, 0x67, 0x88, 0x1b, 0xf7, 0x34, - 0x2e, 0x68, 0xe9, 0x2a, 0x9b, 0x95, 0x02, 0xdd, 0xc4, 0xa3, 0x3e, 0x09, 0x1d, 0xb5, 0x5a, 0xde, 0x0c, 0xd1, 0x3b, - 0xd0, 0xf3, 0x7a, 0xff, 0x04, 0xdf, 0x0e, 0x08, 0x10, 0x51, 0xb8, 0xa3, 0x33, 0xf9, 0xc1, 0xe1, 0x77, 0xae, 0x3c, - 0xff, 0xc8, 0x44, 0x3d, 0x52, 0x99, 0xef, 0x96, 0xf8, 0xbb, 0x5b, 0xde, 0xff, 0xa1, 0x29, 0x53, 0x82, 0xf2, 0x83, - 0x60, 0x60, 0x64, 0x85, 0x0f, 0xae, 0x9d, 0x0e, 0xbf, 0x91, 0x79, 0x89, 0xe2, 0x85, 0xe4, 0xb2, 0x85, 0xdb, 0x2b, - 0xc6, 0x55, 0xac, 0xe2, 0xca, 0x0e, 0xda, 0x67, 0xdc, 0x7a, 0xfc, 0x10, 0x35, 0xc6, 0x1a, 0x47, 0xe7, 0x1c, 0x94, - 0x06, 0x84, 0x04, 0xd3, 0xc0, 0x26, 0x3d, 0x5a, 0x60, 0x99, 0x16, 0x48, 0x09, 0x42, 0x48, 0x2a, 0xba, 0x1f, 0x43, - 0x53, 0x89, 0xcd, 0x8c, 0x20, 0xad, 0x2a, 0x76, 0xa8, 0xc4, 0x29, 0x67, 0x1f, 0xa6, 0x58, 0x23, 0x7c, 0xaa, 0xe9, - 0x3b, 0x88, 0x92, 0xc8, 0x7b, 0x4e, 0x2e, 0x2e, 0x1d, 0x68, 0x45, 0xa6, 0x4a, 0x49, 0xdf, 0x79, 0xc1, 0xad, 0xbf, - 0xd6, 0x3e, 0x20, 0xd6, 0x41, 0x15, 0xf4, 0xac, 0x8a, 0xbf, 0xdc, 0x62, 0xae, 0xa4, 0x25, 0x56, 0xb1, 0xa7, 0x2c, - 0xf6, 0x73, 0x5a, 0x71, 0x1e, 0xce, 0x69, 0xe8, 0x2e, 0x39, 0x77, 0xa9, 0xb8, 0x27, 0x9d, 0xf5, 0x12, 0xe1, 0xbe, - 0x65, 0x07, 0xd3, 0x67, 0x25, 0xfc, 0xf8, 0xbb, 0x39, 0x29, 0x29, 0xd7, 0x81, 0x46, 0xcf, 0x61, 0xe0, 0x65, 0xd0, - 0xa2, 0xee, 0xd4, 0xc0, 0x3d, 0x91, 0xe8, 0x5b, 0x7f, 0x60, 0xc6, 0x66, 0xd9, 0x12, 0x19, 0x14, 0xcf, 0xd4, 0xff, - 0x24, 0x48, 0xe2, 0xb1, 0xfc, 0x23, 0x2f, 0x0e, 0x49, 0x22, 0xa9, 0x3e, 0x80, 0x3e, 0x09, 0x9e, 0x58, 0x80, 0x57, - 0x7f, 0xa0, 0x80, 0xa1, 0x28, 0x57, 0x39, 0x32, 0x77, 0xc2, 0x1c, 0xf2, 0x74, 0x57, 0xbd, 0x53, 0x07, 0x38, 0x7d, - 0xb5, 0x9e, 0x4d, 0x40, 0xa7, 0x85, 0x1e, 0xa0, 0xc4, 0x99, 0x11, 0xa5, 0x19, 0x07, 0xa7, 0x86, 0x39, 0xfc, 0xaf, - 0x57, 0x12, 0x61, 0xec, 0xc1, 0xc3, 0x41, 0xe3, 0x41, 0x05, 0xf9, 0xd9, 0x8e, 0xa6, 0x34, 0x0c, 0x48, 0xc2, 0xb9, - 0x16, 0xab, 0x64, 0x19, 0x5e, 0x3c, 0xf2, 0xca, 0x0c, 0xe1, 0x04, 0xd6, 0x9d, 0x3e, 0x95, 0x0e, 0x82, 0x71, 0x09, - 0x17, 0x2a, 0xaf, 0x39, 0x35, 0x1c, 0x69, 0xb9, 0x40, 0xf1, 0x57, 0x9a, 0xa8, 0x6b, 0x11, 0x4f, 0xe6, 0x47, 0x5c, - 0x35, 0x10, 0x61, 0xda, 0x05, 0x01, 0x96, 0x97, 0xc8, 0xad, 0x85, 0x72, 0xed, 0xb7, 0x1e, 0x36, 0x30, 0x06, 0xeb, - 0xe6, 0xd7, 0x4b, 0x7e, 0x7d, 0xd3, 0xb4, 0xf6, 0xe2, 0x2d, 0x2a, 0x34, 0x9d, 0xe8, 0xe9, 0x90, 0x22, 0x3c, 0x1d, - 0x77, 0x11, 0x19, 0x46, 0x03, 0x4c, 0xdf, 0x56, 0xd5, 0x62, 0x26, 0xed, 0x00, 0xfa, 0xb9, 0x20, 0xcd, 0x01, 0xa0, - 0x29, 0x42, 0xd9, 0x01, 0x70, 0x15, 0xaa, 0xf5, 0xba, 0x5f, 0x69, 0x63, 0x63, 0x3c, 0xe0, 0x11, 0x81, 0x59, 0xf1, - 0x94, 0x42, 0xc9, 0x79, 0x02, 0x79, 0xb1, 0x4d, 0x55, 0xba, 0x99, 0x96, 0xcd, 0xfa, 0xdd, 0xfa, 0x47, 0x96, 0x00, - 0xa2, 0x26, 0x79, 0x64, 0x32, 0x81, 0x0d, 0x15, 0xd2, 0x14, 0xc7, 0xa4, 0x56, 0x02, 0xae, 0xf9, 0xb0, 0x8f, 0x6c, - 0x09, 0x38, 0x3b, 0x70, 0x2d, 0x88, 0xc3, 0x59, 0x33, 0x64, 0xb2, 0x3c, 0xa7, 0xad, 0xd1, 0x3f, 0x5b, 0xad, 0xb1, - 0xb5, 0xff, 0x43, 0x4b, 0x71, 0x3f, 0x19, 0x0b, 0x4d, 0x0c, 0x48, 0x6d, 0x8f, 0xbf, 0xbb, 0x95, 0x74, 0xe6, 0x6d, - 0xc1, 0x49, 0xff, 0x37, 0xd3, 0xe6, 0x74, 0x9e, 0x3d, 0x39, 0x8c, 0x7c, 0xc0, 0x98, 0x0a, 0x61, 0x8c, 0x93, 0xf0, - 0x62, 0x3b, 0xbc, 0x68, 0x0c, 0x6a, 0xff, 0xe5, 0x0e, 0x86, 0x9c, 0xea, 0xd8, 0x7b, 0x1f, 0x44, 0xc9, 0xbe, 0x98, - 0x5b, 0x34, 0x56, 0x87, 0xb4, 0x28, 0x6e, 0xfb, 0x00, 0x32, 0xf0, 0xd2, 0xfd, 0xff, 0xb8, 0x75, 0x88, 0x63, 0xb0, - 0x09, 0x79, 0x89, 0x4b, 0x12, 0xb3, 0x4d, 0x1f, 0x05, 0xf5, 0xfa, 0xb4, 0x11, 0x2e, 0xd1, 0x5c, 0xe9, 0xfe, 0x07, - 0x2f, 0x5b, 0x54, 0x77, 0x29, 0x0f, 0xf7, 0x0e, 0x8c, 0x69, 0x7c, 0x73, 0xf3, 0x3d, 0x0d, 0xa6, 0x14, 0xba, 0x19, - 0xef, 0x60, 0x13, 0xbb, 0xde, 0x56, 0x56, 0x6c, 0x17, 0x99, 0xa2, 0xa2, 0xa9, 0xd1, 0x47, 0x33, 0xd8, 0xec, 0xd0, - 0x80, 0xf6, 0x6f, 0x31, 0xc9, 0x60, 0xf1, 0x70, 0x6b, 0x2e, 0x44, 0xcb, 0xeb, 0x9c, 0xed, 0x28, 0x38, 0x27, 0x23, - 0x8e, 0x24, 0x48, 0x93, 0xee, 0x3b, 0x8e, 0x1e, 0xd4, 0x41, 0xd5, 0x88, 0x3b, 0x6d, 0xc9, 0x7e, 0x45, 0x7d, 0x97, - 0x3e, 0xae, 0x0b, 0x79, 0xe5, 0x1c, 0x48, 0xc4, 0x67, 0x85, 0x37, 0x27, 0x44, 0x46, 0x6d, 0x1b, 0xa9, 0x15, 0x59, - 0x91, 0x5f, 0x21, 0x25, 0xea, 0x5f, 0x51, 0x2b, 0xc8, 0x62, 0x0e, 0xc0, 0xc0, 0x36, 0x00, 0xab, 0xdf, 0xac, 0x18, - 0xb2, 0xa5, 0x80, 0xc6, 0x2f, 0x67, 0xdb, 0x7c, 0xe2, 0x96, 0xec, 0xe8, 0x17, 0x44, 0x6d, 0x6b, 0x45, 0x13, 0x9c, - 0x77, 0x2f, 0xac, 0x9e, 0x89, 0xdf, 0x53, 0xcf, 0xb7, 0xc0, 0x36, 0x90, 0x4f, 0xd2, 0xfd, 0xce, 0x99, 0x3e, 0x60, - 0x0f, 0xc6, 0x58, 0xc7, 0x60, 0x57, 0xd8, 0x63, 0xa3, 0x37, 0x55, 0xe5, 0x39, 0x68, 0x57, 0xb7, 0x1c, 0x15, 0xf1, - 0xf8, 0x2d, 0xcb, 0x3a, 0x18, 0x66, 0x18, 0x3d, 0xf3, 0x05, 0x94, 0x2d, 0xda, 0x11, 0x99, 0x93, 0x5c, 0x46, 0xdb, - 0x54, 0x0e, 0x28, 0x81, 0x05, 0x31, 0xa9, 0x71, 0x4a, 0xdd, 0x2d, 0x9b, 0x97, 0xae, 0xa3, 0x09, 0xf1, 0xd6, 0x5f, - 0x67, 0x3e, 0xd7, 0x83, 0xa3, 0xf2, 0x3c, 0x44, 0x60, 0x1a, 0xc8, 0xc3, 0x02, 0x0e, 0x23, 0x79, 0x5e, 0x8a, 0x40, - 0x01, 0xef, 0x06, 0x7d, 0xb6, 0x19, 0x28, 0x72, 0x0a, 0x91, 0x77, 0x9e, 0x83, 0x05, 0xba, 0xc1, 0x53, 0x44, 0x19, - 0x87, 0x87, 0xff, 0x2e, 0x70, 0x19, 0x1e, 0x92, 0x25, 0x8c, 0xef, 0x1d, 0x4e, 0x24, 0x27, 0xa9, 0x8b, 0xa4, 0xf5, - 0x4b, 0x78, 0xa6, 0xb6, 0x71, 0x6b, 0xfe, 0x22, 0xfb, 0x24, 0x76, 0xaf, 0xbc, 0x80, 0xf9, 0x18, 0x35, 0xd9, 0x65, - 0xfe, 0xc2, 0x3c, 0x26, 0x3d, 0x33, 0xaf, 0xd1, 0x6a, 0x0d, 0x78, 0x20, 0x69, 0x45, 0x58, 0xca, 0x2c, 0x99, 0x73, - 0x19, 0x00, 0xe8, 0xda, 0x78, 0xd8, 0x1a, 0x42, 0x7c, 0x22, 0xd7, 0x77, 0x45, 0x42, 0x65, 0xaa, 0x59, 0x96, 0x23, - 0xf7, 0xc9, 0x4d, 0x08, 0x4b, 0xb5, 0xcb, 0x12, 0xb7, 0x99, 0xe6, 0xb6, 0x36, 0x3c, 0xf7, 0xca, 0xf2, 0xbd, 0xc0, - 0x14, 0xf5, 0xa0, 0xbf, 0xb3, 0x8d, 0x38, 0x45, 0x10, 0x22, 0x66, 0x70, 0x87, 0xa3, 0x11, 0x64, 0x53, 0x4e, 0xf4, - 0x67, 0xbb, 0xc4, 0xe6, 0xa7, 0x97, 0xa9, 0xaa, 0x70, 0x39, 0x62, 0x32, 0xb1, 0x39, 0x1b, 0xb0, 0x98, 0x83, 0x57, - 0x7b, 0x72, 0x9b, 0xdb, 0xb2, 0xec, 0x8d, 0x08, 0x56, 0x83, 0x16, 0xce, 0x1d, 0x2c, 0x15, 0xfa, 0x4e, 0x66, 0xbd, - 0xab, 0x83, 0x9b, 0xd9, 0x6f, 0xd2, 0xee, 0x8f, 0x1c, 0x7d, 0x55, 0x69, 0xdc, 0x81, 0x6d, 0x2c, 0x81, 0x0d, 0x8f, - 0x11, 0x29, 0x87, 0x44, 0xf5, 0xa9, 0x4f, 0x6c, 0x1e, 0xd5, 0x98, 0xe4, 0x38, 0xc8, 0x1d, 0x26, 0xae, 0x68, 0x3a, - 0x9b, 0xb4, 0x10, 0xbb, 0x52, 0x21, 0x3d, 0x9d, 0x85, 0xfc, 0x16, 0x73, 0xd3, 0x75, 0x92, 0xc8, 0xd6, 0xb5, 0x0f, - 0xf9, 0xa2, 0x25, 0x75, 0x68, 0x60, 0xa7, 0xc7, 0x1c, 0xfd, 0x78, 0xb5, 0x95, 0xaf, 0xd4, 0xd6, 0x71, 0x4e, 0x92, - 0x8f, 0x71, 0xbc, 0x68, 0xf8, 0xe7, 0xa2, 0xa2, 0xd1, 0xc2, 0x93, 0xd8, 0xfa, 0x61, 0x27, 0xaf, 0x5f, 0xd1, 0x62, - 0x36, 0x1c, 0xb5, 0x5e, 0x96, 0x57, 0x1c, 0xee, 0xdd, 0xb6, 0x14, 0x4b, 0x58, 0x1f, 0xe3, 0x72, 0xc9, 0xd3, 0xa8, - 0x5a, 0x3a, 0xfa, 0xcb, 0x1b, 0xb8, 0x25, 0xef, 0x04, 0xc0, 0x44, 0x52, 0x1f, 0x61, 0x41, 0x7b, 0x19, 0x31, 0x42, - 0xec, 0x05, 0xd9, 0x27, 0x68, 0x7b, 0xb1, 0xaf, 0x76, 0x3d, 0x0c, 0xd9, 0x92, 0x64, 0x77, 0x6f, 0x46, 0xf8, 0x42, - 0xdd, 0x3d, 0xb2, 0x1a, 0x87, 0x6b, 0xf2, 0xe2, 0x32, 0x44, 0xb1, 0x97, 0x70, 0xc3, 0xa8, 0x2d, 0xc5, 0xdc, 0x82, - 0x1b, 0x49, 0x8b, 0x89, 0xad, 0x51, 0x46, 0x0d, 0x9b, 0x43, 0x9b, 0x43, 0x69, 0xef, 0x15, 0xdf, 0xf0, 0xdf, 0x10, - 0xef, 0x7c, 0x69, 0x4b, 0x12, 0x75, 0xef, 0x42, 0x5a, 0xe6, 0x45, 0xba, 0x92, 0xf5, 0xf3, 0x76, 0x62, 0x43, 0x71, - 0x37, 0xc7, 0x80, 0xf5, 0xc4, 0x41, 0x76, 0x69, 0xf2, 0x81, 0xc4, 0x26, 0x4a, 0x56, 0x5a, 0xfd, 0xcf, 0xee, 0xfa, - 0xb0, 0xe0, 0xa1, 0x89, 0x46, 0xc7, 0xb6, 0x43, 0x37, 0x62, 0x1e, 0x7e, 0x8d, 0x67, 0xaa, 0x16, 0x90, 0x1c, 0xe6, - 0x26, 0x51, 0xea, 0x66, 0x84, 0xea, 0xc4, 0x8d, 0x17, 0x88, 0x7a, 0xda, 0xf5, 0x4c, 0xc7, 0xd2, 0xfb, 0xbb, 0x0c, - 0xa1, 0xa9, 0x21, 0x04, 0x0f, 0x21, 0x39, 0x3f, 0x09, 0x6f, 0x46, 0x27, 0xe2, 0x1b, 0xa6, 0xcb, 0x19, 0x72, 0x0f, - 0x5f, 0xa0, 0x75, 0x27, 0xc1, 0xc2, 0xe1, 0x86, 0x90, 0x22, 0x15, 0x04, 0xc8, 0xf6, 0x31, 0x80, 0x85, 0x46, 0xf6, - 0xa2, 0xc9, 0xd4, 0x80, 0xc8, 0x66, 0x6d, 0x4b, 0x98, 0x63, 0x33, 0x35, 0x68, 0xc1, 0xd6, 0xfc, 0x12, 0x28, 0x1b, - 0xda, 0xe2, 0x2d, 0xfd, 0x4f, 0x5e, 0x13, 0x41, 0x8c, 0x69, 0x6a, 0xd3, 0xcc, 0x7a, 0xe5, 0xda, 0xde, 0xf5, 0x29, - 0x16, 0x0b, 0xe4, 0xc0, 0x75, 0x43, 0x69, 0x6c, 0x8d, 0xd5, 0x25, 0x0d, 0x68, 0xb9, 0xa8, 0x2e, 0x08, 0x84, 0xc4, - 0x10, 0xf3, 0xaa, 0xa1, 0x90, 0x92, 0x84, 0x6a, 0x6e, 0xdd, 0x89, 0x6d, 0x82, 0xc2, 0xec, 0xb8, 0x33, 0x79, 0xe8, - 0xe7, 0x70, 0xfe, 0xfe, 0xc6, 0x2c, 0x40, 0x51, 0xb8, 0xe2, 0xa5, 0x8c, 0x06, 0x89, 0x7e, 0xb3, 0x1e, 0x7a, 0xfe, - 0x83, 0x03, 0xda, 0x9d, 0xca, 0x32, 0xa3, 0xd4, 0xa9, 0x9e, 0x09, 0x4e, 0x6f, 0x0d, 0xd0, 0x88, 0x48, 0x80, 0x09, - 0xfc, 0xa8, 0x3f, 0x0a, 0x15, 0x0b, 0x98, 0xb5, 0x95, 0x53, 0xaf, 0xef, 0x31, 0x10, 0x29, 0xec, 0xb0, 0x71, 0xce, - 0xa2, 0x55, 0x8d, 0x78, 0x42, 0x82, 0x3e, 0x48, 0xc8, 0xce, 0x59, 0xf5, 0x8c, 0xaf, 0x93, 0x0b, 0xbe, 0x60, 0x77, - 0xfc, 0xb5, 0x06, 0x50, 0x8e, 0x7f, 0xb1, 0xf7, 0x86, 0xd7, 0xc3, 0x16, 0xd7, 0x23, 0xe6, 0x8b, 0x32, 0x2f, 0x7f, - 0x78, 0xd0, 0x72, 0xfa, 0xf7, 0xe7, 0x69, 0x80, 0x2a, 0x7f, 0xb1, 0x84, 0x01, 0xa9, 0x3c, 0xbc, 0xf5, 0x46, 0xe4, - 0x4a, 0x66, 0x14, 0x8d, 0x59, 0x3b, 0x6e, 0x09, 0x3b, 0xb8, 0x28, 0x8e, 0x20, 0x54, 0xfc, 0xf3, 0x16, 0x40, 0x62, - 0x2b, 0x68, 0x99, 0xd1, 0xa0, 0x11, 0xed, 0x81, 0x3a, 0x2b, 0x6c, 0xcc, 0x0b, 0xb6, 0x2e, 0x5f, 0xde, 0xad, 0xe0, - 0x20, 0x4b, 0x48, 0x82, 0x87, 0xf5, 0xf6, 0xcd, 0x26, 0xd3, 0xa5, 0x87, 0xa9, 0xd7, 0x1d, 0xbf, 0x67, 0x56, 0x20, - 0xa4, 0xd9, 0x43, 0x64, 0x6d, 0x37, 0x12, 0xd3, 0x1b, 0x4f, 0x6d, 0x3b, 0x62, 0x5e, 0xb7, 0x13, 0x91, 0x2b, 0x75, - 0x6c, 0x9b, 0x87, 0xc8, 0x08, 0x2b, 0x8c, 0x24, 0xb8, 0xfc, 0x32, 0x20, 0x36, 0x51, 0xd0, 0xd8, 0xc7, 0xe2, 0x52, - 0x16, 0x93, 0xec, 0xa3, 0xf8, 0x4b, 0x59, 0xeb, 0x5f, 0x22, 0xd5, 0xd9, 0x13, 0xf8, 0x15, 0x43, 0x7b, 0x0f, 0xa1, - 0xb1, 0x4e, 0x83, 0xbb, 0x16, 0x3c, 0xb2, 0x80, 0x72, 0x1f, 0x1a, 0x12, 0x42, 0x71, 0xba, 0x1d, 0x16, 0xd9, 0xae, - 0x25, 0x46, 0x80, 0x8f, 0x92, 0x5e, 0xa9, 0x4d, 0xc6, 0x70, 0x05, 0x05, 0x70, 0x79, 0xae, 0xc7, 0xf3, 0xd1, 0xcd, - 0xf6, 0x4a, 0x23, 0x09, 0x7d, 0x37, 0xac, 0x78, 0xb9, 0xb9, 0xee, 0x2a, 0x8b, 0x36, 0x9f, 0x62, 0x1c, 0xeb, 0x02, - 0x91, 0x19, 0x21, 0x62, 0x6e, 0xd9, 0xa0, 0x20, 0x1d, 0x6c, 0x17, 0x03, 0xf4, 0xb1, 0x81, 0xe1, 0x0c, 0x56, 0xba, - 0xaa, 0xad, 0x9d, 0xa7, 0xc8, 0xf4, 0x6f, 0xb6, 0x98, 0xc0, 0xcf, 0x17, 0x17, 0x24, 0x04, 0x24, 0x2c, 0xf4, 0xcc, - 0x83, 0x59, 0x0f, 0x27, 0x79, 0xf6, 0x12, 0x13, 0x2e, 0x64, 0xa8, 0x70, 0xfc, 0xa0, 0xad, 0xe6, 0x82, 0xe6, 0xf8, - 0xf5, 0x4c, 0x5b, 0x95, 0xaf, 0x95, 0x34, 0xc9, 0x82, 0x43, 0x5e, 0x38, 0x5d, 0xde, 0x32, 0x44, 0xf1, 0xa9, 0x76, - 0xdd, 0x77, 0xb8, 0xf9, 0x4c, 0x8a, 0x9c, 0x54, 0xda, 0x89, 0x40, 0xa5, 0x21, 0x93, 0xb7, 0x7b, 0x01, 0xb0, 0x6d, - 0x88, 0xbe, 0x68, 0x36, 0x32, 0x53, 0x99, 0x8e, 0xae, 0x96, 0x87, 0x70, 0x6c, 0x0f, 0x6f, 0x06, 0xc3, 0x10, 0xf0, - 0xfa, 0xb4, 0x66, 0xff, 0xba, 0x67, 0x25, 0x55, 0x15, 0x4d, 0x8c, 0x8a, 0xb8, 0xb9, 0x60, 0x72, 0x0f, 0x2a, 0xa6, - 0xc1, 0x43, 0x38, 0x69, 0xc0, 0xe9, 0x38, 0x53, 0xd9, 0x20, 0x79, 0x81, 0x49, 0x10, 0x7b, 0x02, 0x2d, 0x4d, 0xc0, - 0xbc, 0xa2, 0xec, 0x38, 0xda, 0x8c, 0xed, 0x88, 0x50, 0xce, 0x9c, 0x44, 0x45, 0xfc, 0x98, 0x7b, 0xd2, 0x0a, 0xb0, - 0xcf, 0x40, 0x77, 0xbd, 0xc6, 0x4f, 0x6a, 0x41, 0xd1, 0xb7, 0xb6, 0xff, 0x5f, 0x86, 0x41, 0xd8, 0x9e, 0xb6, 0x73, - 0x00, 0x0a, 0xb2, 0x84, 0x00, 0xfe, 0xf2, 0x82, 0xbe, 0x04, 0x59, 0x92, 0x0a, 0x3f, 0x90, 0x57, 0x8f, 0xad, 0x3e, - 0x55, 0xce, 0xbe, 0x3a, 0xfb, 0xf5, 0xb7, 0xec, 0x97, 0xf4, 0xc1, 0x25, 0x27, 0x77, 0xfb, 0x54, 0x62, 0x73, 0xbd, - 0x53, 0x2a, 0x1c, 0x9d, 0x63, 0xb9, 0xac, 0xaf, 0xc4, 0x70, 0x39, 0x95, 0x10, 0xfc, 0x87, 0x0f, 0xc4, 0xef, 0xb3, - 0x72, 0xb7, 0x4f, 0x21, 0xbf, 0x9f, 0x4b, 0x2b, 0xf9, 0xc9, 0xe1, 0x46, 0xba, 0x4f, 0xab, 0x28, 0xc7, 0x5c, 0x7f, - 0x5b, 0x4a, 0xe5, 0xac, 0xb3, 0xb3, 0x4b, 0xb9, 0xb9, 0x9c, 0x73, 0x78, 0xaa, 0xcb, 0xbd, 0x5e, 0xb5, 0xd6, 0xff, - 0x57, 0x66, 0x0d, 0x65, 0xb9, 0x19, 0x94, 0xcc, 0x7b, 0x28, 0x08, 0x72, 0x37, 0xb1, 0x4e, 0x2f, 0x72, 0xe7, 0xb8, - 0x43, 0x39, 0x96, 0xb6, 0xbe, 0x2a, 0x53, 0x8f, 0xcc, 0x45, 0x8c, 0xf3, 0x15, 0xf1, 0xb2, 0x9a, 0xbc, 0x6d, 0xd0, - 0x6f, 0x4f, 0xc8, 0xfc, 0xe7, 0xd7, 0x90, 0x64, 0x3f, 0xc6, 0x2f, 0xea, 0xfe, 0x02, 0x5c, 0xc3, 0x9b, 0x72, 0xe4, - 0x05, 0x3b, 0xae, 0xab, 0xa7, 0x6d, 0xb2, 0xae, 0x85, 0x63, 0xdb, 0xe5, 0xc0, 0x6b, 0x8b, 0x38, 0x04, 0x44, 0x69, - 0x65, 0xdc, 0x73, 0x7a, 0xd7, 0xe9, 0x77, 0xa6, 0x3a, 0x86, 0xdd, 0x80, 0x20, 0x11, 0x0c, 0x28, 0x30, 0x1f, 0x83, - 0xba, 0x93, 0x51, 0xe5, 0xc4, 0x9e, 0x35, 0x10, 0x4a, 0x60, 0x45, 0xf3, 0x35, 0x12, 0x80, 0x96, 0x76, 0xe0, 0x65, - 0xad, 0xa2, 0x93, 0x25, 0x6b, 0x10, 0x1c, 0xf4, 0xff, 0x88, 0xc1, 0x11, 0x07, 0xdf, 0x24, 0x21, 0xce, 0x0a, 0x45, - 0x62, 0x4e, 0xb3, 0x47, 0x1f, 0xb3, 0x8f, 0x72, 0x09, 0xd2, 0xec, 0x47, 0x60, 0x80, 0x60, 0x19, 0x8e, 0x63, 0x91, - 0xa0, 0x64, 0xbe, 0x2a, 0xc8, 0x92, 0x9a, 0xf7, 0x9f, 0x60, 0x6c, 0xff, 0x46, 0xb7, 0x8d, 0xec, 0xef, 0x9a, 0x4a, - 0x6e, 0x7f, 0xe5, 0xdd, 0xf2, 0xeb, 0xfa, 0x7a, 0x79, 0xa1, 0xfe, 0xfc, 0xba, 0x69, 0x81, 0x77, 0x72, 0xf7, 0x52, - 0x0e, 0x35, 0x3f, 0x5f, 0x67, 0xc4, 0x58, 0x30, 0x40, 0xec, 0x53, 0xc7, 0x87, 0x92, 0xee, 0xb7, 0x9e, 0x0d, 0xac, - 0x89, 0xfd, 0x1a, 0xb7, 0xa8, 0x5e, 0xce, 0x0b, 0x6c, 0x56, 0xe3, 0x1a, 0xba, 0xe7, 0x85, 0xd6, 0x3c, 0x17, 0x66, - 0xa9, 0xa0, 0x14, 0x5b, 0x53, 0xc0, 0x27, 0xb8, 0xeb, 0xca, 0x4d, 0x6a, 0xa2, 0xea, 0x4d, 0x78, 0x92, 0xa0, 0xa2, - 0x03, 0x17, 0x4d, 0x5f, 0x3d, 0xb5, 0x2d, 0x36, 0x86, 0x3f, 0x13, 0x74, 0x35, 0x86, 0xac, 0x46, 0x39, 0x66, 0x2d, - 0x56, 0x7a, 0xa1, 0xb5, 0xbc, 0x5a, 0xea, 0x6e, 0x5f, 0x03, 0xbd, 0xf2, 0x82, 0x32, 0xe0, 0x1e, 0x80, 0xac, 0x57, - 0xf4, 0x94, 0x56, 0x91, 0x2d, 0xd9, 0x27, 0x14, 0xdc, 0x3c, 0x9e, 0xe0, 0xb0, 0xf4, 0x51, 0xdd, 0x23, 0x4d, 0x62, - 0x2b, 0x5c, 0xc3, 0xde, 0x64, 0x55, 0xe9, 0x65, 0xf3, 0x84, 0x07, 0x98, 0xd3, 0x82, 0xfd, 0x1b, 0xdb, 0x62, 0xf9, - 0x71, 0x12, 0x68, 0xbb, 0x68, 0x14, 0x37, 0xca, 0x00, 0x88, 0xd2, 0x3d, 0xbd, 0x01, 0x07, 0xa2, 0x5d, 0xd7, 0x42, - 0x7d, 0x9b, 0xd8, 0x0e, 0xe7, 0x26, 0x13, 0x6a, 0xe1, 0xc2, 0x1a, 0xcd, 0xa6, 0x0b, 0x27, 0x6a, 0xef, 0xd2, 0x9e, - 0x27, 0x83, 0x8c, 0xff, 0x2a, 0x83, 0x98, 0xf4, 0xbd, 0xc0, 0xa3, 0x83, 0x70, 0x0f, 0xa1, 0x27, 0x61, 0x91, 0x8a, - 0xd6, 0x14, 0x6c, 0x83, 0x15, 0xa5, 0x71, 0x00, 0xa0, 0xbd, 0x8b, 0xb8, 0x01, 0x07, 0x37, 0x6c, 0x0c, 0x1d, 0x1b, - 0xb7, 0xe4, 0x95, 0x64, 0x82, 0xa0, 0xf2, 0x66, 0x89, 0xcd, 0x78, 0xb2, 0x13, 0x95, 0x6f, 0x70, 0xb3, 0x73, 0x27, - 0x14, 0xf6, 0x3b, 0x9d, 0x11, 0x4c, 0x59, 0x59, 0xed, 0xd0, 0x37, 0x23, 0x5e, 0x70, 0x98, 0x43, 0xb2, 0x20, 0x22, - 0x19, 0xb1, 0xaa, 0x1b, 0xbf, 0xf3, 0x7e, 0x94, 0x9b, 0x89, 0x6d, 0xb1, 0x5e, 0xf1, 0x8c, 0x60, 0xbd, 0x83, 0xa3, - 0x73, 0xf2, 0xdc, 0xcd, 0xc8, 0x5c, 0xe1, 0x3f, 0x86, 0xc9, 0xed, 0x66, 0x7e, 0x30, 0x8c, 0xa8, 0x2f, 0xff, 0x93, - 0x8c, 0x59, 0x55, 0x4e, 0xa3, 0x31, 0x24, 0x44, 0x32, 0xbc, 0x09, 0x40, 0x3c, 0xcf, 0x9a, 0x8c, 0xd1, 0x4c, 0xac, - 0xb6, 0xad, 0xd3, 0x34, 0xfb, 0xf6, 0x92, 0xd3, 0xef, 0x45, 0x85, 0x17, 0x78, 0x5c, 0x75, 0x6e, 0x64, 0xd7, 0x0f, - 0x74, 0x31, 0x87, 0xbe, 0x55, 0xe9, 0xaa, 0xbe, 0x91, 0x1f, 0x6a, 0xf8, 0x4a, 0x0c, 0xea, 0x6e, 0x50, 0xf2, 0x00, - 0x00, 0xfd, 0x71, 0x5e, 0x5e, 0xfd, 0x5f, 0xa3, 0xb9, 0x93, 0x4e, 0xb0, 0xb1, 0x62, 0x69, 0x8e, 0xe3, 0xe5, 0xd0, - 0x5f, 0xa8, 0xe8, 0x39, 0xa1, 0xdf, 0x8d, 0x48, 0xba, 0x44, 0x67, 0x18, 0x4f, 0xcc, 0xd2, 0xe0, 0xb0, 0x86, 0x12, - 0xfa, 0x9b, 0xd1, 0x6f, 0xd7, 0xde, 0x37, 0x90, 0xe2, 0xdf, 0xb8, 0xad, 0x8e, 0x67, 0x47, 0x95, 0x99, 0xd4, 0x32, - 0x0f, 0xdc, 0x16, 0x57, 0x75, 0xd5, 0xcc, 0xa7, 0xed, 0x92, 0x69, 0xda, 0x79, 0xcc, 0x2e, 0xe3, 0x57, 0x38, 0x91, - 0x44, 0x7d, 0xb7, 0x0e, 0x03, 0x34, 0x30, 0xd0, 0x5e, 0x12, 0xa7, 0x17, 0x99, 0xae, 0xde, 0x6a, 0x06, 0x43, 0x73, - 0xa5, 0x4e, 0x3f, 0xb0, 0x7a, 0x41, 0xcb, 0xb0, 0xb3, 0x66, 0xf2, 0xc8, 0x09, 0xb1, 0x8b, 0x9c, 0x9f, 0x98, 0x0f, - 0x39, 0xa1, 0xa6, 0x01, 0xbd, 0x9d, 0x97, 0x57, 0xae, 0x54, 0x91, 0x81, 0x9a, 0x09, 0x29, 0x20, 0xbb, 0xa1, 0xf5, - 0xb1, 0x26, 0xc6, 0x1e, 0xa4, 0x0b, 0xb3, 0xd6, 0x3c, 0x0d, 0x42, 0x53, 0x28, 0x0b, 0x57, 0x66, 0x64, 0xa3, 0xf0, - 0x3d, 0x39, 0x85, 0x86, 0x0b, 0x5a, 0x42, 0x7b, 0xf7, 0x3e, 0x24, 0x74, 0xf7, 0x98, 0x44, 0xd5, 0x74, 0x96, 0x16, - 0xca, 0xcd, 0x42, 0x79, 0x8e, 0xc0, 0x0b, 0x16, 0xb9, 0xe7, 0x55, 0x39, 0x12, 0xb7, 0xee, 0xd6, 0xe9, 0xeb, 0x6e, - 0xb5, 0x86, 0x7d, 0x4c, 0x79, 0x24, 0xbc, 0xa3, 0x85, 0xf9, 0x57, 0xb2, 0xe4, 0x48, 0x87, 0x8d, 0x9a, 0x66, 0xf2, - 0x15, 0x3e, 0xff, 0x47, 0x75, 0x6f, 0xe2, 0x7d, 0xe2, 0x59, 0x81, 0x70, 0x57, 0x14, 0x3a, 0xe3, 0x8e, 0x59, 0x47, - 0xeb, 0x70, 0x4e, 0x9d, 0x98, 0xf1, 0xf0, 0xb8, 0x40, 0x31, 0xfc, 0xf6, 0x8c, 0x06, 0xdc, 0xfd, 0xe9, 0x98, 0xd8, - 0xbd, 0x0e, 0x52, 0xec, 0x32, 0x8b, 0x74, 0x7f, 0xd5, 0x68, 0xaa, 0x0b, 0xb1, 0x6e, 0x95, 0xb9, 0x27, 0xa6, 0xec, - 0x30, 0x9c, 0x51, 0xed, 0xb3, 0x85, 0x9b, 0xbd, 0x91, 0xbb, 0x51, 0xd5, 0x53, 0x6c, 0xe9, 0x92, 0xc3, 0x13, 0x38, - 0x64, 0xd3, 0xa8, 0xdc, 0xfd, 0x5a, 0xcb, 0x57, 0xfb, 0x6a, 0xd1, 0x97, 0x58, 0xc4, 0xf7, 0xf3, 0x21, 0x85, 0x2d, - 0x4f, 0x44, 0xb6, 0x3a, 0x8c, 0x75, 0x80, 0xf1, 0x50, 0xeb, 0xdb, 0xcd, 0x4e, 0x6a, 0x3f, 0xa0, 0xbb, 0x75, 0x96, - 0x96, 0x6f, 0x16, 0xbf, 0xad, 0xff, 0x3c, 0x70, 0x2f, 0x39, 0x14, 0xbf, 0x56, 0x5f, 0x45, 0xa2, 0xc1, 0xfd, 0xb2, - 0x5c, 0x93, 0x49, 0x71, 0xfc, 0x04, 0xc7, 0x54, 0xa6, 0x28, 0x47, 0xd5, 0x6d, 0x37, 0x84, 0x8a, 0x8d, 0x8e, 0xcd, - 0x79, 0xb2, 0x33, 0x75, 0xf5, 0x00, 0x8f, 0x0c, 0x51, 0xfb, 0x9f, 0xca, 0x8b, 0xd3, 0x1e, 0xd9, 0x07, 0xfb, 0xb7, - 0xcc, 0x21, 0xb6, 0x4e, 0xbb, 0x4e, 0xad, 0x9a, 0x70, 0xe0, 0xc3, 0xea, 0x1a, 0xff, 0x17, 0x2f, 0xb8, 0xd1, 0x44, - 0xf4, 0x56, 0xb5, 0x65, 0xa5, 0x04, 0xb6, 0xab, 0x5d, 0x2a, 0x35, 0xbd, 0xd5, 0x4d, 0x8c, 0xcb, 0x9c, 0xd7, 0xd5, - 0xde, 0x90, 0xf5, 0x93, 0xa0, 0x0d, 0xb9, 0x7f, 0xfa, 0x30, 0xe2, 0x10, 0x23, 0x29, 0x6b, 0x17, 0x63, 0xae, 0x0d, - 0xa1, 0x93, 0x2d, 0xca, 0x98, 0xdc, 0xf5, 0x4f, 0x24, 0xaa, 0x2e, 0x9a, 0x20, 0x10, 0xe7, 0xed, 0xb1, 0xf5, 0xea, - 0x6e, 0x71, 0xc9, 0xcd, 0x70, 0x65, 0xaa, 0x12, 0xf0, 0x79, 0x62, 0xf0, 0xc5, 0x82, 0x44, 0x09, 0x3c, 0x0b, 0xd5, - 0x64, 0xdc, 0x35, 0x44, 0x1f, 0x6c, 0xbc, 0xfc, 0xc3, 0xc8, 0x31, 0x3f, 0xf3, 0x4f, 0xca, 0x1b, 0x44, 0x27, 0xc0, - 0x99, 0x00, 0x3c, 0x9e, 0xa5, 0x25, 0xd5, 0x37, 0xa7, 0x7f, 0x6d, 0x92, 0xff, 0x77, 0x6c, 0xf8, 0x56, 0xfb, 0x15, - 0xd0, 0x68, 0x61, 0xd8, 0x21, 0xd0, 0x1a, 0xd4, 0x39, 0x85, 0x71, 0x0f, 0x01, 0xb5, 0xe2, 0x1a, 0xd7, 0x77, 0x69, - 0x84, 0x30, 0x08, 0x49, 0x50, 0xd9, 0x1d, 0x76, 0xb8, 0xb7, 0xbe, 0x2f, 0x90, 0x01, 0xc2, 0x43, 0x19, 0x41, 0x8b, - 0x8c, 0x07, 0xf7, 0x06, 0x7b, 0x10, 0xd6, 0xb9, 0x94, 0x53, 0xae, 0x92, 0xae, 0x43, 0xf6, 0x71, 0xd3, 0xf4, 0x1a, - 0x27, 0xe4, 0x08, 0x52, 0xa9, 0x67, 0x40, 0xd3, 0x74, 0x91, 0x5e, 0xae, 0xb7, 0x74, 0xca, 0xb7, 0x06, 0x62, 0xeb, - 0x5a, 0x58, 0x74, 0x9f, 0x5d, 0xca, 0x43, 0x0f, 0x52, 0x08, 0x0e, 0x89, 0xe5, 0x14, 0xd4, 0x0f, 0x60, 0x52, 0x2e, - 0xff, 0xc3, 0x24, 0x5e, 0xe5, 0xee, 0xfe, 0xd7, 0x6a, 0xb1, 0xaa, 0x1e, 0xcc, 0x6c, 0xfc, 0x40, 0xf7, 0x97, 0xf0, - 0x51, 0xad, 0x3d, 0x5f, 0x39, 0x66, 0x05, 0xa6, 0x0c, 0xfe, 0x93, 0x7f, 0xd0, 0x86, 0x3a, 0x97, 0xf9, 0x6f, 0x71, - 0x25, 0xae, 0x81, 0x14, 0xe7, 0x3d, 0xd4, 0x88, 0x26, 0x69, 0xbc, 0x4c, 0x59, 0x6d, 0x1a, 0x9f, 0x66, 0x8a, 0x40, - 0x88, 0x3a, 0x7a, 0x1d, 0x2e, 0x39, 0x70, 0x91, 0xc3, 0xaa, 0x05, 0xf8, 0x67, 0xc1, 0x0a, 0xe8, 0xf6, 0xb7, 0xe4, - 0x68, 0xcd, 0xfc, 0xed, 0x8e, 0xc6, 0x95, 0x0b, 0x39, 0x34, 0xb1, 0xf6, 0xd5, 0x76, 0x4c, 0xce, 0xd4, 0x9d, 0xa7, - 0x15, 0x8a, 0xae, 0xab, 0x9b, 0x89, 0x2b, 0x02, 0x8e, 0x53, 0xcf, 0x0f, 0x02, 0x0c, 0x66, 0x85, 0x2f, 0xfb, 0x42, - 0x4d, 0xbf, 0xc6, 0x60, 0x0a, 0x32, 0x96, 0x3d, 0x8b, 0x62, 0x78, 0x17, 0xf2, 0x2a, 0x62, 0x2c, 0x97, 0x22, 0x56, - 0x08, 0x65, 0x01, 0x5b, 0x56, 0xae, 0x47, 0xa1, 0x78, 0x78, 0x9c, 0xe2, 0xdd, 0xcc, 0x39, 0x52, 0xee, 0x12, 0xcc, - 0xee, 0x90, 0xf9, 0x49, 0x22, 0xf5, 0xda, 0xb5, 0x60, 0x93, 0x62, 0x8a, 0x5d, 0x51, 0xe4, 0x06, 0x87, 0x30, 0xe1, - 0xa8, 0x7b, 0x7b, 0xa3, 0x69, 0x22, 0xa1, 0x91, 0x28, 0x30, 0x23, 0xa4, 0xbb, 0xfe, 0xe3, 0xee, 0x4d, 0x3f, 0x99, - 0x32, 0x06, 0x11, 0xd0, 0x28, 0x7a, 0x06, 0x10, 0x7a, 0xbe, 0x4a, 0xb9, 0x64, 0x3a, 0xae, 0x60, 0xc4, 0x7d, 0x05, - 0x24, 0x5c, 0x34, 0x6e, 0xcd, 0x2f, 0xd1, 0x49, 0xa6, 0x78, 0x9a, 0x00, 0x45, 0xa3, 0xad, 0xf2, 0x6c, 0x28, 0x1f, - 0x79, 0x16, 0xac, 0x44, 0x3d, 0x69, 0x70, 0x14, 0x0c, 0xba, 0xd9, 0x48, 0xc2, 0x21, 0x35, 0x19, 0xc6, 0xc8, 0x30, - 0x38, 0xfa, 0x97, 0xb6, 0xca, 0x43, 0x6a, 0x5d, 0x2d, 0x14, 0x32, 0xa3, 0x07, 0x33, 0x3f, 0x98, 0xa8, 0x61, 0x55, - 0x0b, 0xf3, 0x41, 0xba, 0x76, 0x5a, 0x65, 0x94, 0x25, 0xc6, 0x69, 0xb0, 0x30, 0x86, 0x1c, 0x6a, 0x1c, 0xb0, 0xd9, - 0x40, 0xee, 0x6a, 0xce, 0xe6, 0x51, 0x33, 0x6e, 0xaf, 0x6b, 0x46, 0x9f, 0xfa, 0xe2, 0x56, 0x7f, 0x2e, 0xd3, 0x0d, - 0x3b, 0x56, 0xf9, 0x4b, 0xbf, 0xa8, 0xa6, 0x0f, 0x3d, 0xe6, 0x4d, 0x39, 0x18, 0x66, 0x78, 0xf5, 0x59, 0x58, 0x3c, - 0x48, 0x1a, 0x94, 0xf9, 0x52, 0xad, 0x1d, 0x6e, 0x7f, 0x3f, 0x30, 0xf4, 0x66, 0x37, 0x31, 0x49, 0x1a, 0x02, 0xe5, - 0x08, 0x89, 0x08, 0x8e, 0x59, 0xf1, 0x1f, 0x57, 0x95, 0xff, 0xbd, 0x53, 0x5f, 0xd0, 0x83, 0xf0, 0xd1, 0x5e, 0xf7, - 0x34, 0x0a, 0x98, 0xb3, 0x96, 0xed, 0xea, 0xd3, 0x84, 0x1a, 0xd2, 0x5f, 0x11, 0x32, 0x6e, 0x1c, 0xab, 0x7f, 0x74, - 0x53, 0xf2, 0x3b, 0x5d, 0x25, 0xf6, 0xd1, 0x5c, 0x9f, 0xd8, 0xa2, 0x4a, 0x3a, 0x3a, 0x36, 0xa7, 0x2d, 0x29, 0xcd, - 0x49, 0xf9, 0x56, 0x7b, 0x78, 0xda, 0x4a, 0x71, 0xc9, 0xe6, 0x3d, 0xb9, 0x9a, 0x27, 0x59, 0x6d, 0xcb, 0x71, 0x84, - 0x3b, 0xc8, 0xd7, 0xe7, 0x8c, 0xd2, 0xd1, 0x07, 0xab, 0x1f, 0xf7, 0x26, 0x81, 0xcc, 0xd3, 0x13, 0x70, 0xa3, 0x6b, - 0x57, 0x7a, 0x7c, 0x2b, 0x4e, 0xcc, 0x93, 0xf7, 0x43, 0xf6, 0x6b, 0x5c, 0xc9, 0x82, 0x8e, 0x7b, 0x5f, 0x35, 0x4c, - 0xb7, 0x19, 0xd3, 0x7e, 0xa4, 0x18, 0x8c, 0xe6, 0xab, 0x2c, 0x89, 0x0a, 0x62, 0xc1, 0x6b, 0xe2, 0x83, 0xd8, 0x00, - 0x40, 0xce, 0x68, 0x8b, 0x5a, 0x7a, 0x8c, 0x25, 0x51, 0xbc, 0xad, 0x40, 0xcd, 0x79, 0x76, 0x96, 0xd1, 0xaa, 0x3a, - 0xd1, 0xab, 0x53, 0xae, 0xd2, 0xec, 0x22, 0x74, 0x3d, 0x7c, 0x65, 0x29, 0x2a, 0x59, 0x56, 0xbd, 0x0b, 0xd3, 0x57, - 0xec, 0x95, 0x17, 0x48, 0x79, 0x57, 0x4a, 0x4d, 0x21, 0x23, 0x1b, 0x83, 0xc6, 0xd6, 0xd9, 0x4b, 0x2c, 0x6e, 0xb2, - 0x3c, 0x4a, 0x28, 0x7c, 0x31, 0xf7, 0x71, 0x7b, 0x2c, 0x55, 0xc5, 0x9c, 0x43, 0x98, 0x93, 0x2a, 0x9d, 0x74, 0x95, - 0x03, 0xf8, 0xd5, 0x65, 0x10, 0xd6, 0x48, 0xa1, 0x3a, 0xc7, 0x3d, 0x6c, 0x49, 0xa6, 0x63, 0x06, 0x19, 0x8b, 0xee, - 0xfa, 0x3b, 0x2a, 0x9d, 0xc7, 0x41, 0x74, 0x1f, 0xba, 0x5a, 0x21, 0xc2, 0x60, 0x7b, 0xd6, 0x92, 0x2b, 0x9e, 0x2b, - 0x8e, 0xb2, 0x2b, 0x31, 0xb5, 0x3c, 0x1b, 0xb2, 0x6d, 0xd1, 0x15, 0x4b, 0x65, 0x4d, 0x77, 0x57, 0x13, 0xa9, 0xe0, - 0xb1, 0x1f, 0x7f, 0xe0, 0xcb, 0x92, 0x91, 0x53, 0x99, 0xc4, 0xb2, 0x0c, 0x61, 0x6e, 0xdc, 0x10, 0x3c, 0xc1, 0x68, - 0xde, 0x92, 0x79, 0xca, 0x29, 0x85, 0xd2, 0xfb, 0x9f, 0x1b, 0x8f, 0x50, 0x36, 0xdb, 0x30, 0xbd, 0x65, 0xea, 0xbb, - 0xc4, 0xf5, 0xfc, 0x87, 0xe8, 0x94, 0x44, 0x0b, 0xde, 0x9f, 0x27, 0x32, 0xda, 0x54, 0xa8, 0xb0, 0x6e, 0x66, 0xbb, - 0xcf, 0xc1, 0xc6, 0x7f, 0x51, 0x49, 0x86, 0x1c, 0x54, 0x98, 0x5e, 0xb5, 0x63, 0xa1, 0x73, 0xc8, 0x5d, 0x6f, 0x28, - 0x88, 0x8d, 0xc0, 0x6e, 0x68, 0x05, 0x89, 0x34, 0x59, 0x88, 0x7d, 0x36, 0xaa, 0xba, 0x9b, 0x55, 0xa1, 0x06, 0xfc, - 0xf2, 0x17, 0xb1, 0x38, 0xbf, 0x40, 0x52, 0x7d, 0xc1, 0x21, 0x21, 0xf4, 0xc9, 0x6f, 0xc4, 0x5e, 0x0d, 0xbe, 0x88, - 0x95, 0x66, 0xdb, 0x31, 0xfa, 0x99, 0x9f, 0x8f, 0xbb, 0xee, 0x0c, 0x53, 0x74, 0xb8, 0x01, 0x8b, 0x11, 0x43, 0x4e, - 0x52, 0x37, 0xf9, 0x4b, 0x4a, 0x7e, 0x32, 0xbd, 0xf9, 0x06, 0xdf, 0x69, 0x6d, 0x6f, 0xa0, 0x50, 0x88, 0x59, 0x67, - 0x68, 0x70, 0xc3, 0x1e, 0x9e, 0xea, 0x98, 0x59, 0x98, 0xe3, 0x90, 0x24, 0xa2, 0x45, 0x0e, 0x67, 0x88, 0xdf, 0x00, - 0x98, 0x40, 0x93, 0x95, 0x08, 0x19, 0x25, 0xb0, 0x47, 0xf0, 0x82, 0x9b, 0x6d, 0xde, 0xef, 0x79, 0x1e, 0x2e, 0xa4, - 0x56, 0xae, 0xe0, 0x0a, 0x30, 0xd5, 0xb3, 0x6b, 0x49, 0xf1, 0xe1, 0x51, 0xb4, 0x86, 0x78, 0xae, 0x25, 0x94, 0xc5, - 0xce, 0x83, 0x60, 0x55, 0x65, 0x57, 0xd9, 0x19, 0xcc, 0x52, 0x0f, 0x0e, 0x54, 0x71, 0x81, 0x24, 0xdd, 0x18, 0x6f, - 0x53, 0xcc, 0xb2, 0x16, 0x7e, 0xaa, 0x62, 0xde, 0xb0, 0xa9, 0xe0, 0xf0, 0x5a, 0x9d, 0x7f, 0x31, 0xd6, 0x30, 0xa1, - 0x4d, 0x0d, 0xac, 0x04, 0x31, 0x69, 0x58, 0xc2, 0xc6, 0xc1, 0x67, 0xd0, 0x9f, 0x07, 0x4c, 0x33, 0x9c, 0xde, 0x8f, - 0x51, 0xbd, 0x65, 0xf5, 0xc9, 0xf7, 0xd2, 0xf3, 0x46, 0x1d, 0x3d, 0xa8, 0xc4, 0xaa, 0xe5, 0xeb, 0x0c, 0x11, 0xdd, - 0xde, 0xfa, 0x8c, 0xe7, 0xd4, 0x32, 0x04, 0x40, 0xe2, 0x49, 0x9d, 0x19, 0x7b, 0x7c, 0xdc, 0x30, 0x46, 0x52, 0xa5, - 0xb7, 0x2c, 0x42, 0xa6, 0x9f, 0x94, 0x55, 0x0d, 0x87, 0x27, 0x9b, 0x76, 0x25, 0x54, 0x0c, 0xd7, 0x6f, 0x96, 0x17, - 0x50, 0x85, 0xc5, 0x0c, 0xc5, 0x1c, 0x9b, 0xca, 0xd9, 0x78, 0x83, 0x79, 0x06, 0xe3, 0x3c, 0xa5, 0x31, 0x37, 0x54, - 0xa0, 0x5f, 0x2a, 0x47, 0x53, 0x67, 0x62, 0xce, 0x58, 0x9e, 0xfb, 0xb0, 0xe3, 0x73, 0xc7, 0x98, 0x5d, 0x78, 0xee, - 0xee, 0xa9, 0xc3, 0xf6, 0x59, 0x74, 0x19, 0xee, 0x6e, 0x61, 0xc9, 0x9e, 0x92, 0x49, 0x8c, 0x03, 0x58, 0xe7, 0xd1, - 0x95, 0xad, 0xf1, 0x52, 0x06, 0xbb, 0x3f, 0x41, 0x0c, 0xe0, 0x68, 0xc1, 0x60, 0x04, 0xec, 0x5a, 0x7e, 0xed, 0x6a, - 0xac, 0x74, 0xf3, 0x71, 0x60, 0x85, 0x17, 0x99, 0xc0, 0xe5, 0x23, 0x26, 0xd2, 0xe0, 0x7f, 0xde, 0xc7, 0xc9, 0x57, - 0x9b, 0x8e, 0x26, 0xb2, 0xd7, 0x42, 0xbe, 0xf0, 0xaf, 0xe1, 0x6e, 0x1e, 0x98, 0xf2, 0xc5, 0x1e, 0x4f, 0x11, 0x05, - 0x4d, 0x62, 0x6c, 0xf5, 0x8c, 0x8b, 0x3d, 0x73, 0xb5, 0xe1, 0x17, 0x22, 0x8a, 0xbb, 0xbb, 0xb8, 0x2c, 0x04, 0x2c, - 0x99, 0xf0, 0x53, 0xce, 0xd4, 0xc8, 0x94, 0x3d, 0xc4, 0xb7, 0xcb, 0xf0, 0xe1, 0x71, 0x23, 0xf6, 0xc9, 0x5d, 0x11, - 0x9e, 0x48, 0xaa, 0xb0, 0xcf, 0xfc, 0xef, 0x90, 0x31, 0x27, 0xa2, 0xe8, 0xb1, 0x4c, 0x37, 0x06, 0xcf, 0x7f, 0x56, - 0xf1, 0x64, 0x55, 0x00, 0xb6, 0x46, 0x05, 0xe9, 0x97, 0x80, 0x73, 0x0a, 0x40, 0x3d, 0x0c, 0x63, 0x20, 0xc5, 0x9c, - 0x42, 0xe0, 0xe8, 0x92, 0x76, 0xc0, 0xf0, 0xb3, 0x59, 0xd0, 0x63, 0xf1, 0x5b, 0xba, 0xdc, 0x6d, 0xce, 0xcf, 0xd6, - 0x28, 0x6f, 0x2a, 0x77, 0xd5, 0xb0, 0xca, 0x4c, 0x4d, 0x61, 0xb2, 0xd8, 0x9b, 0xb9, 0x0e, 0xdf, 0xf9, 0x63, 0x5f, - 0xbb, 0xc4, 0xc1, 0xc8, 0x8d, 0xcc, 0x15, 0x2e, 0x3c, 0x98, 0x76, 0xf2, 0x0a, 0x88, 0x9a, 0xad, 0x04, 0x57, 0x02, - 0xad, 0x07, 0xa7, 0x0e, 0x77, 0x0a, 0x58, 0x41, 0x20, 0xf3, 0xfa, 0xab, 0x3e, 0x39, 0x90, 0xd1, 0xe6, 0x0a, 0x19, - 0xf4, 0xdc, 0xea, 0x05, 0x5a, 0xe5, 0x7d, 0xab, 0xfb, 0x39, 0x79, 0x63, 0xde, 0x75, 0x1f, 0x81, 0xc9, 0xf7, 0x8c, - 0xc4, 0x86, 0x2c, 0xaf, 0x85, 0xc2, 0x24, 0x01, 0x3d, 0x0e, 0xaa, 0x0a, 0x89, 0xd4, 0xa1, 0x6c, 0xd4, 0x0c, 0x15, - 0xc2, 0xf4, 0xfa, 0x07, 0x80, 0x80, 0xa3, 0x94, 0x42, 0x79, 0x22, 0xaa, 0x32, 0x02, 0x08, 0x8c, 0x0d, 0xd0, 0xb0, - 0x0c, 0x4c, 0x61, 0x9b, 0x51, 0xb4, 0xe5, 0x74, 0xe9, 0x6e, 0xbc, 0x2f, 0x47, 0xe6, 0xbc, 0x1b, 0x3c, 0x4b, 0xd0, - 0x6e, 0xec, 0xeb, 0x38, 0x86, 0x7e, 0x2a, 0xfa, 0x47, 0xb0, 0x83, 0x73, 0x58, 0x82, 0x82, 0x53, 0x42, 0x9f, 0x33, - 0xff, 0x83, 0xaf, 0xc4, 0xeb, 0x9e, 0xb6, 0xb8, 0xb7, 0x63, 0xc7, 0xcc, 0xca, 0x8f, 0x4d, 0x96, 0x5c, 0xcb, 0x90, - 0x44, 0x79, 0xcd, 0xa5, 0x63, 0xd0, 0x94, 0xc8, 0xcd, 0x07, 0x81, 0xa4, 0xbd, 0x41, 0xe5, 0x47, 0x9b, 0xd3, 0xfe, - 0x48, 0x08, 0xb1, 0x5a, 0x6a, 0xe6, 0xe2, 0x4b, 0x8a, 0x05, 0x50, 0x70, 0x1f, 0xc0, 0xf9, 0x3b, 0x71, 0xfd, 0xbb, - 0xe8, 0xc0, 0xa1, 0x8f, 0x00, 0x06, 0xe0, 0xad, 0x54, 0x5b, 0x79, 0x13, 0x50, 0x5a, 0x01, 0x90, 0x6b, 0x53, 0x19, - 0xe0, 0x0d, 0xf9, 0x0b, 0x1e, 0xd9, 0x97, 0x29, 0x50, 0x8c, 0xe2, 0xc6, 0xbb, 0x4c, 0xe5, 0xe5, 0xdd, 0x31, 0xe7, - 0x82, 0xdd, 0xdc, 0x30, 0xaf, 0xda, 0x44, 0x99, 0xb4, 0x5d, 0x0c, 0x62, 0x9c, 0x12, 0xa4, 0x28, 0x01, 0xd2, 0xf7, - 0x0f, 0x66, 0x21, 0x7a, 0xfe, 0xee, 0xd1, 0x5d, 0x65, 0xae, 0xc3, 0x30, 0x9a, 0xec, 0x1d, 0x51, 0x0b, 0x72, 0xba, - 0x3a, 0x26, 0xdf, 0x27, 0x07, 0xe1, 0x5f, 0x12, 0xf5, 0x33, 0x55, 0x82, 0xa6, 0xfa, 0xa6, 0x8a, 0x38, 0xa8, 0xf1, - 0x09, 0x88, 0xda, 0x32, 0xa9, 0x09, 0x73, 0x25, 0xea, 0x93, 0xeb, 0x44, 0x60, 0x5a, 0xfd, 0xb3, 0x1f, 0x5f, 0xfd, - 0xc0, 0x60, 0x7b, 0xbf, 0x77, 0xf1, 0x75, 0xa6, 0x0f, 0xe7, 0xef, 0xd8, 0xbd, 0xfb, 0x3c, 0xb8, 0x71, 0x5c, 0x5d, - 0xd6, 0x23, 0x49, 0x23, 0x33, 0xc0, 0x7b, 0xca, 0xa0, 0x61, 0x2f, 0xca, 0xe6, 0xcc, 0x35, 0xbb, 0xcf, 0xba, 0xca, - 0x5d, 0x40, 0x3f, 0x22, 0x69, 0x35, 0xa1, 0xb8, 0x00, 0x6e, 0xe7, 0x31, 0xcd, 0x0a, 0xff, 0x83, 0x42, 0xcd, 0x04, - 0x7b, 0x19, 0x19, 0xa5, 0x2f, 0x23, 0x98, 0xaf, 0x16, 0x41, 0xb6, 0xac, 0x42, 0xbb, 0x8f, 0x1a, 0x6c, 0xd6, 0xae, - 0xcd, 0xb3, 0x7b, 0xcb, 0x01, 0x39, 0x13, 0x44, 0xe3, 0x45, 0xad, 0xe4, 0x59, 0x4f, 0xf5, 0xaa, 0xe7, 0x88, 0x9b, - 0xae, 0xdc, 0xab, 0x47, 0xa6, 0xf9, 0x78, 0x85, 0x77, 0x3f, 0xea, 0x22, 0xda, 0x95, 0xb5, 0x00, 0x56, 0x16, 0x43, - 0xf2, 0x3d, 0xeb, 0xef, 0x99, 0x74, 0x09, 0x16, 0x90, 0xfa, 0xc4, 0xeb, 0xda, 0x75, 0xd0, 0x91, 0x93, 0xba, 0xfd, - 0x28, 0x0a, 0x66, 0xa5, 0x45, 0x26, 0xe5, 0x89, 0xa4, 0x2b, 0xc9, 0x47, 0xae, 0x62, 0x66, 0x98, 0xe6, 0xd2, 0x19, - 0xf6, 0xa4, 0xbf, 0xaa, 0xda, 0xab, 0xed, 0x39, 0x04, 0xc0, 0x35, 0x4f, 0x21, 0x56, 0xef, 0x56, 0xd1, 0xc2, 0x9d, - 0xf1, 0x6e, 0xff, 0xa2, 0xb5, 0xe3, 0x61, 0xec, 0xd6, 0x30, 0x0e, 0x32, 0x67, 0x05, 0xf3, 0x9b, 0x1c, 0xd3, 0x70, - 0xcd, 0x68, 0xa3, 0x0b, 0x3e, 0xc5, 0xbe, 0x5c, 0xbd, 0x8f, 0xba, 0x87, 0x0a, 0x91, 0xde, 0x83, 0x67, 0x84, 0xaf, - 0x37, 0x7f, 0x6d, 0xd4, 0x6b, 0xdf, 0xd1, 0x58, 0xde, 0x58, 0x3a, 0x82, 0xc6, 0x3f, 0x26, 0x38, 0xa3, 0x30, 0xdf, - 0xc0, 0x9b, 0x26, 0x93, 0xb5, 0x09, 0xc7, 0x8e, 0xdc, 0x26, 0xc5, 0xa6, 0xa5, 0x2a, 0xdf, 0x25, 0xb0, 0x92, 0x5b, - 0xa6, 0xfd, 0xe2, 0x9e, 0x52, 0x8f, 0x0d, 0xc1, 0x42, 0xc5, 0x82, 0x00, 0x35, 0xe6, 0xaa, 0xbd, 0x99, 0x48, 0x06, - 0xa7, 0xb4, 0xfd, 0x6c, 0xfb, 0x0c, 0xb3, 0xb3, 0x26, 0x12, 0x82, 0xb3, 0x42, 0xcb, 0xa6, 0x9b, 0xb4, 0x91, 0x00, - 0x8d, 0x54, 0xa3, 0xd0, 0x64, 0x82, 0xc6, 0xce, 0x7b, 0x41, 0xee, 0x86, 0x0e, 0xa9, 0xeb, 0x0c, 0x4a, 0xf0, 0x85, - 0x23, 0x31, 0x4b, 0x77, 0x41, 0x69, 0x0d, 0xdd, 0x63, 0xa8, 0x5e, 0x6f, 0xbf, 0x0b, 0x9e, 0x91, 0xf1, 0xa6, 0x11, - 0x68, 0x8e, 0x41, 0x20, 0xdf, 0x04, 0x63, 0x02, 0x0e, 0xbc, 0xf3, 0xf2, 0xfb, 0x48, 0x44, 0xca, 0x0f, 0xb0, 0x01, - 0xbd, 0xcc, 0x37, 0x5e, 0x04, 0x2b, 0x52, 0xa5, 0x19, 0x16, 0x66, 0x8f, 0xc1, 0xbc, 0xed, 0x8e, 0x7e, 0x32, 0xfc, - 0xcc, 0x04, 0x2f, 0xf9, 0x93, 0xe7, 0xf4, 0xce, 0x10, 0x1e, 0x62, 0xf0, 0xc1, 0x58, 0xf5, 0xc0, 0xe3, 0x94, 0xc6, - 0x0d, 0x01, 0x2e, 0x9f, 0xfd, 0xc8, 0x7c, 0x10, 0xbb, 0x11, 0x80, 0x67, 0x17, 0x57, 0x19, 0x78, 0x9b, 0xa9, 0xab, - 0x93, 0x96, 0x4e, 0xee, 0x17, 0x62, 0xc4, 0x0c, 0x52, 0x31, 0x9f, 0xde, 0xc8, 0x28, 0x0d, 0xe2, 0xa2, 0x64, 0xc6, - 0x25, 0xc5, 0xae, 0x39, 0x6f, 0xe8, 0x96, 0x9f, 0x33, 0x45, 0xed, 0x9d, 0x1d, 0xeb, 0x78, 0xff, 0x8f, 0xf3, 0x27, - 0x5d, 0x30, 0xba, 0xbc, 0xb5, 0xae, 0x66, 0xdb, 0xf3, 0x4d, 0x4d, 0xa9, 0x81, 0xe1, 0x37, 0x26, 0xfc, 0xd1, 0xc7, - 0x4b, 0x4d, 0x41, 0x0d, 0x8d, 0x5d, 0x64, 0x53, 0x8a, 0xac, 0xf0, 0xc6, 0x31, 0x65, 0xbe, 0x04, 0x52, 0x2c, 0xc6, - 0x4f, 0x3e, 0x37, 0x1a, 0x5c, 0x33, 0x52, 0x1c, 0x0e, 0x09, 0xea, 0x45, 0x91, 0xb7, 0x9f, 0x3a, 0xf9, 0x1c, 0x57, - 0x6f, 0xe6, 0x37, 0x43, 0x60, 0xa6, 0xdb, 0x56, 0x5a, 0xac, 0x9b, 0xb6, 0xe2, 0xab, 0xb5, 0x4a, 0xe3, 0x29, 0x5a, - 0xe3, 0xbb, 0x1a, 0x87, 0xe9, 0x95, 0xea, 0xab, 0xe1, 0xd7, 0xbd, 0x8d, 0xcd, 0x8c, 0x66, 0x2e, 0x74, 0x90, 0x38, - 0x24, 0xbd, 0x54, 0x4d, 0xab, 0xa8, 0xc6, 0x9e, 0x1e, 0xab, 0x1a, 0x94, 0x88, 0x77, 0xe8, 0x24, 0xd6, 0x30, 0x5b, - 0x8f, 0x72, 0xfa, 0x61, 0xb5, 0x85, 0x5a, 0xb1, 0x33, 0x31, 0xa9, 0xa9, 0x37, 0x96, 0xe5, 0xd5, 0x56, 0xd5, 0xe7, - 0x74, 0xa0, 0xc3, 0x9f, 0xc3, 0x1d, 0x84, 0xef, 0x6e, 0x05, 0x9a, 0x10, 0x64, 0x2e, 0xb6, 0xf2, 0x75, 0x88, 0xef, - 0xd2, 0x1e, 0x8f, 0x25, 0x56, 0x2b, 0x12, 0x8d, 0x6f, 0xaa, 0x2a, 0x0c, 0xc8, 0x65, 0x46, 0x15, 0x4b, 0x7b, 0x65, - 0x54, 0xc8, 0x8a, 0xec, 0x8d, 0x04, 0x69, 0x55, 0xf0, 0x42, 0x90, 0xd2, 0x2c, 0x9a, 0x98, 0xcc, 0x5f, 0x0d, 0xef, - 0x92, 0x92, 0xcc, 0x26, 0xf8, 0xd3, 0x26, 0xce, 0x66, 0x14, 0x70, 0xb7, 0x52, 0x37, 0xb8, 0xd8, 0x9a, 0x71, 0x55, - 0xae, 0x20, 0xb7, 0x05, 0x07, 0x21, 0xab, 0xa2, 0x68, 0x61, 0x9f, 0xdf, 0xf9, 0xa7, 0x48, 0x53, 0x00, 0x40, 0xe4, - 0x35, 0xa1, 0x29, 0xbb, 0x69, 0x41, 0x66, 0xe9, 0x60, 0x19, 0xc0, 0x07, 0x2c, 0x85, 0xf2, 0xd0, 0x79, 0x1e, 0xd7, - 0xdd, 0xcb, 0x4c, 0x2d, 0x2a, 0x2d, 0x1b, 0x74, 0x4f, 0x07, 0x87, 0x5c, 0xaf, 0x74, 0xfa, 0xef, 0x8f, 0xd4, 0x56, - 0x5e, 0xa0, 0x0e, 0x2a, 0x7c, 0xf7, 0x51, 0xb5, 0x10, 0xe3, 0x50, 0xab, 0xff, 0xad, 0x28, 0xd1, 0x39, 0x05, 0x4f, - 0xa2, 0xd7, 0x3f, 0x56, 0xac, 0xd3, 0x2b, 0x26, 0x91, 0xf8, 0x72, 0x22, 0xa8, 0xdb, 0x66, 0x57, 0x5d, 0x72, 0xda, - 0x5c, 0x65, 0xf6, 0x9f, 0x0e, 0x78, 0xf6, 0xad, 0x77, 0x7e, 0xa7, 0x17, 0x77, 0x90, 0x44, 0xa1, 0xd0, 0xf3, 0x21, - 0x3e, 0xa0, 0xa3, 0xb2, 0xfa, 0x13, 0x91, 0x14, 0xab, 0x96, 0xcb, 0xd0, 0x80, 0x22, 0xc5, 0x4d, 0x19, 0xd5, 0x78, - 0xd0, 0xeb, 0x19, 0xbb, 0x04, 0x69, 0x74, 0xb3, 0x57, 0x2a, 0xc1, 0x49, 0x2b, 0xad, 0x3e, 0x2f, 0x41, 0x90, 0x6d, - 0xbc, 0x93, 0x5c, 0xa5, 0x58, 0x61, 0xf6, 0x58, 0x96, 0x95, 0x51, 0xd7, 0x50, 0x8c, 0xce, 0xb2, 0x8a, 0x5a, 0xe7, - 0xda, 0x6a, 0xa7, 0x08, 0xc5, 0x3a, 0x16, 0x00, 0x9b, 0x16, 0x4e, 0x07, 0x69, 0x61, 0x0b, 0x7a, 0x3a, 0xa9, 0xc6, - 0x25, 0xcd, 0x8e, 0xb1, 0xc8, 0xbb, 0x85, 0x41, 0xd0, 0x44, 0x5f, 0x77, 0x70, 0x53, 0x1e, 0x2b, 0x8a, 0x3a, 0xb8, - 0xde, 0xb1, 0x0c, 0xaf, 0x0e, 0xcd, 0x78, 0x81, 0xae, 0xa4, 0xec, 0x08, 0x95, 0x2b, 0x61, 0x27, 0x6d, 0x4a, 0x07, - 0x6d, 0x15, 0x7e, 0x68, 0x9e, 0x94, 0x8e, 0x05, 0xaf, 0x58, 0xa1, 0xc4, 0x35, 0xdd, 0x79, 0x0f, 0xeb, 0xa5, 0x9b, - 0x18, 0x23, 0xe4, 0x2b, 0xe2, 0xaf, 0x91, 0x22, 0xcc, 0x0d, 0x64, 0x0d, 0x2c, 0x64, 0x34, 0xc5, 0x24, 0x4c, 0x90, - 0xb1, 0xc7, 0x98, 0x78, 0xd1, 0x2d, 0x2e, 0xfd, 0x19, 0xb4, 0x41, 0x9b, 0x4d, 0x2b, 0xe9, 0x3e, 0x70, 0x85, 0xf6, - 0x7b, 0x3c, 0xe9, 0x90, 0x8f, 0x2d, 0x44, 0xcf, 0x14, 0x5c, 0xbe, 0x2e, 0xa1, 0x51, 0x7f, 0x00, 0x65, 0xed, 0x58, - 0x8a, 0xcd, 0x9a, 0x84, 0x1d, 0x98, 0x4e, 0x94, 0xf6, 0x7d, 0xf0, 0xa9, 0xc7, 0x5f, 0xbe, 0xde, 0x23, 0xf8, 0x0c, - 0x65, 0x4f, 0x14, 0x9b, 0x29, 0x86, 0x8a, 0xa6, 0xa6, 0xa0, 0x99, 0x0f, 0xce, 0xc2, 0x6d, 0xf1, 0x7a, 0x42, 0x2f, - 0x57, 0xbb, 0x75, 0x4f, 0xe5, 0xe3, 0x8a, 0x34, 0x40, 0xab, 0xcf, 0x1a, 0x95, 0x0f, 0xe6, 0xc9, 0x3d, 0xaf, 0x74, - 0xcf, 0x0f, 0xe8, 0xa1, 0xd1, 0xee, 0xe2, 0x4d, 0xd7, 0x32, 0x3e, 0xb4, 0x9b, 0xac, 0xcf, 0x60, 0x11, 0x7a, 0xa8, - 0xa1, 0x14, 0xcd, 0xe1, 0x26, 0xab, 0xd9, 0xf0, 0xee, 0x8d, 0x66, 0x6d, 0x29, 0x25, 0x7f, 0x6f, 0xe7, 0xc5, 0x36, - 0x9b, 0x2a, 0x9c, 0xde, 0x0f, 0xa4, 0x77, 0x09, 0x14, 0xcd, 0x91, 0xef, 0x4e, 0xa7, 0xa9, 0x7c, 0xd0, 0x4f, 0x80, - 0xa1, 0x82, 0xef, 0x1e, 0x69, 0x74, 0x67, 0x4d, 0x71, 0xbe, 0x82, 0x4b, 0x0f, 0xa3, 0xc6, 0xb6, 0x4b, 0x4f, 0x04, - 0xe1, 0xd4, 0xe2, 0x1e, 0xe9, 0x25, 0x45, 0x4b, 0x1d, 0x29, 0xe1, 0x9f, 0x22, 0x9c, 0x53, 0x8d, 0x1d, 0xed, 0x6c, - 0x1a, 0x6f, 0xa8, 0x20, 0x3d, 0x6a, 0x72, 0x4d, 0xfb, 0x12, 0x0a, 0xe0, 0xbf, 0x9d, 0xba, 0x12, 0xe1, 0x32, 0x21, - 0x37, 0xab, 0x8a, 0x4a, 0x83, 0x32, 0x00, 0x28, 0xbf, 0x5d, 0xcb, 0xe8, 0x1a, 0x3f, 0x5a, 0xa8, 0xcb, 0x12, 0x73, - 0xa0, 0x83, 0x16, 0x67, 0x77, 0x03, 0x2d, 0x92, 0x6d, 0x73, 0xea, 0xae, 0x51, 0xb5, 0xc5, 0x93, 0xc0, 0x4b, 0x44, - 0x63, 0x39, 0xeb, 0xe7, 0xf0, 0x6d, 0xda, 0x5e, 0x0f, 0xce, 0x3a, 0x40, 0xb7, 0xb0, 0xa0, 0xda, 0x9a, 0xb5, 0xfc, - 0x5e, 0x81, 0x0f, 0xf8, 0x51, 0x6c, 0xec, 0x3c, 0x76, 0x42, 0xcd, 0xc9, 0x0e, 0xbd, 0xb9, 0x49, 0x39, 0x27, 0xca, - 0x9c, 0x4e, 0x62, 0x1c, 0x85, 0x26, 0xea, 0x8c, 0x30, 0xf9, 0xd4, 0x6b, 0x32, 0x03, 0x1e, 0x7d, 0xe3, 0x22, 0x61, - 0x1f, 0x9c, 0x53, 0xc9, 0x96, 0xb5, 0x66, 0x93, 0x9b, 0x9f, 0x49, 0xb1, 0xc9, 0x98, 0x60, 0xbd, 0xa0, 0xf7, 0x37, - 0x44, 0x42, 0x18, 0x37, 0x84, 0x62, 0x68, 0x6a, 0x52, 0xe3, 0x69, 0x73, 0xa5, 0x64, 0x46, 0x97, 0x54, 0xbf, 0xac, - 0x2e, 0x28, 0x24, 0x5a, 0x51, 0xf0, 0xcb, 0x16, 0x8e, 0xbd, 0x46, 0x10, 0x7b, 0x06, 0xa0, 0x0f, 0x1d, 0xa0, 0x89, - 0x91, 0xdc, 0x6a, 0x6a, 0x4f, 0xb6, 0x04, 0x5e, 0x79, 0x19, 0xe2, 0x0d, 0xfb, 0x4d, 0x50, 0x09, 0x3c, 0xc7, 0xbe, - 0x59, 0x0e, 0x79, 0x0e, 0x97, 0xd5, 0x5d, 0x7d, 0x8a, 0xe8, 0xdc, 0xf9, 0xc2, 0xc3, 0x14, 0xbd, 0x43, 0xb5, 0x8f, - 0xd0, 0xd5, 0x2b, 0x79, 0x15, 0x73, 0x90, 0x83, 0x45, 0xdd, 0x98, 0x6c, 0x62, 0x57, 0xa7, 0x9e, 0x6b, 0x40, 0xb7, - 0xc7, 0x6e, 0x06, 0x62, 0x0f, 0x23, 0x26, 0xeb, 0x78, 0x4a, 0x6f, 0x11, 0x31, 0xda, 0x62, 0x22, 0xa9, 0x99, 0x34, - 0x6b, 0x52, 0x03, 0x39, 0x2d, 0xc2, 0x1c, 0xd4, 0x4f, 0x74, 0x8e, 0x3d, 0x12, 0xba, 0xb3, 0x6c, 0xbb, 0x46, 0x25, - 0x93, 0xdc, 0x61, 0xae, 0x50, 0x49, 0x08, 0xa9, 0x28, 0xbb, 0x92, 0x29, 0x30, 0xa5, 0x31, 0xe1, 0x1a, 0x57, 0x83, - 0x22, 0x43, 0x97, 0x0a, 0x6a, 0x6f, 0x9d, 0xa4, 0xd4, 0x39, 0x67, 0x0e, 0xf0, 0x29, 0x94, 0x3f, 0xab, 0x9a, 0x87, - 0x4d, 0xad, 0x40, 0xc5, 0xbd, 0x7a, 0xb9, 0x58, 0xf0, 0x66, 0xfe, 0x04, 0x85, 0x41, 0xa1, 0xaf, 0xa8, 0x29, 0xcf, - 0xe5, 0xa1, 0xac, 0x44, 0xdf, 0x8c, 0xbf, 0xa7, 0xf2, 0x8a, 0xc0, 0x65, 0xbe, 0x42, 0x24, 0xa6, 0xdf, 0x78, 0xa8, - 0x3f, 0x2d, 0x0b, 0x9b, 0x2a, 0x62, 0xfa, 0xf7, 0x93, 0xbf, 0x36, 0x90, 0xe0, 0x87, 0x9e, 0xd8, 0x2f, 0x5a, 0x08, - 0x3a, 0x16, 0x19, 0x54, 0x98, 0x23, 0x72, 0xa2, 0x00, 0x4e, 0xb2, 0x47, 0xd7, 0xcb, 0x4b, 0x6a, 0xe8, 0x08, 0x6e, - 0x81, 0x9c, 0xb0, 0xa2, 0x6a, 0x24, 0xcf, 0xfe, 0xde, 0x76, 0x4b, 0xaa, 0x53, 0x0e, 0x03, 0x36, 0x7f, 0xed, 0x69, - 0x19, 0xba, 0x7b, 0x1b, 0x04, 0xb0, 0x05, 0xbd, 0x1e, 0x87, 0x1f, 0x01, 0x34, 0x82, 0xc9, 0x0c, 0x19, 0xcb, 0xa7, - 0x86, 0xea, 0x55, 0x5f, 0x9e, 0x1c, 0x85, 0xb8, 0x75, 0x8a, 0x8c, 0x28, 0x5b, 0x41, 0x2e, 0x63, 0xcb, 0x6e, 0xbf, - 0x95, 0xfe, 0xc0, 0x0a, 0xaa, 0x6b, 0x15, 0x82, 0x1c, 0x8c, 0x49, 0x78, 0x23, 0x51, 0x61, 0xcd, 0xdf, 0x74, 0x06, - 0x78, 0xcb, 0x53, 0x38, 0x84, 0x7b, 0xbb, 0x03, 0x6a, 0x7d, 0x0d, 0x41, 0xa1, 0xa9, 0x1c, 0x38, 0x9b, 0xa2, 0x35, - 0x47, 0x1c, 0x9c, 0xcf, 0x61, 0xc3, 0xf2, 0x1e, 0x77, 0x33, 0xc4, 0x32, 0x88, 0x14, 0xd1, 0xe1, 0x90, 0xa5, 0xc5, - 0xa2, 0xc6, 0x16, 0xab, 0x90, 0xf6, 0x29, 0xce, 0x08, 0x37, 0x31, 0xa5, 0x38, 0xd1, 0x87, 0x97, 0x60, 0x78, 0x06, - 0xce, 0x30, 0x6b, 0xd7, 0x1e, 0x88, 0xdb, 0x1b, 0x3a, 0x5f, 0x7e, 0xc9, 0x8e, 0x32, 0x7f, 0xae, 0x3a, 0x03, 0x96, - 0xcf, 0x7e, 0xde, 0x13, 0xa0, 0xa7, 0x9f, 0x38, 0x6c, 0xab, 0x9f, 0x4a, 0xcf, 0x46, 0x6a, 0xac, 0xee, 0x99, 0x4e, - 0xce, 0xdc, 0xd6, 0x1b, 0x1e, 0xe8, 0xcc, 0x82, 0x84, 0x0f, 0x5e, 0x63, 0x1f, 0xb4, 0x91, 0x4a, 0xd2, 0x57, 0x3c, - 0x51, 0xde, 0x5b, 0xe0, 0x57, 0x0f, 0x64, 0xe5, 0x91, 0x0b, 0x12, 0xf4, 0x12, 0xda, 0xf3, 0x79, 0x74, 0xc6, 0xc1, - 0xb0, 0x37, 0xca, 0x27, 0xc6, 0x4b, 0x4c, 0xfa, 0xe6, 0xdb, 0x61, 0x88, 0x48, 0xdf, 0x47, 0x54, 0xe0, 0x84, 0x2c, - 0xa9, 0xf2, 0xf0, 0x46, 0xc2, 0x21, 0x9e, 0x4a, 0x74, 0xa6, 0x4e, 0xcd, 0x59, 0xb5, 0x58, 0x6c, 0x59, 0x79, 0xf5, - 0xda, 0x51, 0xe4, 0x77, 0x6c, 0xd5, 0x92, 0x76, 0xb2, 0xaf, 0x94, 0xa1, 0x55, 0x89, 0x33, 0x18, 0x19, 0x5d, 0xb0, - 0xd5, 0x8b, 0x24, 0x1f, 0xb2, 0x26, 0x2a, 0x1a, 0xd6, 0x95, 0xc9, 0xcc, 0x28, 0xb9, 0x95, 0xf5, 0x45, 0xda, 0xb1, - 0x5d, 0x78, 0xad, 0x42, 0x25, 0xe9, 0xa0, 0x9e, 0x93, 0xe4, 0xfc, 0x40, 0xda, 0x3a, 0xca, 0xdf, 0xb6, 0xaa, 0x93, - 0xeb, 0xa1, 0xa7, 0xec, 0x2a, 0x1d, 0x08, 0xf3, 0x55, 0xb2, 0x06, 0x81, 0x81, 0x7c, 0x77, 0x60, 0x3b, 0x06, 0x9d, - 0xbe, 0xdd, 0x1e, 0x99, 0x74, 0x3c, 0x05, 0x3a, 0x65, 0x14, 0x30, 0x4d, 0x14, 0x46, 0x57, 0x6b, 0xcc, 0xfe, 0xee, - 0x78, 0xe9, 0xbb, 0x82, 0x03, 0x55, 0xdc, 0xe9, 0x41, 0xf8, 0xe1, 0xde, 0xd5, 0xc6, 0xe0, 0x87, 0x53, 0x47, 0x4f, - 0xcc, 0xcc, 0x52, 0xcb, 0xbf, 0xaf, 0x77, 0x87, 0xdf, 0xc7, 0x29, 0xc4, 0xf0, 0x81, 0x5e, 0xc7, 0xee, 0x8b, 0xfa, - 0x57, 0xf1, 0x32, 0x97, 0xf8, 0xc2, 0x0f, 0x5d, 0xd1, 0xec, 0xa6, 0xde, 0x95, 0x35, 0xcc, 0xa1, 0x6f, 0xc5, 0xda, - 0x48, 0xe5, 0x16, 0x58, 0xf7, 0x66, 0xb9, 0x13, 0x07, 0x0c, 0x38, 0xd7, 0x73, 0xc4, 0xc4, 0x67, 0xa6, 0x5a, 0xcf, - 0x24, 0x9d, 0x9a, 0xe9, 0x48, 0x86, 0x51, 0x0b, 0x58, 0x89, 0x89, 0x50, 0xdc, 0xe1, 0x01, 0x51, 0xd9, 0x21, 0x8f, - 0x7e, 0x2c, 0xf4, 0x6d, 0x4a, 0x28, 0x1b, 0xbe, 0x82, 0x97, 0x27, 0x97, 0x3f, 0x18, 0xd9, 0x78, 0xdc, 0xfc, 0xb1, - 0xd2, 0x8b, 0x97, 0x33, 0x4f, 0xde, 0xa0, 0x88, 0xd5, 0x79, 0x82, 0xe5, 0x01, 0x12, 0x9b, 0x33, 0x37, 0xd0, 0x49, - 0x30, 0xf5, 0x46, 0x37, 0xbf, 0x14, 0xc1, 0xad, 0xb6, 0x0d, 0x4e, 0xf9, 0x99, 0xe9, 0x1e, 0x85, 0xe2, 0x27, 0x3f, - 0xc3, 0x92, 0x59, 0x36, 0x4e, 0xc0, 0xc9, 0xb2, 0x2b, 0x0f, 0x3e, 0x6d, 0x7d, 0xf1, 0x61, 0x7d, 0x61, 0xfd, 0x79, - 0x8a, 0xb1, 0xfe, 0xfc, 0x92, 0xde, 0xdc, 0x3b, 0xac, 0x83, 0xd8, 0x7a, 0x74, 0x83, 0xfe, 0x29, 0x35, 0xec, 0xfc, - 0xb1, 0x33, 0x84, 0x0a, 0x11, 0x6a, 0xbc, 0x41, 0x58, 0x70, 0xee, 0x8e, 0x94, 0xac, 0x9a, 0x31, 0x4e, 0x2b, 0x49, - 0x6b, 0xc0, 0xbe, 0xd2, 0xa4, 0xb4, 0xca, 0xed, 0x6a, 0x45, 0xcc, 0x2e, 0x4d, 0xed, 0x50, 0x60, 0xd1, 0x77, 0x52, - 0x92, 0x24, 0xe7, 0xa5, 0x67, 0xf7, 0x48, 0x6d, 0x47, 0x40, 0xe5, 0xea, 0xbe, 0x40, 0x30, 0x6e, 0x6f, 0x77, 0x07, - 0xaa, 0xa6, 0xbe, 0x83, 0x41, 0x3d, 0xf2, 0x52, 0xff, 0x1f, 0x6c, 0x44, 0x6f, 0x5e, 0xfa, 0x0e, 0x50, 0x36, 0xa2, - 0xd9, 0xbb, 0x20, 0xd7, 0x01, 0xf2, 0x8e, 0x19, 0x4e, 0x55, 0xe5, 0x30, 0xca, 0xe8, 0xaf, 0x3e, 0x4b, 0xe1, 0xd2, - 0x7b, 0x87, 0x79, 0xb2, 0x49, 0x07, 0x9e, 0x05, 0x5b, 0xba, 0xf7, 0x12, 0x49, 0x60, 0xd9, 0x21, 0x21, 0x57, 0x69, - 0xdf, 0xa0, 0xcb, 0xaa, 0x53, 0x6d, 0x52, 0xf4, 0xd3, 0x8e, 0x1b, 0x3c, 0x62, 0x5a, 0x70, 0x8b, 0x74, 0x84, 0x5a, - 0x25, 0x84, 0x31, 0xe3, 0x29, 0x92, 0xd5, 0x80, 0xf0, 0x5a, 0x4d, 0x3c, 0x25, 0x8d, 0x28, 0x0f, 0x0c, 0x13, 0xdb, - 0x7b, 0xb1, 0xf3, 0x63, 0x17, 0xc4, 0x98, 0xdd, 0xba, 0xfb, 0x55, 0xf1, 0xa1, 0x3f, 0xc3, 0xb3, 0xb6, 0x3b, 0x99, - 0x4b, 0x48, 0x90, 0x38, 0xf4, 0x2f, 0x54, 0xfc, 0x5a, 0x23, 0x3c, 0x01, 0x0d, 0x56, 0x09, 0x86, 0xa9, 0x05, 0xbe, - 0x9d, 0xde, 0x67, 0xbd, 0x5e, 0x3d, 0x61, 0xe2, 0xe8, 0x16, 0xe0, 0xda, 0x23, 0x15, 0x97, 0xd3, 0x87, 0x03, 0xbb, - 0xaf, 0x96, 0x1a, 0xe8, 0xaa, 0x64, 0xb2, 0xf8, 0x4b, 0x6f, 0x9c, 0xff, 0x4d, 0x43, 0xd1, 0x81, 0x25, 0xfa, 0x39, - 0xbd, 0x16, 0x3b, 0xf7, 0xc9, 0x27, 0x12, 0x30, 0x1a, 0xc3, 0x93, 0x9c, 0x1e, 0x58, 0x15, 0x6d, 0x26, 0xdc, 0x9f, - 0x17, 0xa7, 0x09, 0x34, 0x76, 0xa0, 0x17, 0x37, 0x99, 0x6f, 0xe3, 0xdd, 0x95, 0xad, 0xee, 0xfd, 0x61, 0x59, 0xd5, - 0xbc, 0x4d, 0xea, 0xa8, 0xbb, 0x48, 0x10, 0xe2, 0x52, 0x47, 0xa8, 0x49, 0x57, 0x6c, 0xad, 0x39, 0x73, 0x71, 0x9a, - 0xc7, 0x56, 0x7e, 0x96, 0x6d, 0x79, 0x20, 0xeb, 0x85, 0x58, 0xd5, 0x6c, 0xd4, 0x04, 0x29, 0x43, 0x5d, 0x88, 0xee, - 0x32, 0xa2, 0x82, 0x16, 0xab, 0x5f, 0xd7, 0xb1, 0x32, 0xdd, 0xa7, 0xe9, 0x68, 0x42, 0xe0, 0xf7, 0x71, 0x00, 0x46, - 0xb4, 0xfe, 0x15, 0x83, 0xa6, 0xd6, 0x28, 0x39, 0x8d, 0x4e, 0x84, 0x88, 0x43, 0xdb, 0x34, 0x06, 0xbc, 0x74, 0xf9, - 0x99, 0x1c, 0x62, 0xaa, 0x05, 0xc6, 0x1b, 0x18, 0xaf, 0x87, 0xb6, 0x98, 0x72, 0xe7, 0xe9, 0x55, 0x65, 0x94, 0xf1, - 0xc9, 0xfd, 0xcc, 0xeb, 0x9e, 0x7d, 0x40, 0xc9, 0x00, 0x84, 0x0a, 0xaf, 0x55, 0x18, 0x32, 0x58, 0x25, 0xf8, 0xf3, - 0x16, 0x63, 0xf0, 0x73, 0xdb, 0x9c, 0x87, 0xf9, 0x10, 0x2b, 0xec, 0x30, 0x0b, 0xf4, 0xe9, 0x79, 0x01, 0xd3, 0x03, - 0xd9, 0xd9, 0xb3, 0xd2, 0x96, 0x51, 0x5a, 0x22, 0x60, 0x57, 0xb8, 0x4e, 0x01, 0x2c, 0x2a, 0x81, 0xc7, 0x41, 0x94, - 0x40, 0x1b, 0xfb, 0x81, 0x68, 0xca, 0x12, 0x52, 0x00, 0xab, 0xd5, 0x9f, 0x01, 0xec, 0xa0, 0x24, 0xdb, 0xce, 0xae, - 0x09, 0x91, 0xd9, 0x7a, 0xa4, 0xbd, 0xea, 0x30, 0x2d, 0xfa, 0xe7, 0xc4, 0x59, 0x9a, 0x18, 0xfb, 0x28, 0x89, 0x8f, - 0x1a, 0x37, 0x30, 0x25, 0x12, 0x74, 0x23, 0x0f, 0xc0, 0xcd, 0x2d, 0xf7, 0xe4, 0xbc, 0xd3, 0x9b, 0x03, 0x18, 0xe4, - 0xf4, 0x4c, 0x7f, 0xed, 0xc0, 0x82, 0xfb, 0xcf, 0x25, 0xea, 0xe8, 0x69, 0x09, 0xc9, 0x04, 0x2e, 0x7e, 0x34, 0xee, - 0x99, 0x06, 0x02, 0x62, 0xcf, 0x85, 0x51, 0x0b, 0x18, 0xfc, 0xd4, 0x11, 0xaf, 0x69, 0xcf, 0x92, 0x0e, 0xa3, 0xd2, - 0xf7, 0x44, 0x3a, 0xd5, 0xb6, 0x47, 0xa6, 0x7b, 0x9d, 0xc6, 0xd4, 0x87, 0xc8, 0x07, 0x53, 0xb8, 0x52, 0x04, 0xc0, - 0xf9, 0x1f, 0x0f, 0x58, 0xee, 0xdf, 0xc4, 0x8a, 0xae, 0x90, 0xe7, 0xda, 0x98, 0x72, 0x58, 0x25, 0x03, 0x5b, 0xc6, - 0x65, 0x47, 0x4c, 0x98, 0x8d, 0x99, 0x56, 0x46, 0x6f, 0xe6, 0xc8, 0x19, 0xa4, 0xb2, 0x31, 0x5c, 0x44, 0x39, 0xb5, - 0x25, 0x20, 0x21, 0xaa, 0x02, 0x18, 0x3c, 0xbd, 0x85, 0x8d, 0x95, 0xd4, 0xa6, 0x74, 0x26, 0x18, 0xaa, 0x21, 0xca, - 0x57, 0x39, 0xb1, 0x9d, 0xca, 0xd1, 0x7c, 0xa0, 0xc9, 0xea, 0x6f, 0x9f, 0x16, 0xee, 0x63, 0x87, 0xe7, 0xbd, 0x4e, - 0x9e, 0x99, 0x14, 0xe8, 0xd3, 0x96, 0xb9, 0x73, 0xe9, 0xc4, 0x65, 0xf1, 0xd2, 0x74, 0xb1, 0x5f, 0x9c, 0xf5, 0x4d, - 0x0a, 0xb2, 0x6c, 0xed, 0xd7, 0x83, 0xb9, 0xc3, 0xb6, 0x98, 0x3a, 0x8f, 0x45, 0x80, 0xcb, 0x12, 0x51, 0xba, 0x96, - 0x09, 0x81, 0x4d, 0xcb, 0xbc, 0x30, 0x9b, 0xd1, 0xe6, 0x0a, 0x2f, 0xcf, 0x47, 0x35, 0xad, 0xc9, 0x15, 0x7a, 0xdd, - 0xa7, 0xd3, 0x77, 0x42, 0xfe, 0x79, 0x39, 0xea, 0x9e, 0x59, 0xca, 0x40, 0x54, 0xed, 0x94, 0x0e, 0x3c, 0xe9, 0xc0, - 0xce, 0xb6, 0xa6, 0x6f, 0xdf, 0x2f, 0xfe, 0xd1, 0x3e, 0x99, 0x3a, 0xb7, 0xa1, 0xb5, 0xe8, 0xf8, 0xfd, 0x1e, 0x51, - 0xbb, 0x64, 0x85, 0x23, 0x04, 0x2a, 0xcf, 0x18, 0xd8, 0xa4, 0xde, 0xcc, 0x59, 0xdc, 0xf8, 0x48, 0xb5, 0xe8, 0x16, - 0x0e, 0xf0, 0x58, 0xdf, 0xfd, 0xea, 0x4f, 0xaa, 0x6e, 0xcf, 0xff, 0xda, 0x9e, 0x84, 0x76, 0x93, 0x7f, 0x6d, 0x6c, - 0xfd, 0xc7, 0xce, 0xc8, 0x72, 0x05, 0xd1, 0xf3, 0xda, 0x7a, 0xb5, 0x24, 0x1c, 0xbc, 0xc3, 0xdc, 0x3d, 0x01, 0xdf, - 0x8e, 0xbf, 0x35, 0xcd, 0x80, 0xa4, 0x65, 0x16, 0xad, 0x8d, 0x5c, 0xe3, 0x25, 0x0c, 0x28, 0x43, 0xd9, 0x15, 0xce, - 0x54, 0x1b, 0x43, 0xf3, 0xeb, 0x1d, 0xb7, 0x6f, 0x1b, 0x6c, 0xdc, 0xa2, 0x5b, 0x45, 0x37, 0x71, 0x61, 0x75, 0x78, - 0xa6, 0xb8, 0xca, 0xe6, 0x54, 0xb9, 0xca, 0xf8, 0xbb, 0x60, 0xa8, 0x0e, 0x03, 0x6e, 0x33, 0xec, 0xc2, 0x75, 0xe7, - 0xa1, 0x0b, 0x15, 0x45, 0x30, 0x2c, 0x48, 0xa5, 0xe5, 0x04, 0x8a, 0x32, 0xb6, 0xc5, 0x86, 0xa2, 0x04, 0xff, 0xfa, - 0xf7, 0x8c, 0x97, 0x51, 0xc0, 0x37, 0x36, 0xb4, 0xc8, 0x6c, 0x85, 0xa6, 0x29, 0xe1, 0x67, 0x98, 0x92, 0xe3, 0xca, - 0x27, 0x8d, 0xdb, 0xe1, 0x7f, 0x15, 0x4d, 0x04, 0x0a, 0xa8, 0x13, 0x0b, 0x09, 0x99, 0x69, 0x33, 0x45, 0xaf, 0x30, - 0x84, 0xae, 0x48, 0xca, 0x07, 0x97, 0x39, 0xf8, 0xae, 0xcd, 0x3d, 0x77, 0x2d, 0x6f, 0x1e, 0x04, 0x5b, 0x92, 0x4d, - 0x90, 0x96, 0x14, 0x32, 0xc9, 0xc2, 0xda, 0x91, 0x81, 0xeb, 0x6b, 0x7b, 0xa1, 0xa0, 0x24, 0x59, 0x10, 0x89, 0xe7, - 0x6b, 0x6d, 0x91, 0x3a, 0x16, 0x7f, 0x61, 0xba, 0x9f, 0xe2, 0xd3, 0x5e, 0xf8, 0x81, 0xfa, 0x3a, 0xdc, 0x75, 0x5f, - 0x45, 0xb3, 0x21, 0x6e, 0xd5, 0x8f, 0x19, 0x15, 0xd7, 0x23, 0xa5, 0xfd, 0x58, 0xfe, 0xf9, 0xfb, 0x0d, 0x8b, 0xc9, - 0x00, 0xc7, 0xc3, 0x9b, 0x9b, 0xa9, 0xc3, 0x8c, 0xbd, 0x86, 0x54, 0x6d, 0xac, 0xbe, 0x01, 0xb9, 0x45, 0x0e, 0xd5, - 0xae, 0x89, 0xa2, 0x0e, 0xb8, 0x99, 0x08, 0x0e, 0xc4, 0x7d, 0x64, 0xce, 0xa8, 0xd7, 0x9e, 0x54, 0x73, 0xba, 0x94, - 0x69, 0xd1, 0x52, 0xcd, 0x18, 0x66, 0xca, 0x74, 0x54, 0x31, 0xa0, 0xae, 0xdc, 0xd9, 0x95, 0xe7, 0x7b, 0x7e, 0x2a, - 0xcb, 0x8b, 0x0d, 0x9b, 0xa1, 0x4b, 0x2d, 0xcc, 0x51, 0xbe, 0xea, 0xf3, 0xb8, 0xbf, 0xf1, 0x8a, 0xf7, 0x7a, 0x87, - 0xa1, 0x43, 0x2d, 0x3d, 0x66, 0x6f, 0xd6, 0xfa, 0x7a, 0x3e, 0xe3, 0x20, 0x2d, 0x6a, 0xa7, 0x82, 0x71, 0xce, 0xa2, - 0x80, 0x05, 0x78, 0x85, 0xdd, 0x11, 0x8c, 0x8f, 0x67, 0xf1, 0x88, 0x7e, 0x64, 0xec, 0xe6, 0x6d, 0xf3, 0x1a, 0x10, - 0xa4, 0xca, 0x8e, 0x7b, 0x4b, 0x17, 0x2a, 0x16, 0xd1, 0xd2, 0x3b, 0xd3, 0x29, 0x94, 0x8f, 0x15, 0x00, 0xa4, 0xee, - 0xf4, 0x66, 0x30, 0x1e, 0xca, 0xcd, 0x01, 0xc2, 0x8d, 0x9c, 0x19, 0x37, 0x26, 0x0a, 0x47, 0x37, 0x06, 0x84, 0x08, - 0x71, 0x35, 0xf0, 0x95, 0x97, 0xb4, 0x4f, 0x54, 0x2c, 0x0d, 0xf1, 0xbd, 0xf2, 0xe0, 0x3e, 0xdc, 0xda, 0x6f, 0x2f, - 0x4e, 0x55, 0xc9, 0xc1, 0x22, 0x14, 0x1b, 0xc5, 0x7b, 0xe3, 0x77, 0x2f, 0xec, 0x7e, 0x62, 0x31, 0xd7, 0x12, 0x01, - 0xe5, 0x96, 0xef, 0x97, 0x56, 0x4d, 0x9c, 0x3f, 0xfd, 0x87, 0x0f, 0xe5, 0x12, 0x72, 0xe4, 0xab, 0x58, 0x76, 0x40, - 0x66, 0xbe, 0xa2, 0x9f, 0x45, 0x59, 0x4d, 0xe1, 0x53, 0xde, 0xc2, 0xdd, 0x75, 0xc7, 0xb5, 0x0e, 0x00, 0x59, 0x38, - 0x44, 0xb3, 0xd1, 0xd3, 0x2e, 0x89, 0xd0, 0x36, 0xda, 0xf8, 0x96, 0x47, 0x9a, 0x51, 0x51, 0x51, 0x34, 0x2e, 0xcd, - 0x46, 0x3d, 0xa4, 0x20, 0x9e, 0xa0, 0x17, 0xdf, 0x86, 0x80, 0x7c, 0x74, 0xed, 0x9d, 0x12, 0xb3, 0x74, 0x6b, 0xe1, - 0xfb, 0xfe, 0x46, 0xa2, 0x9e, 0x02, 0xfd, 0x28, 0xc2, 0x64, 0x41, 0x33, 0xf2, 0x95, 0xff, 0x2e, 0x00, 0x6f, 0x62, - 0xd1, 0x3f, 0xb1, 0xf0, 0x67, 0xb2, 0xee, 0xe1, 0xab, 0x9d, 0xb8, 0xde, 0x2e, 0x9a, 0xd3, 0x41, 0xfb, 0x10, 0x94, - 0xaa, 0xbe, 0xc7, 0xe1, 0x4d, 0xa1, 0xf5, 0xcb, 0x8e, 0x5d, 0xb0, 0x15, 0x05, 0x03, 0x9e, 0x75, 0x4a, 0x34, 0x31, - 0x5d, 0x97, 0x15, 0x01, 0xc6, 0x12, 0x27, 0x90, 0x5b, 0x9d, 0xb3, 0x74, 0x94, 0x9b, 0xb3, 0x9b, 0x3c, 0x6b, 0x27, - 0xd7, 0xd1, 0xbe, 0x9d, 0xcc, 0x4a, 0x59, 0xe5, 0xba, 0x21, 0x34, 0x7b, 0xf9, 0xe4, 0x2c, 0x94, 0x8b, 0x42, 0x1d, - 0x15, 0x37, 0xb4, 0x6a, 0x4d, 0x56, 0xc0, 0xca, 0xc9, 0x45, 0xab, 0xf2, 0xe6, 0xe9, 0xd0, 0xb8, 0xc9, 0x36, 0xfd, - 0x58, 0xd1, 0x76, 0x07, 0x0a, 0xaf, 0x14, 0xd6, 0xf6, 0x1c, 0x6c, 0xc3, 0x89, 0x06, 0xc7, 0x7d, 0xbb, 0x6d, 0x40, - 0x54, 0x20, 0xbb, 0x98, 0x50, 0x62, 0x6b, 0xf9, 0x2f, 0x0e, 0x28, 0xbe, 0xbd, 0x9a, 0x5e, 0x47, 0x31, 0x32, 0x7c, - 0x53, 0xff, 0x1e, 0x08, 0xf0, 0x2f, 0x7c, 0x60, 0x6a, 0x67, 0x54, 0x45, 0x28, 0x6b, 0x37, 0xab, 0xd4, 0x1f, 0x15, - 0xb9, 0xa5, 0x49, 0x6a, 0xb6, 0x79, 0x7a, 0x82, 0x29, 0x2b, 0xda, 0x9b, 0xc3, 0x06, 0x7b, 0x74, 0x6d, 0x44, 0xd8, - 0x60, 0x42, 0x1c, 0xff, 0x83, 0x9d, 0x00, 0x9d, 0x48, 0xf1, 0x82, 0xcb, 0x71, 0x65, 0x29, 0x9a, 0x12, 0xcd, 0x4b, - 0x51, 0xfb, 0x14, 0xe6, 0x3d, 0xaf, 0x02, 0xae, 0x9b, 0x93, 0xa3, 0x6e, 0x87, 0x3e, 0x71, 0x8a, 0x38, 0xcf, 0x81, - 0x08, 0x7b, 0x2a, 0xa7, 0x5d, 0xbd, 0x59, 0x5b, 0x86, 0xb8, 0x6e, 0x56, 0x88, 0x90, 0x7c, 0x18, 0xa7, 0x62, 0x88, - 0xb1, 0x7d, 0x23, 0x73, 0xde, 0xe3, 0xab, 0x85, 0x28, 0xac, 0x70, 0x11, 0x8f, 0x91, 0xa5, 0x3f, 0x79, 0x05, 0xd1, - 0x9d, 0x51, 0x93, 0x60, 0xd6, 0xea, 0x64, 0xa4, 0x38, 0x53, 0xff, 0x02, 0x02, 0x43, 0x6d, 0x90, 0xd1, 0x41, 0x4d, - 0x95, 0xf9, 0xd1, 0xd3, 0x88, 0x1b, 0x1f, 0x38, 0xaa, 0x46, 0x9b, 0x90, 0x71, 0xa6, 0xd8, 0x16, 0xbf, 0x35, 0x77, - 0x4b, 0x00, 0x66, 0x77, 0x1d, 0xa8, 0x15, 0x71, 0x24, 0xa1, 0x55, 0x7f, 0xae, 0xfe, 0x7a, 0x89, 0x44, 0x71, 0x4e, - 0xe8, 0x63, 0xa0, 0xc5, 0x47, 0x98, 0xae, 0xe6, 0x62, 0x1b, 0x87, 0x1d, 0x47, 0xa2, 0x8a, 0xd3, 0xbb, 0xe8, 0x72, - 0x3f, 0x93, 0x60, 0xf7, 0x13, 0x22, 0x9e, 0xef, 0xad, 0x0b, 0x35, 0x0b, 0x47, 0x1f, 0xb6, 0x3f, 0x09, 0x12, 0x32, - 0xd4, 0x5f, 0x0b, 0x37, 0x47, 0xed, 0xd5, 0x4b, 0x2d, 0xa3, 0x0d, 0x3f, 0x2d, 0xa2, 0xc5, 0xa9, 0x00, 0x94, 0x0c, - 0xa3, 0xf3, 0xcf, 0x5f, 0xec, 0xb0, 0x9f, 0x83, 0x73, 0x0c, 0x26, 0x0f, 0x78, 0xc0, 0xcd, 0xdd, 0x4f, 0xe8, 0xda, - 0x52, 0xce, 0x08, 0x67, 0xac, 0x0d, 0x09, 0x56, 0xc6, 0xb9, 0x66, 0x6b, 0xe3, 0x45, 0xc3, 0x09, 0xe1, 0x08, 0x3a, - 0x68, 0x8c, 0x7a, 0x9e, 0x33, 0x9a, 0xa7, 0x58, 0xfd, 0xca, 0x19, 0x6f, 0xf9, 0x81, 0x9d, 0xcb, 0x15, 0x04, 0x55, - 0x17, 0x55, 0x62, 0x4d, 0x27, 0xda, 0x81, 0xcb, 0xfd, 0x25, 0x7b, 0xca, 0x22, 0x7f, 0xbb, 0xc0, 0xa4, 0xa6, 0x42, - 0x21, 0x67, 0x85, 0x1b, 0xda, 0x15, 0x82, 0xd5, 0x6a, 0xdc, 0xf0, 0x3b, 0x6f, 0x59, 0x96, 0xaa, 0x4e, 0xed, 0x46, - 0x35, 0x34, 0xc3, 0x84, 0x29, 0x6e, 0x68, 0x19, 0xdf, 0x91, 0x94, 0xd8, 0x59, 0xd7, 0x06, 0x73, 0xfa, 0x1f, 0x52, - 0x9c, 0x0a, 0x2d, 0x44, 0xa9, 0xfa, 0x1c, 0x34, 0x3a, 0x31, 0x4d, 0xd5, 0x79, 0x23, 0x77, 0x26, 0x07, 0x83, 0x9a, - 0xb2, 0xb1, 0x53, 0xf3, 0x8e, 0xe9, 0xc8, 0x0c, 0xfe, 0x8e, 0xfc, 0xe4, 0x21, 0xab, 0x65, 0x72, 0x99, 0xe8, 0x67, - 0xbd, 0xf1, 0xaa, 0x00, 0x14, 0xd6, 0x31, 0xa8, 0xd0, 0x3c, 0x6b, 0x2a, 0x6b, 0x55, 0x65, 0xba, 0x09, 0x5f, 0x75, - 0x4b, 0x03, 0x05, 0xcf, 0x94, 0xa7, 0x10, 0x51, 0x53, 0xe2, 0xa4, 0xd5, 0x43, 0xa8, 0x01, 0xe5, 0xe8, 0xbc, 0x88, - 0xb9, 0x8e, 0x95, 0x2a, 0x1b, 0xff, 0x72, 0xef, 0xa3, 0x75, 0xeb, 0x20, 0xef, 0x67, 0x36, 0xea, 0xfd, 0x22, 0x55, - 0x4e, 0xa1, 0xcf, 0x8f, 0x34, 0x8d, 0x35, 0x09, 0xb6, 0x71, 0x32, 0x90, 0x0a, 0x3a, 0xa9, 0xc0, 0xff, 0xcb, 0x94, - 0x33, 0x56, 0x4c, 0x2a, 0x40, 0xc5, 0x62, 0xed, 0x9a, 0x7f, 0xdd, 0x87, 0x05, 0x93, 0xa0, 0xee, 0x1f, 0x80, 0x5e, - 0x0b, 0xb9, 0x90, 0x5f, 0xad, 0xb7, 0xa1, 0xec, 0x7c, 0xc3, 0x49, 0xeb, 0x73, 0xf5, 0x93, 0x23, 0x17, 0xb1, 0x9a, - 0xa2, 0xcd, 0xb4, 0x9e, 0x3a, 0x4b, 0x98, 0xf0, 0xd3, 0x72, 0x6e, 0xba, 0x41, 0xe6, 0x1a, 0x47, 0xca, 0xcb, 0xec, - 0xe3, 0xa8, 0x55, 0x66, 0xe9, 0xd8, 0x86, 0x2a, 0x6a, 0x73, 0x3c, 0x73, 0xc6, 0xf8, 0x62, 0x4f, 0x9a, 0x6a, 0x57, - 0x56, 0xf2, 0xcd, 0xb5, 0x98, 0x37, 0x87, 0x32, 0x45, 0x3d, 0x32, 0xad, 0x93, 0x8b, 0x13, 0x2a, 0xb3, 0x02, 0xc0, - 0xdb, 0x90, 0x6c, 0x84, 0xc0, 0x2e, 0xd8, 0x8f, 0xb5, 0x78, 0xe9, 0x2e, 0xa5, 0x51, 0x82, 0x97, 0x10, 0x2b, 0xfa, - 0x87, 0xd2, 0x42, 0x83, 0x54, 0x57, 0x94, 0x2c, 0x0d, 0xf5, 0xdf, 0x4a, 0x0f, 0x27, 0x39, 0x6a, 0x70, 0x0e, 0xb5, - 0xc7, 0xc2, 0xa8, 0xf7, 0x63, 0xd2, 0xa3, 0x3c, 0xd6, 0x4b, 0x81, 0x4d, 0x96, 0xc0, 0xca, 0x09, 0x76, 0x17, 0x20, - 0xe5, 0xb5, 0x87, 0xbe, 0x56, 0x64, 0xc2, 0xe3, 0xf3, 0xe4, 0xd6, 0xa5, 0x65, 0xe0, 0x15, 0xf4, 0xae, 0xbd, 0xa1, - 0xd2, 0x02, 0x77, 0xbf, 0xc8, 0x95, 0xff, 0xe8, 0x50, 0x24, 0x1d, 0xf1, 0x54, 0x12, 0x78, 0x2b, 0xa9, 0xc1, 0xc0, - 0xad, 0x65, 0xc3, 0xb5, 0x69, 0x1b, 0x7d, 0xa8, 0x8f, 0xe3, 0x3b, 0x46, 0xab, 0xe0, 0x3f, 0x9f, 0x7e, 0xc3, 0x38, - 0xb4, 0xe0, 0xd9, 0xaa, 0x54, 0x59, 0xd7, 0x53, 0x47, 0xb2, 0xfd, 0xd5, 0xce, 0x5b, 0x04, 0xb3, 0x70, 0x25, 0x0b, - 0x4d, 0x02, 0x3a, 0xb6, 0x49, 0x16, 0xb8, 0x4d, 0x81, 0x99, 0x47, 0x3f, 0x45, 0x6f, 0x23, 0xd5, 0x38, 0x52, 0xb5, - 0x68, 0x12, 0xe3, 0x70, 0x41, 0x34, 0x79, 0x73, 0xb7, 0x2a, 0x02, 0x19, 0x1c, 0xc0, 0x2d, 0xbf, 0x33, 0xce, 0x3d, - 0xf5, 0x91, 0xd6, 0x3a, 0xf0, 0xbb, 0x6e, 0xb2, 0x5d, 0xda, 0xa1, 0x51, 0x4b, 0xf4, 0xb6, 0x1d, 0x35, 0x1a, 0x64, - 0xd8, 0x23, 0xc5, 0xd8, 0xbd, 0x8f, 0xcf, 0xea, 0x31, 0x83, 0x2c, 0xd1, 0x01, 0x5f, 0x77, 0x0d, 0x54, 0x2c, 0x32, - 0x90, 0xbb, 0x0b, 0x21, 0x51, 0x87, 0x6d, 0xb4, 0x00, 0x50, 0xfa, 0x04, 0xab, 0xef, 0xc4, 0x2d, 0xf5, 0x06, 0x94, - 0xf9, 0x3e, 0xa4, 0x94, 0x42, 0x7d, 0x51, 0x91, 0x29, 0x67, 0x8b, 0xc5, 0x8c, 0x22, 0x8c, 0x3c, 0x11, 0x19, 0x6a, - 0x13, 0xc4, 0x08, 0x9c, 0xde, 0x32, 0xaa, 0x7e, 0x6c, 0x2f, 0x03, 0x2d, 0xed, 0xb5, 0x88, 0xa9, 0xca, 0x19, 0xcf, - 0x01, 0x94, 0x80, 0xc1, 0x55, 0x00, 0x67, 0xa6, 0x7a, 0x57, 0xc6, 0x9c, 0x58, 0x66, 0x05, 0x0f, 0x94, 0x4e, 0x2e, - 0xc6, 0xd7, 0xc0, 0xf9, 0x8f, 0xad, 0x89, 0xab, 0xf8, 0xeb, 0xd7, 0x2d, 0x9f, 0x67, 0xff, 0x97, 0x89, 0x76, 0x75, - 0x06, 0xac, 0x9c, 0xb0, 0xcf, 0x13, 0xc4, 0xeb, 0x06, 0xdb, 0xcb, 0xd6, 0x62, 0xc5, 0x93, 0x5e, 0x7f, 0x6c, 0xb5, - 0xa4, 0x2c, 0xab, 0xe4, 0x57, 0x1b, 0x08, 0xa4, 0xf1, 0x9d, 0x49, 0x64, 0x90, 0x0a, 0x92, 0x62, 0xba, 0x11, 0xfc, - 0xee, 0x5b, 0xef, 0x61, 0x47, 0x1a, 0x78, 0xd9, 0xea, 0xc2, 0xf0, 0x99, 0xba, 0x5d, 0xd3, 0x49, 0xce, 0xe0, 0xcc, - 0x9b, 0x09, 0x47, 0x5b, 0xef, 0xf2, 0xe5, 0x0a, 0x2d, 0xfa, 0x3c, 0xf4, 0x2b, 0xba, 0x4d, 0x5a, 0x96, 0xc7, 0x3d, - 0xcc, 0xa0, 0xfe, 0xaf, 0x62, 0xcd, 0x69, 0xf4, 0x55, 0x51, 0x5f, 0x7a, 0x41, 0x2b, 0xcd, 0x6d, 0xad, 0x2d, 0xe4, - 0x74, 0x6e, 0x91, 0x7b, 0x30, 0x34, 0xed, 0xfb, 0x8f, 0x2a, 0xc2, 0x92, 0x3d, 0xa5, 0xad, 0xf7, 0xc9, 0x45, 0x2f, - 0xd5, 0xb9, 0x11, 0xff, 0x96, 0x53, 0x79, 0xd3, 0x3a, 0x6a, 0x64, 0x27, 0xfe, 0x0f, 0xd6, 0x4d, 0x94, 0x71, 0xb9, - 0x4e, 0xee, 0xb4, 0x83, 0xe2, 0xa8, 0x4b, 0x8e, 0x87, 0x38, 0xd7, 0x8c, 0x46, 0x7a, 0x25, 0xcc, 0x33, 0xa7, 0x55, - 0x85, 0x1e, 0x8b, 0x06, 0xc9, 0x1a, 0x1a, 0x90, 0x04, 0xa8, 0xc9, 0x09, 0x71, 0xea, 0x4e, 0x70, 0x6b, 0x40, 0x72, - 0x72, 0x89, 0x90, 0x9c, 0x16, 0xde, 0xe5, 0xe7, 0x0d, 0x19, 0xa2, 0x9c, 0xd7, 0x37, 0xb1, 0x23, 0x2a, 0x3e, 0x8b, - 0x6e, 0xb9, 0x6f, 0x11, 0x1a, 0x6d, 0x1f, 0x34, 0x9a, 0x8e, 0x39, 0xb0, 0xcb, 0x9b, 0x35, 0x68, 0x39, 0x33, 0x08, - 0xf9, 0xe9, 0x19, 0x34, 0x61, 0xc0, 0x6c, 0x85, 0x10, 0x73, 0x94, 0x6c, 0x95, 0x9a, 0x34, 0x06, 0xf5, 0xc4, 0x4e, - 0x1c, 0xa8, 0xcf, 0xcf, 0xba, 0x59, 0x29, 0xd9, 0x9c, 0x9a, 0xda, 0xf4, 0x03, 0xd8, 0xe2, 0x89, 0x36, 0x1f, 0x28, - 0xc3, 0x20, 0x0d, 0x57, 0x25, 0xc2, 0xdf, 0xa8, 0xa8, 0xf3, 0x65, 0x3e, 0x6f, 0xd8, 0x46, 0xd0, 0x88, 0x21, 0x03, - 0xb3, 0x13, 0xac, 0x81, 0x20, 0x58, 0x16, 0x67, 0x72, 0x96, 0xd2, 0xd9, 0x38, 0x96, 0x58, 0x0b, 0x05, 0xb4, 0xbc, - 0x4d, 0xce, 0x1d, 0x04, 0x50, 0x46, 0xa2, 0xc4, 0xb2, 0x8d, 0x88, 0x3e, 0x30, 0x09, 0xde, 0x10, 0x2b, 0xf8, 0x05, - 0xdf, 0x50, 0x3a, 0xe9, 0x40, 0x6d, 0x92, 0x3b, 0x85, 0xaa, 0x0c, 0x0e, 0xc6, 0xe1, 0x15, 0xff, 0xed, 0xca, 0x0f, - 0x0e, 0x6d, 0x22, 0xee, 0x2a, 0xe0, 0x92, 0xd1, 0x73, 0x08, 0xea, 0xe4, 0xda, 0xb2, 0x89, 0xef, 0x34, 0xda, 0xde, - 0x55, 0xb5, 0x2b, 0x8e, 0xf8, 0xfc, 0x51, 0x60, 0x14, 0xa4, 0x5c, 0xce, 0xfe, 0x8d, 0x27, 0x69, 0xa2, 0x39, 0xb7, - 0xef, 0x1d, 0x2e, 0x16, 0x69, 0xa6, 0x3a, 0x35, 0xbd, 0xb9, 0x58, 0xb5, 0x0f, 0x46, 0xae, 0xb5, 0x67, 0x67, 0x1c, - 0x47, 0x20, 0x05, 0xe5, 0x03, 0xfe, 0x0b, 0xa9, 0x1a, 0xf2, 0xf9, 0xd0, 0xcf, 0x01, 0xd5, 0x4c, 0xf1, 0x69, 0xd5, - 0xd6, 0xbe, 0x49, 0xb5, 0xe0, 0x7f, 0x73, 0x58, 0xa4, 0x75, 0xfd, 0xfd, 0xf9, 0x8b, 0xde, 0x36, 0xd2, 0xf1, 0x23, - 0xb1, 0x92, 0xfa, 0x13, 0xa0, 0xac, 0xbd, 0x19, 0xb3, 0x76, 0x10, 0x4e, 0x63, 0x4a, 0x21, 0xfa, 0x4f, 0xe2, 0x53, - 0x4f, 0x65, 0xf0, 0x0d, 0xd4, 0x0f, 0xde, 0xc4, 0x68, 0xe9, 0x67, 0x6d, 0x6a, 0x21, 0xfc, 0x2d, 0xe6, 0x8b, 0x5a, - 0x3d, 0xe8, 0x38, 0x0f, 0xb9, 0x78, 0xc5, 0xea, 0x3f, 0x7d, 0xf1, 0xe5, 0x6c, 0x61, 0xb0, 0x96, 0xb5, 0x07, 0xff, - 0x8f, 0xf3, 0x00, 0x20, 0x5b, 0x61, 0x58, 0x4b, 0x34, 0xd3, 0x2f, 0xad, 0xa7, 0x00, 0xdf, 0x9d, 0xa7, 0x52, 0x6c, - 0x4d, 0x8b, 0x95, 0xa9, 0x67, 0x3a, 0xa8, 0x57, 0x6a, 0x6b, 0xdc, 0x37, 0xd6, 0x87, 0xd1, 0xd0, 0x77, 0x30, 0x57, - 0xbc, 0x7e, 0x8c, 0xe9, 0xee, 0x9f, 0x26, 0x26, 0x86, 0xfd, 0x4e, 0x75, 0x4a, 0x9a, 0xa5, 0xbe, 0x6d, 0xc8, 0x99, - 0xdd, 0x26, 0x70, 0xdf, 0x64, 0x8a, 0x90, 0xe3, 0x7d, 0x72, 0x94, 0xa2, 0xa6, 0x7d, 0x4b, 0x25, 0xab, 0x5b, 0x0f, - 0x69, 0xc4, 0x2c, 0x35, 0xd0, 0xfb, 0xe2, 0x55, 0x81, 0x81, 0x87, 0xea, 0xbc, 0x7e, 0x8b, 0x02, 0x9e, 0xc1, 0x47, - 0xcb, 0xac, 0xda, 0xba, 0x04, 0x8e, 0x51, 0xeb, 0xc0, 0xfd, 0xf2, 0xc0, 0x1f, 0x29, 0xfa, 0xe2, 0x6d, 0xe4, 0x60, - 0x83, 0xd7, 0x53, 0x83, 0x53, 0x1e, 0x9e, 0x8d, 0xf5, 0x31, 0xe3, 0xa7, 0x95, 0xa3, 0xb0, 0x67, 0x5c, 0x3c, 0x99, - 0x5d, 0x8c, 0xc3, 0xa6, 0xdb, 0x2a, 0x27, 0x4a, 0xa6, 0x4c, 0xd7, 0x64, 0x7e, 0xc6, 0x85, 0x9e, 0x37, 0x6b, 0xb5, - 0x84, 0xcd, 0x0f, 0xfe, 0x70, 0x53, 0x5c, 0x19, 0x27, 0xa3, 0xd0, 0xfe, 0x1f, 0xd9, 0x99, 0xa6, 0x77, 0xa1, 0x46, - 0xe0, 0x52, 0x70, 0xb5, 0x54, 0x96, 0x46, 0xda, 0xcf, 0xf6, 0xe9, 0xfb, 0x24, 0x5f, 0x41, 0x9e, 0xfe, 0x92, 0x15, - 0x1b, 0x73, 0x92, 0x64, 0xff, 0xa8, 0x14, 0x32, 0x87, 0xaa, 0x45, 0x3b, 0x46, 0x5b, 0xf9, 0x09, 0x41, 0x7d, 0xd9, - 0x21, 0xea, 0x00, 0xcc, 0xb6, 0x4a, 0x79, 0xbf, 0x18, 0x68, 0x46, 0x51, 0xb6, 0x1c, 0xf4, 0xb5, 0x61, 0x06, 0x07, - 0xaf, 0x1a, 0xd6, 0xef, 0xbd, 0xac, 0x55, 0x32, 0x52, 0x69, 0xb3, 0xcc, 0x51, 0x6a, 0xf2, 0x74, 0xbf, 0xd4, 0xb9, - 0xe8, 0x9a, 0x38, 0xf8, 0xd9, 0xda, 0xf7, 0x60, 0xd7, 0x4e, 0xcb, 0xae, 0x14, 0xe6, 0x06, 0xc7, 0x79, 0xcc, 0x71, - 0x65, 0x03, 0x11, 0x6b, 0x16, 0x5a, 0xde, 0x14, 0x2d, 0x52, 0x77, 0xea, 0xbb, 0xb3, 0xec, 0x26, 0x80, 0xad, 0x62, - 0xef, 0xa1, 0xe5, 0xdb, 0x67, 0xe9, 0x8d, 0x0e, 0x6c, 0x6b, 0xe3, 0x5e, 0xc7, 0x37, 0x16, 0x84, 0x9e, 0x2c, 0xaf, - 0xce, 0xa8, 0x8e, 0x3b, 0xa7, 0xf9, 0xfc, 0x50, 0x31, 0x96, 0x6e, 0x93, 0xe8, 0x9c, 0x8f, 0xe4, 0x09, 0xb2, 0x0c, - 0x15, 0xcb, 0x69, 0x60, 0x2d, 0x23, 0x68, 0xec, 0x24, 0x7d, 0xe5, 0x91, 0xac, 0xc6, 0x8a, 0xf9, 0x47, 0xa0, 0x76, - 0xae, 0xec, 0xb8, 0x6d, 0x86, 0xa4, 0x5a, 0xae, 0xb4, 0x46, 0x30, 0x0c, 0x8d, 0x7f, 0x2d, 0x44, 0xa2, 0xda, 0x4a, - 0x40, 0x02, 0x0e, 0x67, 0x29, 0xa8, 0xdd, 0x6d, 0x79, 0xf3, 0x6e, 0x94, 0x1e, 0x51, 0xa4, 0xa2, 0x56, 0x54, 0x4e, - 0xf1, 0x86, 0xb2, 0xf5, 0x4c, 0x34, 0x01, 0x13, 0x8d, 0x62, 0x23, 0x33, 0x28, 0x6f, 0xb7, 0x2a, 0xe4, 0x5e, 0xae, - 0xfb, 0xb7, 0x57, 0xef, 0x28, 0x0d, 0x9b, 0xbe, 0x12, 0x92, 0x06, 0xad, 0x50, 0x44, 0x7c, 0xc0, 0x8e, 0x31, 0x8e, - 0xae, 0xc9, 0xf4, 0x99, 0x3a, 0x30, 0x46, 0x75, 0x89, 0x94, 0x2f, 0xcd, 0x9f, 0xbd, 0xf1, 0xea, 0x25, 0xb0, 0xf5, - 0x3b, 0x5d, 0x6b, 0x4d, 0x66, 0xde, 0x96, 0x52, 0x2b, 0x91, 0x6e, 0x32, 0x22, 0x8d, 0xff, 0x4c, 0xb3, 0x6f, 0x26, - 0xf2, 0x87, 0x1d, 0xed, 0xc0, 0x40, 0x86, 0xf4, 0x66, 0xb3, 0x39, 0xa7, 0x6a, 0x16, 0x00, 0x0a, 0xff, 0xd5, 0xba, - 0x0f, 0x66, 0x6b, 0xa6, 0xa9, 0x88, 0xe0, 0xb3, 0x30, 0x34, 0x6f, 0xe1, 0x90, 0xd5, 0x69, 0x04, 0xe0, 0x20, 0x09, - 0x81, 0xcc, 0xd9, 0x5c, 0x6f, 0x08, 0xaa, 0xd8, 0xdb, 0xb0, 0x46, 0x9f, 0x42, 0xe8, 0x7f, 0xe4, 0xd3, 0xcf, 0xf9, - 0x5e, 0x45, 0x51, 0x0c, 0x5d, 0x1d, 0x0a, 0x87, 0xd6, 0xdf, 0x64, 0xd2, 0x78, 0x97, 0x2c, 0x14, 0x83, 0xfa, 0x8b, - 0xbd, 0x43, 0xcb, 0xdc, 0x74, 0x67, 0x03, 0x0b, 0x97, 0x0a, 0x06, 0x52, 0x2c, 0x42, 0x48, 0x73, 0x83, 0xb3, 0x7e, - 0xeb, 0xb1, 0x7c, 0xe9, 0x02, 0x4d, 0xdf, 0xca, 0xe3, 0x31, 0x3e, 0xfb, 0x76, 0xbc, 0xe3, 0x13, 0x66, 0x5a, 0x66, - 0x89, 0x4a, 0x0a, 0xe9, 0x93, 0xff, 0x0e, 0xa3, 0x96, 0xc7, 0x84, 0x05, 0xd3, 0xea, 0xee, 0xa9, 0x14, 0xc5, 0xce, - 0x73, 0x58, 0x53, 0x2f, 0xa0, 0x0e, 0x85, 0x9b, 0xea, 0x03, 0xbb, 0x12, 0x41, 0x6a, 0x53, 0x00, 0x30, 0xfe, 0x08, - 0x80, 0x88, 0x07, 0x99, 0x57, 0xaa, 0x25, 0x64, 0xb8, 0x59, 0x4e, 0xa4, 0xbb, 0x8b, 0x51, 0xe2, 0x9b, 0x23, 0x02, - 0xb4, 0xa5, 0x66, 0x18, 0x9e, 0xc9, 0x6f, 0x73, 0x79, 0x13, 0x2e, 0x81, 0xed, 0x1a, 0xc1, 0x1b, 0x21, 0x6d, 0xd6, - 0x7e, 0x38, 0x02, 0xaa, 0xb6, 0x01, 0x51, 0xfa, 0x4d, 0x79, 0x63, 0xde, 0x88, 0x14, 0xaa, 0xd5, 0xce, 0xee, 0x4d, - 0x5a, 0xa7, 0x0d, 0xab, 0xe1, 0x29, 0xdc, 0x54, 0xa9, 0x6d, 0x23, 0xd7, 0xf6, 0x7f, 0x92, 0x82, 0x9c, 0x4d, 0xdd, - 0xd5, 0x6d, 0xf7, 0xfb, 0xa7, 0x09, 0x38, 0xfc, 0x24, 0x31, 0xbe, 0xfb, 0xd5, 0x32, 0xfb, 0x3f, 0xb6, 0xf2, 0xa0, - 0x04, 0x0f, 0xa7, 0x20, 0x9f, 0x62, 0x0d, 0xd7, 0x90, 0x7a, 0xf2, 0xae, 0xaf, 0xbb, 0x80, 0xc0, 0xfa, 0x2d, 0xb9, - 0x13, 0xef, 0x32, 0x82, 0x53, 0x00, 0xdb, 0xd6, 0x11, 0x58, 0xeb, 0xe6, 0x3b, 0x90, 0x82, 0x18, 0xf9, 0x2d, 0x92, - 0xff, 0xb3, 0x32, 0x37, 0xfc, 0x48, 0x51, 0xdc, 0x9c, 0x4b, 0x17, 0xd1, 0x93, 0x55, 0xd8, 0x0e, 0x1b, 0x55, 0x80, - 0x23, 0xb0, 0xf0, 0x7e, 0x6e, 0x26, 0xff, 0x0c, 0xa1, 0x9d, 0xab, 0x33, 0xc5, 0xa1, 0x18, 0xd5, 0x4f, 0x75, 0x01, - 0xca, 0xc3, 0x64, 0xc4, 0xa6, 0x26, 0xb4, 0x18, 0x0b, 0x4b, 0x97, 0x24, 0x80, 0x40, 0x7b, 0xa8, 0x25, 0x32, 0x97, - 0x6b, 0x91, 0x5d, 0x32, 0xee, 0xd9, 0x56, 0x2c, 0x5d, 0xfb, 0x98, 0xd7, 0xd9, 0x33, 0x70, 0xe3, 0x3c, 0x06, 0x5f, - 0xdc, 0xd9, 0x52, 0x58, 0xe9, 0x19, 0xb2, 0x3a, 0x3b, 0x57, 0xe2, 0xb0, 0x4d, 0xb6, 0x1f, 0x15, 0xec, 0xee, 0xdb, - 0x5b, 0x22, 0x0b, 0xc4, 0xe0, 0x3f, 0xad, 0x35, 0x59, 0xeb, 0x6f, 0xe4, 0x00, 0xbe, 0x85, 0x95, 0x7c, 0x41, 0x33, - 0xe0, 0x72, 0x77, 0x73, 0x40, 0xea, 0x81, 0x4f, 0x26, 0xac, 0xaa, 0x72, 0xcd, 0xcd, 0x46, 0xa6, 0x09, 0x9a, 0x10, - 0xff, 0xbf, 0xb2, 0xd5, 0x10, 0x1b, 0x80, 0x27, 0x63, 0xdf, 0x7c, 0xd9, 0x85, 0xc1, 0x66, 0xa1, 0xc5, 0x16, 0xf6, - 0xe1, 0x2d, 0xa7, 0xe2, 0x75, 0x73, 0x03, 0x35, 0xfc, 0x20, 0x81, 0x95, 0xef, 0x12, 0xaa, 0xf9, 0x9e, 0x38, 0xf6, - 0xbd, 0x57, 0xbe, 0x7a, 0x4e, 0x8f, 0x40, 0xd3, 0xe8, 0xac, 0x99, 0xf4, 0xe4, 0x70, 0x6e, 0x0c, 0x55, 0x23, 0xaf, - 0x95, 0xb7, 0x07, 0x57, 0xab, 0xbf, 0x3e, 0x9b, 0xf3, 0x36, 0x3f, 0xa2, 0x1f, 0x5d, 0x63, 0x23, 0x66, 0x71, 0xc2, - 0x57, 0xd7, 0x47, 0x91, 0x50, 0x51, 0xc4, 0xc5, 0x87, 0x75, 0x9f, 0x36, 0xae, 0xb7, 0x8e, 0x6e, 0xf1, 0x2e, 0xc0, - 0x9c, 0x92, 0x54, 0x9d, 0x6d, 0x67, 0xe8, 0x0a, 0xbe, 0x97, 0xb5, 0xc5, 0xf1, 0xa5, 0xb5, 0x6e, 0xcb, 0xcb, 0xae, - 0xbc, 0x37, 0x46, 0x5d, 0xb4, 0x60, 0xd7, 0x77, 0x9c, 0xbc, 0xd5, 0xc8, 0xfd, 0xea, 0xa9, 0x2d, 0x96, 0x50, 0x40, - 0x1b, 0x5a, 0xbe, 0x20, 0x3b, 0xc6, 0x9e, 0x8d, 0x4e, 0xa5, 0xc9, 0x53, 0xf4, 0xba, 0xfb, 0xcc, 0x23, 0x1e, 0xd6, - 0x81, 0xae, 0x9c, 0x06, 0x1d, 0xff, 0xc2, 0x7f, 0x79, 0x59, 0xaa, 0xb7, 0x2a, 0xae, 0xbd, 0x12, 0x00, 0x93, 0x2a, - 0x9f, 0xf4, 0xf2, 0xf7, 0x41, 0x10, 0x19, 0xd9, 0x08, 0xf1, 0x4c, 0x54, 0x96, 0x00, 0x3a, 0xae, 0x72, 0xf1, 0xce, - 0x74, 0xd0, 0x2f, 0x67, 0x22, 0x11, 0x39, 0x03, 0x6d, 0x1b, 0x14, 0x0a, 0x91, 0x7a, 0xbb, 0x08, 0xe2, 0x1e, 0x45, - 0x4c, 0x34, 0xd7, 0x5d, 0xdf, 0xaf, 0xd1, 0x71, 0x34, 0x36, 0xa3, 0x76, 0xfb, 0x5b, 0xc1, 0x14, 0x48, 0x89, 0x83, - 0x81, 0xba, 0xa2, 0x22, 0x1e, 0xff, 0xf1, 0x40, 0xfb, 0x25, 0x35, 0x9c, 0xb2, 0xc3, 0x78, 0x15, 0x5f, 0x59, 0x55, - 0xb5, 0xe2, 0x97, 0x88, 0x99, 0x21, 0x88, 0x37, 0x1a, 0xe9, 0x95, 0xcd, 0x5e, 0xcd, 0x64, 0xa2, 0x38, 0x29, 0x2c, - 0x8f, 0x6b, 0xd7, 0x84, 0x75, 0x00, 0x6b, 0xf5, 0xd1, 0xa1, 0xa5, 0xf8, 0xfb, 0xec, 0x8f, 0x4b, 0x8e, 0x99, 0xe7, - 0xcf, 0xf0, 0xbf, 0xcd, 0x2e, 0x97, 0xfc, 0xd1, 0x3d, 0xc9, 0xf6, 0x3d, 0x76, 0x00, 0xcd, 0x32, 0xa5, 0x8e, 0x32, - 0x86, 0x00, 0xc0, 0x41, 0xe2, 0x7b, 0x8b, 0xdb, 0xff, 0xee, 0x18, 0x44, 0xce, 0xf2, 0xa6, 0xc5, 0x83, 0xff, 0x18, - 0x51, 0x5a, 0x1a, 0x6b, 0xe1, 0x08, 0x82, 0x71, 0x6d, 0xac, 0x1b, 0xc9, 0x3c, 0xd0, 0x75, 0x04, 0xb2, 0x96, 0x9c, - 0x60, 0xa2, 0x44, 0xee, 0x55, 0xcd, 0xeb, 0x10, 0x6a, 0x25, 0x96, 0xa9, 0xcd, 0x23, 0xea, 0xa8, 0xb1, 0xef, 0x40, - 0xf0, 0x32, 0x3b, 0x44, 0x6d, 0xfe, 0x63, 0x4b, 0x81, 0x5f, 0x4a, 0x79, 0x32, 0x70, 0x78, 0x23, 0x14, 0x15, 0x1f, - 0x05, 0x30, 0x9c, 0x11, 0xbc, 0xa8, 0xd5, 0x57, 0x8e, 0x63, 0xa0, 0x1f, 0x4a, 0x2a, 0x5e, 0xec, 0x3e, 0x6f, 0xbc, - 0x01, 0x77, 0xa1, 0xfc, 0x03, 0xe5, 0x3a, 0x52, 0x2d, 0x7b, 0xf9, 0xc8, 0x4e, 0x6d, 0xc7, 0xd9, 0x50, 0x15, 0x54, - 0x45, 0xef, 0xd0, 0x2f, 0x85, 0x70, 0x60, 0x79, 0xb2, 0xda, 0x1b, 0xee, 0x0c, 0x7c, 0x6c, 0xc4, 0x47, 0x7d, 0x25, - 0x7b, 0x43, 0xa2, 0x8c, 0x85, 0xe4, 0x38, 0x2a, 0x40, 0xf4, 0xe4, 0xd3, 0x75, 0x36, 0x0d, 0x7b, 0x75, 0xb6, 0x14, - 0x48, 0x23, 0x46, 0x3a, 0x97, 0x4a, 0x67, 0xf6, 0xf4, 0x48, 0x19, 0x3f, 0xef, 0xfc, 0x6a, 0xd9, 0xa0, 0xcc, 0x36, - 0xa4, 0xf2, 0xa7, 0xbc, 0x2f, 0x25, 0x65, 0xb2, 0xad, 0xd8, 0xf4, 0xc6, 0xe6, 0x14, 0xc0, 0x64, 0x05, 0x61, 0xee, - 0xbe, 0x41, 0x39, 0x18, 0x63, 0x5d, 0xa9, 0x22, 0xdf, 0xf8, 0x3c, 0x76, 0x7a, 0x7a, 0xc1, 0x33, 0x8a, 0x2c, 0xfa, - 0x53, 0x04, 0x36, 0xcb, 0x6b, 0x85, 0x09, 0xdf, 0xe7, 0xb8, 0x46, 0xbf, 0xd0, 0x14, 0x4d, 0x42, 0xf4, 0xe3, 0x8d, - 0x48, 0x35, 0x2b, 0xe0, 0xcd, 0xfb, 0xa6, 0x1b, 0xc1, 0xb3, 0x32, 0xda, 0x48, 0x24, 0xda, 0xba, 0x29, 0xf0, 0xef, - 0x11, 0x7d, 0x23, 0x66, 0xfa, 0x83, 0x34, 0x5f, 0xfd, 0x20, 0xcc, 0x37, 0xdb, 0x03, 0xaa, 0xda, 0x87, 0xdc, 0xf8, - 0xe4, 0x42, 0x01, 0x16, 0x10, 0x46, 0x2f, 0x95, 0x36, 0xd6, 0x04, 0xa5, 0x84, 0x4b, 0x51, 0x93, 0x51, 0x5e, 0x4f, - 0xf5, 0x09, 0xad, 0xeb, 0x25, 0x19, 0x60, 0x12, 0xba, 0xb1, 0x8d, 0xbe, 0x8d, 0xb9, 0x4d, 0x97, 0xfd, 0x87, 0x0a, - 0xed, 0x81, 0x2b, 0x1b, 0x2c, 0xe0, 0x73, 0xb5, 0xe7, 0xce, 0x45, 0x04, 0x5a, 0x83, 0xf8, 0x8f, 0xe3, 0x7a, 0xb1, - 0x77, 0x4b, 0x25, 0x25, 0x56, 0x59, 0x08, 0x19, 0x2a, 0x17, 0x76, 0x73, 0xc3, 0x3c, 0xeb, 0x71, 0xf0, 0x8c, 0x04, - 0x01, 0xc1, 0xa9, 0x82, 0x49, 0x5c, 0x4d, 0x69, 0x58, 0xd9, 0x73, 0x74, 0xc3, 0x69, 0xf9, 0x35, 0x53, 0x65, 0xbb, - 0x40, 0xa7, 0x6f, 0x5c, 0x31, 0x98, 0x9f, 0xd8, 0x17, 0x8e, 0x1e, 0x5a, 0x46, 0xd7, 0x67, 0x07, 0x46, 0x80, 0x1c, - 0x56, 0x96, 0x81, 0x84, 0x2d, 0x49, 0xab, 0x37, 0x79, 0x78, 0xcf, 0x14, 0x22, 0xc9, 0x02, 0x55, 0x8e, 0x5f, 0x60, - 0x6b, 0x69, 0x49, 0x39, 0x2b, 0xd1, 0x5a, 0x85, 0x32, 0x44, 0x6b, 0xbd, 0x6f, 0x57, 0x9d, 0xde, 0x7b, 0x5f, 0xd0, - 0x79, 0x69, 0x24, 0x87, 0x18, 0x02, 0x43, 0x2c, 0x8d, 0xef, 0x14, 0x36, 0x5a, 0x6f, 0x96, 0xd9, 0x7d, 0x35, 0xb6, - 0x5f, 0xc3, 0x75, 0x3d, 0xf1, 0xa6, 0xfc, 0xb6, 0xce, 0x1e, 0xe6, 0xbc, 0x72, 0xa2, 0x1b, 0xba, 0x86, 0xcd, 0xda, - 0x4e, 0x7f, 0x55, 0xdf, 0x32, 0x19, 0x16, 0x1f, 0x7b, 0x08, 0x21, 0x17, 0xaa, 0x54, 0x88, 0xf4, 0x76, 0x27, 0x90, - 0x2a, 0xf7, 0x94, 0x2b, 0x9d, 0xe3, 0x44, 0xd6, 0xb1, 0x9d, 0x1c, 0x2e, 0x4d, 0x2a, 0x88, 0x63, 0x7b, 0xf7, 0x9d, - 0x58, 0xf0, 0xc9, 0x17, 0xd2, 0x9c, 0xa7, 0xeb, 0x97, 0x7e, 0x78, 0x65, 0xac, 0x94, 0x9c, 0x6e, 0x66, 0x51, 0xd3, - 0xdd, 0x2c, 0xb2, 0xf3, 0xaf, 0x71, 0xeb, 0x92, 0xf0, 0x3a, 0x69, 0xff, 0x6a, 0x84, 0x97, 0x5c, 0xeb, 0x52, 0x44, - 0x53, 0x94, 0xba, 0x7f, 0x9d, 0xa0, 0x20, 0x12, 0xfc, 0xb9, 0x68, 0x18, 0x6b, 0x9f, 0x56, 0xcd, 0x47, 0x63, 0xc5, - 0xd6, 0xde, 0xb7, 0x92, 0x1a, 0x17, 0x05, 0xd7, 0x8c, 0x5c, 0x69, 0xa5, 0xc4, 0xe0, 0x38, 0xd0, 0x94, 0x3f, 0x50, - 0xe5, 0x0f, 0x53, 0xd2, 0x79, 0x8b, 0xd9, 0xea, 0xfb, 0xd4, 0x6e, 0x1d, 0x53, 0x45, 0x23, 0x9d, 0x19, 0xb3, 0x51, - 0x2b, 0x38, 0xda, 0xe3, 0x7a, 0x59, 0x48, 0xe7, 0xb4, 0xcd, 0xe0, 0x93, 0xf4, 0xf1, 0xad, 0x7c, 0xb6, 0xca, 0x5f, - 0xea, 0xfd, 0x5e, 0xda, 0xdb, 0xe4, 0xc5, 0x06, 0xde, 0x0a, 0x13, 0x60, 0x20, 0xa2, 0x52, 0x05, 0xb5, 0x84, 0x24, - 0xec, 0xb4, 0xd3, 0x39, 0x43, 0x55, 0x5a, 0x4c, 0x81, 0x1f, 0x97, 0xf5, 0xf1, 0xf8, 0x5a, 0x34, 0xa6, 0xd6, 0x51, - 0x23, 0x3e, 0x2e, 0xe7, 0x19, 0x20, 0x2f, 0x54, 0x3c, 0x53, 0x11, 0x7d, 0x46, 0xce, 0xf0, 0xa0, 0xcc, 0x82, 0x91, - 0x76, 0x18, 0x8a, 0x2d, 0x37, 0xa6, 0x3a, 0x03, 0xba, 0xf0, 0x67, 0x8d, 0x94, 0x69, 0x84, 0x52, 0xc8, 0xb5, 0x49, - 0xbb, 0xcc, 0x37, 0x08, 0xd3, 0x0b, 0x1a, 0x7f, 0x3d, 0xf9, 0x5e, 0x0a, 0x99, 0x02, 0xee, 0x23, 0x85, 0xd7, 0xf4, - 0x42, 0x66, 0xc0, 0x9b, 0x1a, 0x20, 0x09, 0x40, 0x9a, 0x55, 0x27, 0xbc, 0x0d, 0x0f, 0x49, 0xb3, 0xdf, 0xca, 0x52, - 0xb9, 0x27, 0x57, 0x5a, 0xf2, 0xad, 0x6e, 0x2b, 0xe6, 0x4b, 0xd6, 0x36, 0xad, 0x9d, 0x9d, 0xd0, 0xeb, 0x34, 0x4d, - 0xba, 0x44, 0x38, 0xa8, 0x24, 0xbd, 0xdf, 0x01, 0x06, 0x53, 0x5f, 0xbe, 0x45, 0xcd, 0xfc, 0x5e, 0x82, 0x9d, 0x0c, - 0xd8, 0x50, 0x65, 0xe5, 0x32, 0x0b, 0x00, 0x01, 0xba, 0x6d, 0xa3, 0x9b, 0x26, 0x8b, 0x37, 0x22, 0xf7, 0x80, 0xce, - 0x05, 0x77, 0x64, 0x6f, 0x29, 0xdd, 0x99, 0x8e, 0x95, 0x6c, 0xbc, 0x2b, 0x6b, 0xb2, 0x0b, 0x95, 0xf8, 0x26, 0x06, - 0x66, 0x3b, 0x2b, 0x09, 0x80, 0xeb, 0xc6, 0x2e, 0xf3, 0x42, 0x9d, 0xc9, 0x6c, 0xcd, 0xaa, 0x3c, 0x55, 0xc3, 0x54, - 0x3a, 0x74, 0xd5, 0x44, 0x0d, 0x41, 0x36, 0x20, 0x6c, 0x5e, 0xdb, 0x5c, 0xc7, 0x67, 0x01, 0x20, 0xe8, 0x41, 0x29, - 0x4b, 0xc6, 0x8e, 0x1b, 0x69, 0x77, 0xbd, 0xac, 0x00, 0x61, 0xbc, 0xb3, 0x26, 0x39, 0x39, 0x2d, 0xfd, 0xc9, 0x78, - 0xdb, 0x6a, 0xa6, 0xdd, 0xf1, 0x43, 0x42, 0xdb, 0xe2, 0xd0, 0x82, 0x1f, 0xa9, 0xdd, 0xb9, 0x5a, 0xc4, 0xaa, 0xbd, - 0x2c, 0x60, 0xb0, 0x8d, 0xd6, 0xba, 0x6d, 0xee, 0xe6, 0x98, 0x08, 0x27, 0xcb, 0xc6, 0x74, 0x27, 0x96, 0x17, 0x89, - 0x35, 0x06, 0x6a, 0x6b, 0xde, 0xf8, 0xa5, 0xc0, 0xd4, 0x04, 0xdf, 0xa8, 0x5c, 0x2c, 0x8d, 0xfe, 0xf4, 0x03, 0x11, - 0xa1, 0x59, 0x6c, 0xae, 0xd6, 0x4d, 0x68, 0xbc, 0xc6, 0xf5, 0x06, 0xdc, 0x0d, 0x2c, 0x1a, 0x4e, 0xf4, 0x60, 0xce, - 0xee, 0x48, 0xcd, 0x8a, 0x65, 0xf8, 0xd1, 0xa3, 0xa3, 0x02, 0xbb, 0xb3, 0x39, 0x96, 0x14, 0x48, 0x30, 0xe2, 0xd7, - 0xd7, 0x58, 0x2c, 0x6a, 0x97, 0x46, 0x07, 0x63, 0x2e, 0xf9, 0x0f, 0xaa, 0x9b, 0x69, 0x5f, 0x01, 0x9b, 0x7b, 0x86, - 0x23, 0x49, 0x99, 0x19, 0xbd, 0xbc, 0x36, 0x0d, 0xec, 0x55, 0x1e, 0x75, 0x1c, 0x46, 0x4f, 0x4a, 0xc2, 0xde, 0x6c, - 0x4d, 0xa9, 0x5c, 0x8a, 0x51, 0xe8, 0x79, 0x83, 0x58, 0xf4, 0xb8, 0x07, 0x38, 0xc9, 0x98, 0x22, 0x7d, 0xb5, 0x51, - 0x90, 0xb7, 0xda, 0x5f, 0xba, 0xec, 0xa0, 0x49, 0x87, 0xce, 0x02, 0x9c, 0x8c, 0x92, 0x82, 0x10, 0xa0, 0x0d, 0xa1, - 0xd7, 0x06, 0x2f, 0xa5, 0x08, 0x4d, 0x4d, 0x66, 0xd4, 0x85, 0xf9, 0x9c, 0x71, 0x46, 0xa1, 0xa0, 0xa7, 0x5d, 0x9a, - 0x77, 0xab, 0xdb, 0x5c, 0x38, 0xde, 0x5d, 0x54, 0x2b, 0x02, 0x29, 0x5a, 0xf1, 0xd3, 0x43, 0xe1, 0x22, 0xb7, 0x20, - 0xa2, 0xd6, 0x1c, 0xde, 0x1a, 0x9c, 0x5c, 0x4c, 0x68, 0x95, 0xea, 0xae, 0xf7, 0xf0, 0x85, 0x88, 0xef, 0xda, 0x3c, - 0x21, 0x8e, 0x5a, 0x6f, 0xe8, 0xe6, 0x2c, 0xcd, 0x53, 0x09, 0xf5, 0xcc, 0x16, 0x02, 0x97, 0x8d, 0x8c, 0x2a, 0x7c, - 0x33, 0x3e, 0xc7, 0xc8, 0x92, 0x80, 0x32, 0x38, 0x9d, 0xc5, 0x08, 0x2c, 0x32, 0xe6, 0xe3, 0xd8, 0x1f, 0xcf, 0x6c, - 0x82, 0x7c, 0xd7, 0x98, 0x11, 0x89, 0xb7, 0xbd, 0x37, 0xd4, 0x28, 0x94, 0x8c, 0x44, 0x5c, 0x1e, 0x39, 0xb4, 0x7b, - 0x50, 0x7d, 0x37, 0x20, 0x36, 0x8c, 0x29, 0xd3, 0x09, 0xa1, 0x4f, 0x1e, 0xc4, 0x9a, 0x5c, 0x98, 0xb0, 0xd2, 0x49, - 0x0c, 0xc4, 0xe8, 0x6c, 0x40, 0xf5, 0x8d, 0xd0, 0x22, 0x91, 0x05, 0x25, 0x12, 0xf9, 0x6c, 0x4e, 0x88, 0xc3, 0x56, - 0x64, 0xfc, 0x60, 0xb5, 0x77, 0x11, 0x95, 0x3e, 0xe3, 0xa4, 0xb0, 0x2e, 0x0b, 0xa3, 0x3f, 0x46, 0x09, 0x61, 0xc0, - 0xd9, 0xed, 0x49, 0x51, 0xde, 0x0d, 0x8b, 0x47, 0x17, 0xa8, 0xca, 0xb7, 0x5c, 0x01, 0xec, 0xd1, 0x42, 0x0d, 0x54, - 0x59, 0xb2, 0x9c, 0xeb, 0x47, 0x21, 0xc2, 0x53, 0x66, 0x8e, 0xaa, 0x10, 0x06, 0x84, 0x88, 0x4a, 0xed, 0xc2, 0xae, - 0x95, 0x02, 0x74, 0x30, 0xa6, 0x8d, 0x46, 0x88, 0x0b, 0x78, 0x9e, 0xb7, 0x3f, 0x0e, 0x98, 0xe6, 0x89, 0x3f, 0xa8, - 0x06, 0xfd, 0x77, 0x24, 0x9b, 0xac, 0x9f, 0xdc, 0xf7, 0xc3, 0x27, 0x7d, 0x87, 0xce, 0xde, 0xef, 0xab, 0xbf, 0x7f, - 0xec, 0xd1, 0x40, 0x16, 0xf2, 0x0b, 0xdd, 0x84, 0x56, 0xcf, 0xde, 0x18, 0xee, 0x88, 0x56, 0xcf, 0x4e, 0x2f, 0x0a, - 0xd4, 0x3b, 0xd7, 0x4e, 0x6d, 0x1b, 0x36, 0x32, 0x89, 0xc7, 0x9a, 0x27, 0x63, 0xb0, 0x22, 0x83, 0x6a, 0x05, 0x2b, - 0x9b, 0x2c, 0xd1, 0x5d, 0x9f, 0x99, 0x83, 0x7b, 0xe2, 0x46, 0xbe, 0x93, 0x67, 0x1f, 0x80, 0x9b, 0x10, 0xf9, 0x4b, - 0x0e, 0xab, 0xfa, 0x1d, 0xd5, 0xa6, 0x3b, 0x28, 0x18, 0x4a, 0x2d, 0x31, 0x5b, 0x15, 0x8d, 0x25, 0xd8, 0x1b, 0x04, - 0x5a, 0x53, 0xab, 0x0f, 0xeb, 0x70, 0xc8, 0x1f, 0x5b, 0xfb, 0x07, 0x95, 0x89, 0xba, 0x68, 0x40, 0x9e, 0x86, 0x5f, - 0xba, 0x44, 0xb8, 0x6c, 0x53, 0xff, 0xaf, 0x6e, 0x2f, 0x76, 0x46, 0xc1, 0x24, 0xe4, 0x6d, 0xc8, 0xc3, 0xdd, 0xc1, - 0x00, 0x05, 0x4a, 0xe7, 0x1b, 0x6d, 0x78, 0x12, 0x3d, 0xc9, 0xc3, 0xf6, 0x79, 0x69, 0xaf, 0x46, 0x7d, 0xae, 0x63, - 0x9b, 0xda, 0xb6, 0x49, 0x4d, 0x49, 0x73, 0x70, 0x05, 0x96, 0x18, 0x17, 0x34, 0xad, 0xe4, 0x11, 0x4b, 0x2c, 0xc7, - 0xa4, 0xca, 0xad, 0xe4, 0x29, 0xa7, 0x8c, 0xff, 0x10, 0xb4, 0x97, 0x59, 0x1e, 0x0d, 0x97, 0xe5, 0xd1, 0x65, 0xb0, - 0x36, 0xa1, 0xba, 0xb7, 0xa1, 0xfa, 0x62, 0xd6, 0xb4, 0xd4, 0x6a, 0x93, 0x24, 0x91, 0xc6, 0x7b, 0xba, 0x58, 0xd7, - 0x03, 0xe8, 0xce, 0xd4, 0x2e, 0x65, 0x12, 0xc7, 0x38, 0xd9, 0x86, 0xb9, 0xfa, 0xd8, 0x2a, 0xad, 0xcf, 0x5f, 0xd0, - 0xf8, 0xdc, 0x7d, 0x2b, 0x8f, 0x18, 0xb5, 0x18, 0x78, 0x7f, 0x78, 0x2a, 0xc1, 0xc5, 0xa1, 0xb1, 0xb3, 0x3d, 0x4c, - 0x1c, 0x76, 0xec, 0xec, 0xd7, 0x14, 0x4c, 0xcf, 0x81, 0x36, 0xf4, 0xd5, 0xe0, 0xf8, 0xda, 0x3d, 0x77, 0xf0, 0x62, - 0x40, 0x4b, 0xa4, 0xbc, 0x53, 0xe4, 0x88, 0x01, 0x26, 0x5a, 0xf9, 0x9b, 0x5f, 0xe7, 0xf5, 0x87, 0xf8, 0x7a, 0x3c, - 0x10, 0x3b, 0x51, 0x1e, 0x3d, 0x2b, 0x14, 0xa5, 0x44, 0x45, 0x4f, 0xe1, 0x2f, 0x6e, 0xa1, 0x0c, 0xa7, 0x89, 0x4e, - 0x47, 0x45, 0xb7, 0x77, 0x4f, 0x7c, 0x67, 0xff, 0xa6, 0x3a, 0x97, 0xf3, 0x0a, 0x03, 0x5d, 0x08, 0x6c, 0xa0, 0x8c, - 0x8c, 0x05, 0x4a, 0xf1, 0x63, 0xcc, 0x2e, 0x43, 0x94, 0xdc, 0xea, 0x13, 0x3e, 0x70, 0x11, 0x98, 0x3b, 0xa4, 0x49, - 0xc2, 0xe8, 0x51, 0x7b, 0x6e, 0x5a, 0x9e, 0x84, 0x99, 0x9d, 0x27, 0x99, 0x9d, 0x53, 0xc5, 0x85, 0x09, 0x53, 0x35, - 0x28, 0x16, 0x8f, 0xe5, 0xa4, 0xb6, 0x5a, 0x4d, 0x33, 0x27, 0x9a, 0xe9, 0x91, 0x3b, 0x0c, 0x81, 0x6e, 0xd2, 0x0d, - 0x35, 0xfa, 0x4d, 0x54, 0xf1, 0xd1, 0x7a, 0x11, 0x0c, 0xd1, 0xfa, 0x74, 0xd6, 0x46, 0xb9, 0x63, 0x14, 0x25, 0xdf, - 0x17, 0x80, 0xb8, 0xb7, 0xae, 0x28, 0x5d, 0x7d, 0xf2, 0xc7, 0x3f, 0x4c, 0xb5, 0x9e, 0x07, 0x10, 0x23, 0xbe, 0x66, - 0x93, 0x33, 0xa3, 0xf2, 0x48, 0xfc, 0x43, 0x98, 0xb4, 0x80, 0x3b, 0x42, 0x58, 0xb5, 0x71, 0x30, 0x49, 0x4e, 0xe7, - 0x62, 0xa8, 0xef, 0xa2, 0x91, 0x24, 0x94, 0x49, 0x7d, 0x0a, 0x9e, 0x4d, 0x4e, 0xad, 0x8f, 0x0e, 0x09, 0x77, 0xeb, - 0x20, 0x14, 0x62, 0xa6, 0x5a, 0x03, 0xdc, 0x3d, 0xa5, 0xfb, 0x7a, 0xed, 0x8d, 0xd7, 0x2a, 0xe2, 0xfe, 0xfb, 0xc5, - 0xc1, 0xfb, 0xef, 0x78, 0xa9, 0xa9, 0xdf, 0x6f, 0x9c, 0x0d, 0xdb, 0xb7, 0x3c, 0x00, 0x2f, 0x06, 0x78, 0x08, 0x70, - 0x11, 0xf5, 0x56, 0xa7, 0xfd, 0x61, 0x74, 0xe3, 0xeb, 0xca, 0xec, 0x59, 0xd1, 0xf5, 0x3b, 0x3f, 0x78, 0xb7, 0x6f, - 0x21, 0x60, 0x17, 0xdd, 0xff, 0x1f, 0x81, 0x0a, 0x08, 0x86, 0x82, 0xbf, 0x3f, 0x6e, 0x87, 0xb3, 0x23, 0x78, 0x0e, - 0xbd, 0x3e, 0x8e, 0x62, 0xa5, 0x7b, 0x27, 0x4d, 0xb1, 0x57, 0x11, 0x54, 0x99, 0x57, 0xc4, 0xa6, 0x8c, 0xcd, 0x2e, - 0xeb, 0x52, 0xaf, 0xcd, 0x37, 0x18, 0xd0, 0x97, 0x00, 0xc8, 0x48, 0xf5, 0xa6, 0x0c, 0x20, 0xfc, 0xfa, 0x52, 0x2c, - 0x46, 0xf3, 0x7c, 0xa7, 0xb5, 0x6b, 0xf7, 0x29, 0xf4, 0xc3, 0x76, 0x1d, 0x1e, 0x0c, 0xed, 0x09, 0x79, 0x9e, 0x37, - 0xbc, 0xcb, 0xf0, 0x6d, 0x5e, 0x14, 0x9c, 0x06, 0x2f, 0xa3, 0x5a, 0x1a, 0xf2, 0x49, 0x34, 0x06, 0xfa, 0xb4, 0x6f, - 0x29, 0x01, 0xb7, 0x21, 0x31, 0xd8, 0x41, 0x56, 0x7a, 0x7d, 0x24, 0xed, 0x9d, 0xeb, 0x31, 0xbc, 0xd9, 0x6e, 0x71, - 0x91, 0x32, 0x22, 0xb1, 0x63, 0xa0, 0xc9, 0x8d, 0x50, 0xed, 0xed, 0xce, 0x9e, 0x0f, 0xdf, 0xdc, 0x5c, 0xde, 0xdc, - 0xae, 0x8f, 0x43, 0xaa, 0xb1, 0x4e, 0xa7, 0xd6, 0x6a, 0x6c, 0x27, 0x6d, 0x91, 0xef, 0x2d, 0x0b, 0x9b, 0x84, 0x16, - 0xe9, 0x06, 0x96, 0x96, 0x0f, 0x93, 0xaa, 0x55, 0x06, 0x38, 0x91, 0x9a, 0xba, 0x9f, 0x9e, 0x9e, 0x33, 0x25, 0xcb, - 0x03, 0x7a, 0x71, 0xd0, 0x55, 0x21, 0xb6, 0x4b, 0xd7, 0x6f, 0x2f, 0x97, 0x9e, 0xeb, 0x06, 0x60, 0x13, 0x39, 0x30, - 0x90, 0xf2, 0x7f, 0xc7, 0xa2, 0x5e, 0x0e, 0xcb, 0x93, 0x25, 0x82, 0xc2, 0x25, 0xde, 0x75, 0x49, 0x4a, 0xb4, 0x29, - 0x45, 0x68, 0x2e, 0x5e, 0x1f, 0x15, 0xe3, 0x49, 0xdd, 0x59, 0xf3, 0xec, 0x20, 0x12, 0x19, 0x5b, 0x19, 0x1b, 0xcc, - 0x4d, 0x5a, 0x86, 0x00, 0x07, 0x85, 0x64, 0xcb, 0xf5, 0xa6, 0x0b, 0xb0, 0x5d, 0xf2, 0x57, 0xa3, 0x71, 0x9e, 0x2c, - 0xd1, 0x1d, 0x1a, 0xf6, 0xe5, 0x40, 0xc1, 0xe4, 0xe6, 0xca, 0xe9, 0x91, 0x1f, 0xc5, 0x6c, 0xb1, 0x46, 0x86, 0xc1, - 0x82, 0xe9, 0x04, 0x1c, 0x08, 0xb9, 0x57, 0x0e, 0x10, 0x5b, 0x16, 0xf8, 0x30, 0x98, 0x5b, 0x22, 0x9b, 0x3c, 0xda, - 0xd9, 0x3d, 0x55, 0x28, 0xf8, 0xe4, 0xd6, 0x6d, 0x59, 0xf2, 0xca, 0x0f, 0x82, 0x5e, 0xc5, 0xe5, 0x69, 0xbb, 0x68, - 0x8e, 0xc9, 0xd1, 0x77, 0xd9, 0x94, 0xfd, 0x30, 0x8d, 0xc8, 0xc3, 0x43, 0x92, 0xe1, 0x30, 0x0b, 0x82, 0xc5, 0x4e, - 0x78, 0x29, 0x6c, 0x60, 0xac, 0x6d, 0xc2, 0x8e, 0xd4, 0x10, 0xde, 0x21, 0x26, 0xac, 0x99, 0xb3, 0x16, 0x2c, 0x10, - 0x71, 0x39, 0xe8, 0x3e, 0x72, 0xa0, 0x5f, 0xd9, 0x0a, 0x1d, 0xed, 0xe2, 0x6e, 0xf6, 0x23, 0x16, 0xc8, 0xd8, 0x92, - 0x39, 0xe9, 0x1a, 0x7e, 0xcb, 0x50, 0xad, 0xad, 0x67, 0xa3, 0xb3, 0x7b, 0xc3, 0x34, 0xd1, 0x96, 0x25, 0x3b, 0xa2, - 0x64, 0xfd, 0x42, 0x02, 0xb7, 0x50, 0xe5, 0x46, 0xee, 0xad, 0x44, 0x11, 0xc4, 0x14, 0xba, 0x78, 0xdd, 0x2d, 0x8c, - 0x88, 0x37, 0xd3, 0xa9, 0x39, 0x2a, 0x7c, 0x22, 0x63, 0x50, 0x52, 0x92, 0x82, 0xff, 0x67, 0xbd, 0x5f, 0x80, 0x82, - 0xf8, 0xc4, 0xaf, 0x7f, 0x17, 0x44, 0x38, 0xb0, 0xdb, 0x5e, 0xb6, 0xaa, 0x1d, 0x4b, 0x50, 0x1e, 0x15, 0xe6, 0xdc, - 0x40, 0x6a, 0xfd, 0x7b, 0x6e, 0xe3, 0xcd, 0x9f, 0xbf, 0xcb, 0x5c, 0xad, 0xdb, 0xe5, 0xe6, 0xb5, 0x3b, 0xe4, 0x9a, - 0xb1, 0x03, 0xf6, 0xe5, 0xe0, 0xc3, 0x6a, 0x26, 0xdd, 0x02, 0x92, 0x86, 0x4c, 0x2f, 0xdc, 0xae, 0xe8, 0x86, 0x13, - 0x72, 0x07, 0xe4, 0x10, 0x20, 0xd0, 0x66, 0x50, 0xd6, 0xe8, 0x58, 0xef, 0xc3, 0x79, 0x7b, 0x7d, 0xf9, 0xf7, 0xba, - 0x5e, 0xa2, 0x43, 0x9a, 0x9d, 0xc5, 0xa0, 0xff, 0x7e, 0x2b, 0x19, 0xc9, 0xf6, 0xcd, 0xf6, 0xfe, 0x5d, 0x0b, 0x8a, - 0x6b, 0x9a, 0xf6, 0x0f, 0x7e, 0xf9, 0xa2, 0xb7, 0xf0, 0x7a, 0xe7, 0x23, 0xa9, 0x49, 0x53, 0x6e, 0xf8, 0x71, 0xb5, - 0x95, 0xef, 0x4a, 0x66, 0x7c, 0x40, 0x60, 0xc4, 0xc9, 0xea, 0xe2, 0xe9, 0x61, 0xc4, 0x64, 0x3d, 0x6a, 0x18, 0x4e, - 0x6e, 0x6d, 0xc6, 0xb4, 0x6a, 0x21, 0x32, 0xc0, 0x25, 0x1a, 0x95, 0x28, 0x12, 0x25, 0x31, 0x40, 0x70, 0x6f, 0x7d, - 0x9e, 0xa0, 0x2d, 0x6a, 0xd6, 0x0e, 0xd4, 0x76, 0x56, 0x36, 0x27, 0x01, 0xa3, 0xcd, 0x1c, 0xd3, 0x6a, 0x2e, 0x42, - 0xe7, 0xee, 0x34, 0x88, 0x0e, 0xbd, 0x25, 0xba, 0x94, 0xb9, 0x62, 0xdf, 0xb4, 0xac, 0x2d, 0x03, 0xf2, 0x49, 0xd4, - 0x46, 0x1d, 0x24, 0x58, 0xe5, 0x54, 0x6c, 0x26, 0xf6, 0x8d, 0xa1, 0x2d, 0xdc, 0x81, 0xbe, 0x81, 0x1e, 0xac, 0xf1, - 0x92, 0xdd, 0xe4, 0xed, 0x53, 0xca, 0x0b, 0x8b, 0x49, 0xf7, 0x3b, 0xa9, 0x1e, 0xdb, 0x5b, 0x03, 0xa2, 0x50, 0x8c, - 0x77, 0x0f, 0x09, 0x56, 0x1e, 0xbd, 0x0d, 0x38, 0xb6, 0x4a, 0xaf, 0x71, 0x55, 0x3d, 0x31, 0x26, 0x78, 0x58, 0xca, - 0x27, 0xdf, 0x3f, 0x79, 0x35, 0xee, 0x1a, 0xc6, 0x4b, 0x8b, 0x5b, 0x10, 0x54, 0x30, 0x7b, 0x8b, 0x59, 0xfc, 0xd2, - 0xfc, 0xbe, 0x7b, 0xe0, 0xc6, 0xce, 0x21, 0x37, 0x6f, 0x70, 0xf7, 0x5a, 0xdc, 0xa7, 0xce, 0x67, 0xf5, 0xec, 0xd3, - 0xe9, 0x6a, 0x6b, 0x14, 0x7d, 0x3b, 0x03, 0xed, 0x11, 0xe9, 0xac, 0x01, 0x98, 0x04, 0x28, 0x4b, 0x32, 0xa0, 0x86, - 0x05, 0x5e, 0x2e, 0xad, 0xba, 0x13, 0xd4, 0x54, 0x7b, 0xb6, 0x29, 0x9f, 0x0b, 0x6b, 0x2c, 0xbe, 0x58, 0xba, 0x4e, - 0x53, 0xc3, 0x14, 0xb5, 0xae, 0x5d, 0xf3, 0xf7, 0x6f, 0x65, 0x09, 0x34, 0x4c, 0xe5, 0x8a, 0xfd, 0x1a, 0x55, 0x43, - 0xf0, 0x29, 0x2c, 0xa2, 0x84, 0x00, 0xcf, 0x62, 0x12, 0xa8, 0x5a, 0x3f, 0xb4, 0xbd, 0xdf, 0xbb, 0x63, 0xeb, 0x64, - 0x3a, 0xb8, 0x6b, 0x40, 0x96, 0x99, 0xf3, 0xce, 0x99, 0x96, 0xa1, 0x9b, 0xc6, 0x45, 0x48, 0xd9, 0x4f, 0x5f, 0xa0, - 0x4e, 0x96, 0xdb, 0xec, 0x51, 0xd0, 0x58, 0x0e, 0x91, 0x14, 0xb9, 0x20, 0xc5, 0xbf, 0x0b, 0x47, 0x3c, 0x46, 0x6a, - 0x9d, 0xa9, 0x65, 0x8c, 0xa6, 0xff, 0x16, 0xd6, 0x82, 0xa5, 0xdd, 0x7b, 0x96, 0xc1, 0x8f, 0x93, 0x01, 0xd5, 0x3a, - 0x77, 0x52, 0x26, 0x9b, 0x25, 0x8c, 0x0c, 0xed, 0x8e, 0x5a, 0xfd, 0xf4, 0x6b, 0xbd, 0x5d, 0x9a, 0xbd, 0x34, 0xcd, - 0x2f, 0xa2, 0x85, 0x81, 0x2c, 0x01, 0x17, 0x0b, 0x4a, 0x3b, 0x27, 0xd5, 0xbf, 0xf7, 0xcd, 0xf7, 0xc4, 0xf7, 0xc2, - 0x5f, 0x66, 0x3e, 0x8f, 0x7c, 0xca, 0x2b, 0x3f, 0x40, 0x9e, 0x4f, 0xee, 0xad, 0x16, 0x0c, 0x23, 0x98, 0x88, 0xac, - 0x5c, 0x81, 0x80, 0x45, 0x91, 0x3c, 0x50, 0x01, 0x89, 0x88, 0x2b, 0xdb, 0x21, 0xad, 0x66, 0xbd, 0x9b, 0x01, 0x85, - 0x01, 0xd7, 0xfe, 0x42, 0xe3, 0x9c, 0x2e, 0xf6, 0xd6, 0x51, 0x51, 0xe9, 0x58, 0x1a, 0xfd, 0x11, 0x98, 0x18, 0x51, - 0xc9, 0xe9, 0xa8, 0x38, 0xb3, 0x18, 0xed, 0x2b, 0x3a, 0x8b, 0x19, 0xc8, 0x58, 0xa9, 0x29, 0x5b, 0xf9, 0x0d, 0x30, - 0xbb, 0x3d, 0x97, 0x34, 0xf5, 0x18, 0x0e, 0xe4, 0x05, 0x44, 0x0d, 0xac, 0x68, 0x03, 0x9d, 0xda, 0x6f, 0x08, 0xcf, - 0x1b, 0x96, 0x47, 0x80, 0x20, 0x28, 0xdf, 0x41, 0xd8, 0x9f, 0xd8, 0xbe, 0x72, 0x35, 0xc3, 0x29, 0xc3, 0xf4, 0x19, - 0x87, 0x86, 0xfa, 0x14, 0xfc, 0x04, 0x6c, 0xa2, 0xab, 0x11, 0x20, 0xdf, 0x24, 0x84, 0x1e, 0x04, 0xfd, 0x2b, 0x8f, - 0x48, 0x7f, 0xdd, 0xd4, 0xea, 0x2b, 0x98, 0xe2, 0xa8, 0x4c, 0xd6, 0x6d, 0x6a, 0x5b, 0xbd, 0xb2, 0x65, 0x5c, 0xd7, - 0x80, 0x3a, 0x2d, 0x9d, 0xe3, 0x0c, 0x27, 0x0d, 0xf1, 0xbf, 0x06, 0x86, 0x3f, 0xa8, 0xdd, 0x0e, 0xa3, 0x0f, 0xfd, - 0xc6, 0x8c, 0x79, 0x87, 0x70, 0x78, 0x3c, 0x31, 0x8d, 0xdc, 0x9f, 0x0b, 0x4c, 0x87, 0x96, 0xf8, 0x23, 0x8d, 0x38, - 0xe9, 0x83, 0xd2, 0x8b, 0xd5, 0xa1, 0x32, 0xfe, 0xdb, 0xb8, 0x1f, 0xbe, 0x6d, 0xb3, 0x8a, 0xe1, 0xc9, 0x88, 0x02, - 0xb6, 0x1a, 0xb3, 0x8e, 0x4f, 0x8e, 0xd6, 0xe3, 0x98, 0xdb, 0x80, 0xa8, 0x71, 0xbd, 0xa9, 0xda, 0x2c, 0x52, 0xb1, - 0xe5, 0x96, 0x3d, 0x1f, 0xcc, 0xa8, 0x7c, 0xfc, 0xf3, 0x32, 0x15, 0x82, 0x00, 0x55, 0xe2, 0x43, 0x34, 0xd0, 0xc5, - 0x6e, 0x27, 0x68, 0xe1, 0xb7, 0x96, 0xd2, 0x4a, 0xe6, 0xc1, 0x6a, 0xee, 0x90, 0x80, 0x8e, 0xaa, 0x01, 0xc3, 0xa7, - 0x68, 0xb2, 0xab, 0xc9, 0x31, 0x42, 0x01, 0x4d, 0xce, 0x92, 0x86, 0x93, 0x61, 0xbf, 0x2d, 0x4e, 0x7f, 0x9d, 0xf3, - 0x51, 0xb3, 0x21, 0x52, 0xdf, 0x8e, 0x89, 0x98, 0x7e, 0xc7, 0x57, 0x59, 0x19, 0x1b, 0xa1, 0x78, 0x33, 0x88, 0x8d, - 0x21, 0xc9, 0x1b, 0x05, 0x25, 0x42, 0x24, 0xbb, 0x38, 0x11, 0x66, 0xf3, 0x7e, 0xa5, 0xf0, 0xf4, 0x15, 0xa1, 0xd4, - 0x1c, 0x23, 0x8d, 0x0e, 0xb6, 0x74, 0xc2, 0xda, 0xb4, 0x7d, 0x5c, 0x7d, 0x81, 0x41, 0x87, 0xcf, 0x1c, 0xf0, 0x02, - 0xe0, 0xc6, 0xb0, 0x0a, 0x60, 0xad, 0x31, 0x77, 0x0c, 0xb7, 0x65, 0x7c, 0x62, 0x2d, 0x73, 0x40, 0xff, 0x98, 0xc8, - 0x72, 0x43, 0x7b, 0x0e, 0x41, 0xc1, 0xb4, 0x1d, 0x58, 0xa2, 0xf2, 0xef, 0xb4, 0x29, 0x76, 0x55, 0x31, 0x31, 0x0f, - 0x84, 0xcb, 0x12, 0x09, 0x95, 0xaf, 0x7b, 0xd7, 0x63, 0x06, 0xf8, 0x88, 0xa8, 0x19, 0x54, 0xbc, 0xce, 0x4d, 0x7e, - 0x55, 0x3f, 0xbf, 0x04, 0xec, 0x75, 0xf6, 0xba, 0xfe, 0xf0, 0xba, 0x7a, 0xfa, 0x93, 0x52, 0x00, 0xf4, 0x5c, 0xd8, - 0x95, 0x61, 0x26, 0x0b, 0x9b, 0xc8, 0xf0, 0x73, 0xbd, 0x84, 0xf2, 0xb4, 0x99, 0x03, 0x42, 0x38, 0xc7, 0xf9, 0xe4, - 0xfa, 0x74, 0x95, 0xb9, 0x09, 0xa4, 0x08, 0xb8, 0x09, 0x20, 0xf3, 0xfe, 0x08, 0x67, 0xce, 0x07, 0x04, 0xe2, 0x5d, - 0x5c, 0x9b, 0x1c, 0x3d, 0x0e, 0x92, 0x98, 0xdd, 0x4f, 0x3d, 0x2a, 0x88, 0xcb, 0x68, 0x01, 0x0d, 0x5b, 0x53, 0x76, - 0x2d, 0x58, 0xee, 0x08, 0x1d, 0x36, 0x84, 0x99, 0x42, 0x57, 0x89, 0xfc, 0x87, 0x47, 0x4b, 0xaa, 0xe8, 0xb1, 0x3b, - 0x7a, 0xb6, 0x22, 0xca, 0x70, 0x52, 0x47, 0x42, 0x82, 0xf0, 0x85, 0xa8, 0x81, 0x7e, 0xc0, 0xc6, 0xa8, 0x52, 0xe2, - 0x12, 0xdb, 0x12, 0xe8, 0x3b, 0x09, 0xc2, 0xb2, 0x53, 0x1a, 0x86, 0xe6, 0x90, 0xc3, 0x48, 0x14, 0x41, 0x29, 0xfc, - 0x02, 0x25, 0xcf, 0x34, 0x94, 0x80, 0x32, 0x75, 0x60, 0x47, 0x0d, 0x55, 0x89, 0x09, 0x75, 0x7a, 0x7a, 0x10, 0xdd, - 0xbb, 0x0c, 0x34, 0x4d, 0x07, 0xa7, 0x1d, 0x2a, 0xc6, 0xd2, 0x98, 0xea, 0x60, 0x3b, 0x2a, 0x04, 0x47, 0x3a, 0x1e, - 0x32, 0x0a, 0x4e, 0x6e, 0xdf, 0xe1, 0xb2, 0xe1, 0xd3, 0xed, 0xa7, 0x4a, 0x8c, 0x8e, 0x9e, 0xac, 0xce, 0xa5, 0xd5, - 0xf3, 0x6c, 0xcc, 0x24, 0x48, 0x9f, 0xc0, 0xa1, 0x52, 0xf8, 0x32, 0x03, 0xd3, 0x22, 0x8f, 0xb7, 0x65, 0xb4, 0x38, - 0x85, 0x92, 0xab, 0x6e, 0x1f, 0xe9, 0x36, 0xdf, 0xce, 0xa4, 0xdb, 0x6f, 0xa7, 0xc1, 0x51, 0xd6, 0xcc, 0xfa, 0x42, - 0xf9, 0xbc, 0x52, 0xaa, 0xed, 0x5b, 0xf9, 0x49, 0xa2, 0x83, 0x63, 0x0d, 0xd5, 0x2a, 0x2c, 0xf1, 0x93, 0x81, 0xd5, - 0x6b, 0x48, 0xb5, 0x91, 0x8a, 0x61, 0x07, 0x9e, 0x8f, 0x3c, 0x9e, 0xbb, 0xae, 0x34, 0xe3, 0xca, 0x30, 0xb3, 0x49, - 0x25, 0xc6, 0xf7, 0xc3, 0x63, 0x0f, 0xed, 0x99, 0xf6, 0xf9, 0x74, 0xf8, 0x12, 0xe8, 0x74, 0x20, 0x9a, 0x80, 0x81, - 0x39, 0x84, 0x32, 0x16, 0x68, 0x6c, 0x2c, 0x66, 0x51, 0x1e, 0x95, 0x29, 0x4d, 0x95, 0xc6, 0x30, 0x86, 0xda, 0x00, - 0xae, 0x6e, 0xd7, 0x4c, 0x4a, 0x46, 0x49, 0x77, 0x29, 0x0d, 0x14, 0xd3, 0x31, 0x8c, 0x15, 0x9e, 0x29, 0x19, 0x2e, - 0x0a, 0x71, 0x1a, 0xe0, 0xcb, 0x8b, 0xff, 0xf7, 0xaf, 0xc0, 0xa8, 0xb9, 0xed, 0x91, 0xac, 0xd9, 0xec, 0x68, 0x4b, - 0x2b, 0x3c, 0x4f, 0xe7, 0xcb, 0x17, 0x29, 0xeb, 0x52, 0x2d, 0x8a, 0xd3, 0xe8, 0x28, 0x23, 0x4a, 0xfb, 0x76, 0xf7, - 0x97, 0xba, 0x33, 0x8c, 0x98, 0x2b, 0xdf, 0xf8, 0x3d, 0xe5, 0x5a, 0xf2, 0x6e, 0xb7, 0x8c, 0xac, 0x4a, 0x31, 0xe1, - 0x43, 0xe5, 0x1a, 0x5e, 0x69, 0xfd, 0x07, 0xf9, 0x4f, 0xb9, 0xaa, 0x6d, 0x7f, 0x0c, 0xeb, 0x95, 0x6c, 0x4e, 0xb4, - 0xde, 0x3c, 0xe3, 0x88, 0xb7, 0x3d, 0xc6, 0xfd, 0x25, 0x85, 0x63, 0x69, 0xfc, 0xae, 0xea, 0x64, 0x37, 0x3f, 0xb9, - 0x5c, 0x90, 0xb4, 0x98, 0x74, 0xeb, 0xad, 0xca, 0x7e, 0xe6, 0xab, 0xf7, 0xfb, 0xb3, 0x87, 0x3b, 0x26, 0x41, 0xc2, - 0x6d, 0x43, 0x3e, 0x0d, 0x22, 0xbd, 0x6d, 0x46, 0x47, 0x69, 0xf2, 0xca, 0x99, 0x4d, 0x08, 0x84, 0xe3, 0x8d, 0xe9, - 0x01, 0x26, 0x3b, 0x93, 0xd2, 0xcb, 0xfe, 0x67, 0x76, 0xe5, 0xda, 0xd4, 0xc5, 0x5d, 0xb1, 0xc5, 0x83, 0xe4, 0xd7, - 0x43, 0x7c, 0x38, 0x86, 0x37, 0x9f, 0xe3, 0x77, 0xc8, 0x3f, 0xea, 0xb8, 0x0c, 0x0c, 0x4c, 0xac, 0x1c, 0xfb, 0x4e, - 0x78, 0xd9, 0xdf, 0x12, 0x6b, 0x50, 0x56, 0x69, 0x8a, 0x21, 0x18, 0xc4, 0x79, 0x1d, 0x00, 0xc8, 0x95, 0x0d, 0x62, - 0x9b, 0x27, 0xb2, 0xe5, 0xab, 0x60, 0xf1, 0xce, 0xf1, 0xd1, 0x0b, 0x6e, 0x4a, 0x7c, 0xaa, 0xbc, 0x3d, 0x63, 0x0c, - 0x70, 0x0b, 0xca, 0xd3, 0xb1, 0x83, 0x19, 0x31, 0x47, 0x42, 0xed, 0x8a, 0x4a, 0x2c, 0x49, 0x1d, 0x2a, 0x14, 0xcd, - 0xea, 0x82, 0x91, 0x89, 0xe4, 0xb3, 0x35, 0x55, 0x82, 0x81, 0xd4, 0x41, 0x7b, 0xf6, 0x2c, 0x4a, 0x9a, 0x7d, 0x1e, - 0x9a, 0x6c, 0x92, 0x3b, 0x7e, 0x09, 0xa6, 0x3f, 0xf8, 0x59, 0x28, 0xe9, 0x73, 0x6f, 0x62, 0x21, 0x7f, 0xb7, 0x95, - 0xf5, 0x27, 0xec, 0x1d, 0xfe, 0x26, 0x21, 0x7c, 0x39, 0x85, 0xd5, 0x24, 0x61, 0x59, 0xb8, 0xf0, 0x76, 0x49, 0x80, - 0x3c, 0x65, 0x69, 0x57, 0x83, 0x03, 0x85, 0x3e, 0x14, 0x94, 0x2c, 0x96, 0xb1, 0x12, 0x33, 0xc3, 0x22, 0xa6, 0xe4, - 0x5e, 0xf4, 0x35, 0xf3, 0xbe, 0xf9, 0x3a, 0x85, 0x47, 0x06, 0x4f, 0xe5, 0xa6, 0x6d, 0x5b, 0x88, 0x0e, 0x18, 0x9a, - 0xe9, 0x4f, 0x70, 0x40, 0xbb, 0x7f, 0xdd, 0xa5, 0xa7, 0x1c, 0xf8, 0xec, 0x39, 0x0e, 0xd6, 0x56, 0x9e, 0xa5, 0x9c, - 0x35, 0x54, 0xf7, 0x39, 0x05, 0x3f, 0x17, 0xef, 0x10, 0x57, 0x26, 0xc1, 0xd3, 0x5d, 0x4c, 0x12, 0x54, 0x9f, 0x82, - 0x21, 0xe9, 0x04, 0x74, 0xb1, 0xc2, 0xea, 0x5a, 0xb3, 0xe5, 0x09, 0xba, 0x98, 0x60, 0x05, 0x63, 0x38, 0x14, 0xf4, - 0xf2, 0x30, 0xb3, 0x1e, 0x56, 0xd3, 0xd3, 0x22, 0x48, 0x22, 0x9d, 0xec, 0xf6, 0x53, 0x92, 0xbd, 0x26, 0x12, 0x40, - 0x3f, 0x37, 0x2b, 0x69, 0x03, 0xe0, 0x41, 0xad, 0x10, 0xb1, 0xef, 0x45, 0xcc, 0x49, 0x2a, 0x55, 0x73, 0x46, 0xb7, - 0x15, 0x02, 0x62, 0x5d, 0xf8, 0x5b, 0x5e, 0xdd, 0x94, 0xfa, 0x53, 0xb0, 0x80, 0xbe, 0xe1, 0x42, 0x02, 0xaf, 0x8d, - 0x8d, 0xf7, 0x8a, 0xc6, 0x1a, 0x5f, 0x02, 0x58, 0x1c, 0x0c, 0xf0, 0xa4, 0xc6, 0x32, 0x2c, 0x01, 0x69, 0x15, 0x0f, - 0x9d, 0x98, 0xb0, 0xf2, 0xb4, 0xe0, 0x98, 0xe5, 0xbb, 0x7f, 0x98, 0xdf, 0xe9, 0xb4, 0x4e, 0x20, 0x31, 0xd3, 0xa9, - 0x76, 0x4b, 0x2f, 0x1f, 0x58, 0xbf, 0xd6, 0x98, 0x25, 0xe2, 0x9e, 0xe4, 0x65, 0xb7, 0x63, 0x15, 0xda, 0x58, 0xc4, - 0x32, 0x9e, 0x29, 0x87, 0x57, 0x53, 0x6f, 0xf3, 0xf0, 0x00, 0x0e, 0xcf, 0xa7, 0x96, 0xfb, 0xeb, 0x00, 0x13, 0x87, - 0x9b, 0x52, 0x28, 0x15, 0xf1, 0x7a, 0x10, 0x20, 0x12, 0xc4, 0x44, 0xbb, 0xc8, 0x50, 0x7a, 0xca, 0x0d, 0x62, 0xb3, - 0x01, 0x25, 0x62, 0x87, 0xb6, 0x8e, 0xd2, 0x1f, 0xc2, 0x57, 0x47, 0xf9, 0x54, 0x99, 0xea, 0xa4, 0xb7, 0x30, 0xcb, - 0xe5, 0x48, 0x35, 0x34, 0x60, 0xd9, 0x71, 0xfb, 0xc9, 0x63, 0x5b, 0x61, 0x78, 0x6e, 0xab, 0xfe, 0x6e, 0x1b, 0xfe, - 0xfe, 0x02, 0x5e, 0x3c, 0xfd, 0xbe, 0xee, 0x6b, 0x6e, 0xd9, 0x90, 0x43, 0x5d, 0xda, 0x8d, 0x88, 0xb8, 0x17, 0x2f, - 0xaf, 0x52, 0x48, 0x01, 0xd2, 0xfc, 0x01, 0x3c, 0x3b, 0xbe, 0x3d, 0xd2, 0x7d, 0x2a, 0x32, 0x41, 0x24, 0xe4, 0xed, - 0x82, 0xb0, 0xe2, 0xb1, 0xa7, 0xb0, 0x69, 0x64, 0x41, 0x9f, 0x4a, 0xe8, 0x12, 0x7e, 0x8a, 0x7c, 0x79, 0x39, 0x17, - 0xfc, 0x18, 0xd2, 0x09, 0x68, 0xb0, 0x3b, 0xeb, 0x45, 0x50, 0x06, 0x39, 0xed, 0x2d, 0xa5, 0x79, 0x27, 0x97, 0x8d, - 0x02, 0xd3, 0x96, 0x85, 0xf6, 0x4b, 0xa3, 0x6e, 0xba, 0x78, 0x6a, 0xa2, 0x10, 0xf0, 0xf0, 0xb0, 0xd9, 0xed, 0xa4, - 0xa1, 0x9c, 0x55, 0x73, 0xef, 0xab, 0x55, 0xe3, 0x8a, 0xe4, 0xe3, 0x61, 0x86, 0x20, 0xa4, 0xdd, 0x8e, 0x9c, 0x1a, - 0xc3, 0x51, 0xd1, 0xbe, 0x48, 0xd6, 0x79, 0xe2, 0x70, 0xdc, 0xcb, 0x27, 0x71, 0xb2, 0x71, 0xac, 0x8b, 0x93, 0x48, - 0x05, 0xbe, 0x58, 0x7d, 0xd5, 0x10, 0x6d, 0xa6, 0xc5, 0xe9, 0x5d, 0x55, 0xa5, 0x6a, 0x0a, 0xb4, 0x93, 0x22, 0x47, - 0x76, 0x33, 0xbb, 0x2b, 0xb6, 0xa1, 0xd0, 0x0c, 0x38, 0x7f, 0xd6, 0x5e, 0xac, 0x47, 0x78, 0xa8, 0xbc, 0xf8, 0x47, - 0xd1, 0x3f, 0x56, 0x3d, 0x91, 0x65, 0x2b, 0xfc, 0xd5, 0x78, 0xbd, 0xb4, 0xf8, 0x37, 0x0f, 0xdc, 0x67, 0xd7, 0xd9, - 0x91, 0xb7, 0xde, 0x9c, 0x8f, 0x57, 0x15, 0x4f, 0x17, 0x89, 0x6f, 0x18, 0x06, 0x70, 0x39, 0xa4, 0x79, 0xb9, 0xdb, - 0x7b, 0x0c, 0x9f, 0x86, 0x80, 0x90, 0x6c, 0xe7, 0xdc, 0x3e, 0x9f, 0x3f, 0x1c, 0x69, 0x33, 0x9c, 0xc9, 0x4b, 0x21, - 0xd9, 0x57, 0x08, 0x00, 0x64, 0xd5, 0x66, 0xa4, 0x63, 0x5d, 0x4d, 0x02, 0x69, 0x32, 0x49, 0xdd, 0x6e, 0x03, 0x5c, - 0x80, 0x54, 0x94, 0x2f, 0xd7, 0x83, 0x15, 0x35, 0xf5, 0xc2, 0x14, 0x5f, 0xee, 0xe5, 0x0b, 0x34, 0xad, 0x69, 0xda, - 0xcb, 0xb9, 0x0c, 0x05, 0xd6, 0xcb, 0x0e, 0x11, 0x1e, 0x64, 0x2b, 0xc6, 0xe3, 0x71, 0xe4, 0xbb, 0xc9, 0x07, 0x94, - 0x1b, 0x2c, 0x2e, 0xf7, 0xea, 0xcb, 0xa9, 0xdd, 0x14, 0xb6, 0x42, 0x9f, 0x61, 0x15, 0x05, 0x73, 0xc0, 0x9b, 0x6b, - 0x7a, 0x3b, 0x9b, 0x0b, 0xb2, 0xd9, 0xc5, 0x67, 0x0b, 0xdb, 0x20, 0x81, 0x78, 0x1c, 0x06, 0x6b, 0x72, 0x88, 0x94, - 0x78, 0x74, 0x4a, 0x53, 0x42, 0x01, 0xc8, 0x00, 0x5e, 0x4c, 0xe2, 0x2d, 0x24, 0xfd, 0xf7, 0xe0, 0x13, 0xec, 0xad, - 0x71, 0xc5, 0xc8, 0x79, 0xfe, 0xe1, 0x74, 0xc0, 0xe9, 0xcf, 0xed, 0x9d, 0xcf, 0x3d, 0x23, 0xa0, 0x46, 0xa9, 0x0f, - 0xe5, 0xc1, 0x7f, 0xd2, 0x15, 0x9d, 0xd6, 0x62, 0xbe, 0x13, 0xb1, 0x4a, 0x85, 0x2d, 0xf7, 0x32, 0xd8, 0xdf, 0xef, - 0x87, 0xe9, 0xff, 0xab, 0x6b, 0x43, 0x55, 0x7e, 0xfe, 0xb7, 0x35, 0xfc, 0x27, 0xbd, 0x0e, 0x4b, 0xcd, 0xfd, 0x6f, - 0x0d, 0x36, 0xfd, 0xf6, 0x1a, 0xea, 0xa1, 0x6d, 0xff, 0xd6, 0x03, 0x88, 0x3a, 0x28, 0x72, 0xb3, 0x27, 0xb2, 0xd2, - 0xaa, 0x73, 0x0f, 0x06, 0xda, 0xc2, 0xff, 0x9f, 0xe5, 0x3d, 0xcb, 0x9e, 0xad, 0x30, 0xb5, 0xf0, 0xf1, 0xfd, 0x8c, - 0x49, 0x00, 0xcb, 0x49, 0x84, 0x36, 0x0e, 0x39, 0xad, 0xfc, 0xb4, 0x46, 0xae, 0x43, 0x5a, 0xb1, 0x56, 0x01, 0xfd, - 0xb2, 0xa4, 0x4f, 0x10, 0xcf, 0x3d, 0x8c, 0xbd, 0x86, 0x92, 0xe0, 0x81, 0x7a, 0xbe, 0x75, 0x94, 0x1f, 0x49, 0xd3, - 0x62, 0x57, 0x4a, 0x7e, 0xe9, 0x9f, 0x3f, 0x66, 0xd9, 0x57, 0x96, 0x1f, 0x88, 0xa1, 0x26, 0xb7, 0xff, 0xc1, 0x42, - 0xda, 0x17, 0x24, 0x31, 0x16, 0xa6, 0x6e, 0x5d, 0x38, 0x9e, 0x38, 0xbd, 0x63, 0x5b, 0xb5, 0x19, 0x84, 0x17, 0x55, - 0x2d, 0x14, 0x67, 0xd7, 0x82, 0x32, 0xa6, 0xf7, 0xe9, 0x4c, 0x13, 0x0c, 0xa8, 0xa5, 0xe4, 0x9d, 0xdf, 0xf0, 0xef, - 0x6c, 0x85, 0x79, 0x57, 0x63, 0xee, 0xde, 0xc0, 0x3e, 0x1a, 0x39, 0x8c, 0xe3, 0x3e, 0x42, 0xa1, 0x6e, 0x70, 0x83, - 0x2f, 0x35, 0x12, 0xdd, 0xb3, 0x65, 0x1a, 0x46, 0x54, 0xf6, 0xbc, 0x05, 0x47, 0xe2, 0x9c, 0x71, 0x09, 0x32, 0xf4, - 0x08, 0x0d, 0xcb, 0x69, 0x78, 0x8b, 0x29, 0x6c, 0x2f, 0xef, 0x18, 0x77, 0x96, 0xad, 0xed, 0x55, 0x9a, 0x21, 0x90, - 0xce, 0x8b, 0xe0, 0xad, 0xe2, 0x49, 0xb8, 0x31, 0x6d, 0xcf, 0xd4, 0x83, 0x5d, 0x7b, 0x49, 0x2f, 0x6a, 0xf3, 0x37, - 0xb2, 0xdb, 0x7b, 0xe9, 0x98, 0x29, 0xcd, 0xeb, 0x9a, 0x2d, 0x5e, 0xbc, 0x20, 0x13, 0x7e, 0x1c, 0x5c, 0x1b, 0xb3, - 0x6e, 0xb7, 0x12, 0x80, 0xcc, 0x89, 0xc6, 0xd5, 0x5c, 0xec, 0x7f, 0xda, 0x1f, 0xa4, 0xf5, 0x60, 0xde, 0x3d, 0xb8, - 0x92, 0x11, 0x9b, 0xbf, 0x33, 0x37, 0x92, 0x7d, 0x93, 0x49, 0x0e, 0xb5, 0xa8, 0xaa, 0xe2, 0xc1, 0xbb, 0x17, 0xc9, - 0xdd, 0xd5, 0xa5, 0x25, 0xa3, 0xde, 0x20, 0x9f, 0xef, 0xd0, 0xcd, 0x3e, 0xac, 0xdb, 0x5a, 0xe3, 0xd4, 0xe2, 0x24, - 0x36, 0x4d, 0xac, 0xc2, 0xac, 0xa6, 0x13, 0xc1, 0xf6, 0xbf, 0xd6, 0xe0, 0x9a, 0x89, 0x3a, 0x14, 0xd6, 0x56, 0x28, - 0x94, 0x82, 0x1f, 0x25, 0x20, 0x61, 0xc6, 0x98, 0x13, 0x70, 0x82, 0x64, 0x4c, 0x27, 0x53, 0xa2, 0x69, 0x28, 0x37, - 0x3f, 0x88, 0x19, 0xbe, 0xcd, 0x28, 0x46, 0x40, 0x72, 0x3f, 0x32, 0x72, 0xc3, 0xc9, 0x92, 0x50, 0x23, 0xee, 0xf6, - 0xc9, 0x2f, 0x70, 0xcc, 0x78, 0x8e, 0xa5, 0xd4, 0xf8, 0x69, 0x7d, 0x7e, 0xcc, 0x7a, 0x3f, 0x5d, 0xff, 0xb0, 0xba, - 0xe7, 0xce, 0x3f, 0x28, 0xe9, 0xd4, 0x5c, 0x43, 0x66, 0x55, 0x00, 0xc8, 0x9b, 0xf2, 0xce, 0xb8, 0x8e, 0xd3, 0x7b, - 0xab, 0x44, 0x04, 0x2e, 0x55, 0xb4, 0xaa, 0x31, 0x82, 0xf9, 0x5e, 0x88, 0x18, 0x27, 0x2b, 0x07, 0xbe, 0xf7, 0x2b, - 0x54, 0x24, 0xe7, 0xe1, 0x73, 0xf6, 0x46, 0x9a, 0x3e, 0x16, 0x4d, 0x26, 0xcf, 0x1d, 0xf1, 0x55, 0x7c, 0x7e, 0x37, - 0x4b, 0x17, 0x9b, 0x6c, 0x0e, 0x52, 0xc1, 0x92, 0x86, 0xba, 0x80, 0xda, 0xd6, 0x62, 0x28, 0xd1, 0x8e, 0xd4, 0xea, - 0x84, 0x2f, 0xa5, 0x80, 0xa5, 0x32, 0x22, 0x67, 0xa8, 0xad, 0xc1, 0xa9, 0xa3, 0x34, 0x71, 0xdd, 0xab, 0x0a, 0xbe, - 0x28, 0xf3, 0xc8, 0x9d, 0x31, 0xfc, 0xd2, 0xc7, 0xeb, 0x90, 0x8c, 0x91, 0x69, 0x36, 0x70, 0x7e, 0x9a, 0x15, 0xeb, - 0x1d, 0x7c, 0x21, 0x74, 0xea, 0xd4, 0x4c, 0xbe, 0x40, 0xdd, 0x0a, 0x4a, 0x32, 0x1c, 0x7c, 0xad, 0x8a, 0x5b, 0xb4, - 0x12, 0xf7, 0x1f, 0x90, 0xf5, 0x49, 0x2b, 0x69, 0xd1, 0x9e, 0x56, 0x56, 0x04, 0xa5, 0x65, 0x52, 0xb5, 0x29, 0x4c, - 0xbf, 0x14, 0x1d, 0xd5, 0xd3, 0xba, 0x7b, 0x3f, 0xe4, 0x76, 0xc9, 0x25, 0xdb, 0x7d, 0x8b, 0x34, 0x34, 0xba, 0xda, - 0x15, 0x80, 0xb4, 0xeb, 0x4d, 0x5f, 0x85, 0xcc, 0x53, 0xd2, 0x94, 0x92, 0x1e, 0x1c, 0xb2, 0x23, 0x34, 0xbf, 0xef, - 0xc6, 0x56, 0x1d, 0xe9, 0x4e, 0x05, 0xfb, 0xce, 0x2f, 0x73, 0xbb, 0x19, 0x9c, 0xc4, 0xe7, 0x36, 0x7e, 0xed, 0x11, - 0x40, 0xb6, 0xa8, 0x84, 0xaf, 0x4d, 0x39, 0x68, 0x97, 0x5f, 0xe2, 0x99, 0x9a, 0x1d, 0x0a, 0xef, 0xf3, 0xd6, 0x69, - 0xba, 0x75, 0x6c, 0x94, 0x4a, 0x1e, 0x7e, 0xa3, 0x42, 0xb6, 0x62, 0x77, 0x56, 0xb8, 0x00, 0x73, 0xfe, 0xaa, 0x20, - 0xea, 0x4a, 0x56, 0xdb, 0x45, 0x8d, 0xc1, 0x06, 0xda, 0x38, 0xd4, 0x2b, 0x44, 0xcc, 0x3b, 0x46, 0x39, 0x42, 0x87, - 0xa4, 0x43, 0x49, 0x27, 0xd3, 0x40, 0x4e, 0xac, 0x3a, 0x24, 0xd8, 0x9f, 0x8e, 0x94, 0x03, 0xf8, 0x9f, 0x4c, 0x91, - 0xe5, 0x9f, 0xea, 0x55, 0xce, 0xd4, 0x29, 0xfe, 0x5c, 0xb2, 0x6b, 0x76, 0x94, 0x5a, 0x4d, 0x35, 0xee, 0x17, 0x4d, - 0x01, 0xa3, 0x52, 0x5e, 0xcb, 0x8e, 0xdc, 0xcc, 0x91, 0x14, 0xff, 0x60, 0xb2, 0xf4, 0xa4, 0x7f, 0x7c, 0xc8, 0xa5, - 0xaf, 0x9c, 0x7b, 0xf5, 0xce, 0x22, 0xa7, 0x2a, 0xdd, 0xfd, 0x34, 0x77, 0x9e, 0xfe, 0xfe, 0x92, 0x9d, 0x1f, 0xfd, - 0xc5, 0x43, 0x74, 0x86, 0xbf, 0x60, 0x43, 0xec, 0xc1, 0xda, 0x65, 0xe1, 0xc9, 0xeb, 0xf3, 0x43, 0xa3, 0x4f, 0x19, - 0x58, 0xf2, 0xee, 0x82, 0x96, 0x40, 0x99, 0xd7, 0x94, 0xa5, 0x5a, 0xdf, 0x17, 0xd3, 0xa7, 0x2b, 0x76, 0xbe, 0x98, - 0x55, 0x5b, 0x6d, 0xdf, 0x97, 0xd5, 0x6d, 0x75, 0xff, 0x72, 0xf6, 0xe1, 0xaf, 0xdb, 0x7b, 0x3e, 0x31, 0x01, 0x08, - 0xec, 0xf4, 0x50, 0xf5, 0x8b, 0x9f, 0xab, 0xb2, 0x98, 0xaa, 0xba, 0x38, 0xab, 0xc6, 0xc5, 0x79, 0x35, 0x3d, 0xfc, - 0x74, 0xc4, 0x0f, 0x3c, 0x12, 0x86, 0xd5, 0x89, 0x06, 0x59, 0x5b, 0xfc, 0xd2, 0xd4, 0x32, 0xcb, 0x27, 0x8a, 0xdd, - 0x4a, 0xad, 0x3f, 0xed, 0xd2, 0xf8, 0xd3, 0x64, 0x79, 0x23, 0x05, 0xbd, 0x52, 0xd1, 0x2e, 0x27, 0xb6, 0xd3, 0x4c, - 0x2c, 0x48, 0x2c, 0x65, 0xa7, 0xbd, 0xb5, 0x0e, 0x39, 0x83, 0x41, 0x6f, 0xbf, 0xe4, 0x1a, 0xcf, 0x22, 0x8c, 0x99, - 0xbc, 0xa1, 0xb7, 0x4c, 0x05, 0x5f, 0xa1, 0x1a, 0x33, 0xeb, 0x3b, 0x51, 0x47, 0x12, 0x0b, 0x82, 0x18, 0xba, 0xd4, - 0x49, 0xed, 0xed, 0xd2, 0xd5, 0xad, 0xab, 0xbe, 0x04, 0x70, 0x2d, 0xd6, 0x94, 0x9e, 0xfa, 0xa2, 0x46, 0x31, 0x3a, - 0x2a, 0x4b, 0x66, 0xaa, 0x84, 0x8a, 0x1e, 0x62, 0x7d, 0xcb, 0xbc, 0xce, 0xca, 0x73, 0x33, 0x4c, 0xd3, 0x2d, 0xcd, - 0x00, 0x5f, 0xd1, 0x85, 0xac, 0xcc, 0x05, 0x6f, 0x29, 0x99, 0xd6, 0x23, 0xe3, 0x54, 0xd3, 0xba, 0x7a, 0x44, 0xf6, - 0xf2, 0x97, 0xb7, 0x40, 0x64, 0x1f, 0xfa, 0xa2, 0xf6, 0x59, 0x94, 0xad, 0x30, 0x89, 0x41, 0xa6, 0x21, 0xe4, 0x28, - 0x0d, 0xd1, 0x88, 0xb3, 0x78, 0xb4, 0xab, 0x20, 0xb1, 0xf1, 0x59, 0x7e, 0xcd, 0x8c, 0xbd, 0x0e, 0x20, 0x16, 0xa8, - 0xb8, 0x2c, 0xbd, 0xe0, 0xff, 0x41, 0x0d, 0xe5, 0xbe, 0xe9, 0x7f, 0xa0, 0x98, 0x14, 0xca, 0xcd, 0xd0, 0x8f, 0x4b, - 0xae, 0x60, 0x13, 0x62, 0xd0, 0x83, 0x15, 0x51, 0x9d, 0xc5, 0xbe, 0x45, 0x9d, 0x40, 0x0a, 0x38, 0x50, 0x9c, 0x41, - 0xe3, 0x44, 0x01, 0x8e, 0x06, 0xad, 0xb5, 0x48, 0x85, 0x50, 0x78, 0x3f, 0xea, 0xaa, 0x75, 0x39, 0xd2, 0xd0, 0x4d, - 0xa4, 0xdf, 0xea, 0xd7, 0x56, 0x94, 0xc1, 0x9c, 0x5f, 0xae, 0xbc, 0xf9, 0xa0, 0xe4, 0xef, 0xdb, 0x3f, 0xa9, 0x0b, - 0x54, 0xf4, 0x0e, 0x1c, 0x46, 0xb4, 0x39, 0x62, 0x6c, 0x61, 0x71, 0x18, 0x5b, 0xea, 0x09, 0xb1, 0xfe, 0x0e, 0x3d, - 0xc2, 0xd9, 0x37, 0x49, 0xad, 0x79, 0x39, 0x99, 0xe5, 0x76, 0x3b, 0xba, 0xdd, 0xf9, 0x99, 0x29, 0xfc, 0xa4, 0xe6, - 0x60, 0x51, 0xef, 0x49, 0xa4, 0x01, 0xba, 0x5e, 0x38, 0x8f, 0xc0, 0xf5, 0x28, 0x49, 0xc1, 0x64, 0x40, 0x13, 0x1a, - 0x3b, 0x62, 0x65, 0xc5, 0x59, 0x1a, 0x8d, 0xce, 0x85, 0xab, 0xa2, 0xfa, 0xfb, 0xcb, 0x62, 0x2e, 0x00, 0x8c, 0x20, - 0xf4, 0xc1, 0x1b, 0xbb, 0x9d, 0x36, 0xbd, 0xda, 0x96, 0x34, 0xc4, 0x11, 0x44, 0x65, 0x41, 0xc5, 0x2e, 0xa8, 0x3a, - 0xda, 0x2f, 0xa8, 0x1c, 0x27, 0xd5, 0x90, 0x9f, 0x7a, 0x65, 0xb9, 0x0b, 0xfe, 0xdc, 0xa3, 0x5a, 0xfd, 0xf3, 0x43, - 0xc3, 0x53, 0xfd, 0x43, 0x98, 0xf7, 0x95, 0xf2, 0x3c, 0x97, 0x7c, 0x6c, 0x12, 0xc9, 0xd5, 0x56, 0x05, 0x1f, 0x1e, - 0x4a, 0x7a, 0x2b, 0x6a, 0x16, 0x58, 0x6f, 0x0f, 0xcf, 0x6b, 0xcf, 0x61, 0xc6, 0x8e, 0xfa, 0x25, 0x51, 0x37, 0x67, - 0xff, 0x0d, 0x06, 0xf6, 0x9b, 0x56, 0x72, 0xae, 0x9b, 0xf5, 0x9e, 0x27, 0xc5, 0x7a, 0x3d, 0xbf, 0xa2, 0x81, 0x8d, - 0x7d, 0xf6, 0x99, 0x3f, 0xa0, 0x61, 0x90, 0x3d, 0x5d, 0x37, 0xe7, 0xb4, 0xce, 0xce, 0xb9, 0x72, 0xd8, 0x69, 0x33, - 0x7e, 0xd2, 0xbd, 0xe5, 0xa0, 0xda, 0x02, 0xf9, 0x9d, 0xfd, 0x84, 0x38, 0x69, 0xf9, 0xf9, 0x69, 0xb4, 0x33, 0x0b, - 0x21, 0x0f, 0xce, 0x76, 0x2b, 0x20, 0xe5, 0x65, 0x76, 0x01, 0x49, 0x73, 0xa1, 0xe7, 0x38, 0x2a, 0x45, 0x82, 0x2f, - 0x03, 0x66, 0xdd, 0x35, 0x02, 0xd3, 0xf5, 0x6e, 0x65, 0xde, 0xc5, 0xaa, 0x06, 0x9d, 0xd7, 0x36, 0x6d, 0xdf, 0x7c, - 0xa5, 0x3b, 0x9e, 0xbe, 0x28, 0x16, 0x3b, 0xac, 0xdc, 0xe5, 0x20, 0x7f, 0xaf, 0x04, 0x1e, 0x05, 0xf0, 0x5e, 0x4c, - 0xd2, 0x4f, 0xf0, 0x74, 0x27, 0x13, 0x98, 0xa8, 0x86, 0xa4, 0x6c, 0x75, 0x77, 0x23, 0x9b, 0x51, 0x35, 0xd0, 0x29, - 0x47, 0x8e, 0x78, 0xf5, 0xb3, 0xf6, 0x98, 0x07, 0x3b, 0xf7, 0xad, 0x17, 0x7e, 0x94, 0x0d, 0x15, 0x96, 0x67, 0x0c, - 0x0d, 0x38, 0x65, 0x58, 0x5c, 0xc6, 0x60, 0x40, 0x6e, 0xae, 0xe3, 0x46, 0x0a, 0xcd, 0x3f, 0x47, 0x3f, 0xa6, 0xa0, - 0x06, 0xea, 0x8d, 0xeb, 0xf1, 0xa1, 0x19, 0xec, 0x97, 0xbf, 0x01, 0x8f, 0x0f, 0x32, 0xa0, 0x9a, 0x85, 0xce, 0x68, - 0xe3, 0x69, 0x9e, 0x7f, 0xd2, 0xb7, 0xb9, 0xe4, 0xfc, 0x47, 0xff, 0x34, 0x1b, 0xa7, 0xce, 0xc9, 0x99, 0x26, 0xc1, - 0x79, 0x0a, 0x5d, 0x9d, 0xfd, 0x7f, 0x97, 0x6c, 0x64, 0x15, 0x2f, 0x9a, 0x47, 0x71, 0x75, 0x81, 0x28, 0xaa, 0xf5, - 0x91, 0x67, 0xed, 0xce, 0x5e, 0xec, 0x7b, 0x38, 0x0c, 0x7a, 0x83, 0x0f, 0x7e, 0xaa, 0xf2, 0x24, 0x66, 0xfd, 0xca, - 0x44, 0xca, 0x25, 0x7e, 0x4a, 0x5d, 0xd9, 0xd7, 0x49, 0xb3, 0x0f, 0x97, 0xa6, 0x34, 0x1c, 0xd8, 0x94, 0x62, 0x8d, - 0x0a, 0xb0, 0x5f, 0x89, 0xd2, 0xb7, 0x76, 0xce, 0xd0, 0x07, 0xff, 0xac, 0x0a, 0x2c, 0x4e, 0xeb, 0x32, 0x40, 0x52, - 0xd7, 0xe3, 0xca, 0x7e, 0x3d, 0x09, 0x88, 0x8b, 0x7c, 0x85, 0x36, 0x47, 0x8c, 0x51, 0x91, 0x0b, 0xd1, 0x41, 0xe6, - 0xaa, 0x62, 0xa2, 0xd6, 0xa7, 0x17, 0xb4, 0xfb, 0x6e, 0x22, 0x2e, 0xd4, 0xd0, 0xf9, 0x57, 0x27, 0x16, 0x94, 0x36, - 0xc7, 0xf6, 0x8e, 0xd0, 0x23, 0x97, 0xf1, 0x11, 0x41, 0x12, 0x5f, 0x4f, 0x61, 0xde, 0x7e, 0xc7, 0x8f, 0xab, 0x08, - 0x20, 0x81, 0x77, 0x8b, 0xb8, 0x19, 0x18, 0x4a, 0x12, 0xa8, 0x9a, 0x5a, 0xeb, 0x01, 0x13, 0xf3, 0x4e, 0x47, 0xe1, - 0x56, 0x54, 0x20, 0xf0, 0x10, 0x99, 0xd8, 0x83, 0x44, 0x56, 0x8f, 0xa2, 0x87, 0x3b, 0xda, 0xe9, 0x4a, 0xa6, 0x68, - 0x04, 0x25, 0xda, 0xf4, 0x90, 0xa4, 0x87, 0x2f, 0x9b, 0x89, 0xde, 0x89, 0x73, 0xd3, 0x1f, 0xf5, 0x5e, 0xcb, 0xfe, - 0x77, 0x5d, 0x47, 0xf6, 0x2e, 0x63, 0x44, 0xcc, 0xe1, 0x51, 0xb6, 0x9e, 0xac, 0x8e, 0xdb, 0x3e, 0xe4, 0xdc, 0x0b, - 0x8a, 0x01, 0x68, 0x6f, 0x0e, 0xdd, 0x77, 0xa5, 0x44, 0xad, 0xeb, 0xd6, 0x43, 0xca, 0x35, 0x12, 0xfd, 0xc5, 0xf7, - 0xe7, 0x77, 0xb5, 0xc9, 0xc9, 0x26, 0x0a, 0x15, 0x4d, 0xf2, 0x18, 0x44, 0x87, 0x97, 0xc6, 0x30, 0xea, 0xc5, 0xc5, - 0x18, 0xb1, 0xa7, 0xd3, 0x28, 0x6e, 0x61, 0x31, 0x5a, 0x65, 0x6f, 0x11, 0x62, 0x5d, 0x3a, 0x35, 0x4c, 0x51, 0xf5, - 0xdf, 0x9f, 0x46, 0xb5, 0x3b, 0x05, 0x11, 0xf8, 0x7a, 0xee, 0x58, 0xb2, 0x0b, 0xa8, 0x97, 0xf3, 0x77, 0xac, 0x68, - 0xd3, 0x69, 0x1f, 0x84, 0x71, 0x8c, 0xcc, 0x7b, 0xf9, 0xb6, 0x08, 0x31, 0x94, 0x12, 0xa4, 0xe0, 0x6b, 0xc7, 0x30, - 0x08, 0x0e, 0xf3, 0xf2, 0x31, 0xb4, 0xff, 0x10, 0xee, 0xc8, 0x8c, 0x31, 0x99, 0xe2, 0xde, 0x00, 0xeb, 0x0d, 0x77, - 0xd8, 0x47, 0x47, 0xbd, 0xd2, 0xe4, 0x4e, 0x12, 0x7b, 0x9a, 0x49, 0x8e, 0xde, 0xed, 0xd2, 0x28, 0x53, 0x3a, 0x7c, - 0x33, 0x89, 0xf8, 0x56, 0x9c, 0x10, 0xa9, 0xba, 0xac, 0xad, 0xae, 0xfd, 0xbe, 0x74, 0x1c, 0xdd, 0xb3, 0x6b, 0xbd, - 0x8f, 0x62, 0x6c, 0xd5, 0x9b, 0x9a, 0x6d, 0xea, 0xa7, 0xa1, 0x40, 0x8e, 0x0e, 0x77, 0xba, 0x95, 0x4c, 0xc7, 0xea, - 0xf2, 0x17, 0x6d, 0x5b, 0xe4, 0x0b, 0x03, 0x98, 0x9e, 0xba, 0xb7, 0x59, 0xed, 0x27, 0x44, 0x89, 0xf4, 0x81, 0x98, - 0x25, 0x3e, 0x4a, 0x01, 0xe3, 0x2b, 0xa7, 0x89, 0x6c, 0xf0, 0xb3, 0xfc, 0x5c, 0xc4, 0xed, 0xae, 0xf1, 0x9c, 0x4f, - 0x00, 0xbd, 0x1f, 0x8f, 0xb3, 0x33, 0x68, 0xe7, 0xdb, 0x74, 0xa6, 0x53, 0x79, 0x31, 0xfd, 0xb3, 0xff, 0xcf, 0xf4, - 0x40, 0xfd, 0x01, 0x24, 0x1a, 0xff, 0xf7, 0x22, 0x93, 0xd7, 0x6a, 0x24, 0x26, 0x07, 0x31, 0xea, 0x1e, 0x14, 0x8b, - 0x68, 0x08, 0xe0, 0x2b, 0x2f, 0x88, 0x1b, 0x1c, 0x1e, 0x15, 0x3e, 0x4d, 0xef, 0x0e, 0xe4, 0x70, 0xa7, 0xe3, 0x49, - 0x5b, 0xdc, 0x57, 0xc9, 0xcd, 0x8c, 0xfd, 0x3e, 0x83, 0x68, 0x18, 0x14, 0x7d, 0x81, 0x41, 0x29, 0xe4, 0xe7, 0x4b, - 0xf1, 0xa5, 0x99, 0xab, 0x2b, 0xa3, 0xa4, 0xb5, 0x82, 0xf5, 0x2a, 0xa4, 0x06, 0x12, 0xef, 0xa5, 0xf0, 0x19, 0xf4, - 0x14, 0x8a, 0xfd, 0xfe, 0xd4, 0x29, 0x27, 0x68, 0x2f, 0xab, 0xd2, 0xa4, 0x57, 0x92, 0xdb, 0x7b, 0x67, 0x1d, 0xfd, - 0x04, 0x28, 0xc7, 0x0f, 0xa2, 0xc5, 0xd7, 0x0e, 0x8b, 0x72, 0xbb, 0x54, 0x75, 0x1c, 0x43, 0xf0, 0xfc, 0xc9, 0xb3, - 0xb0, 0x5d, 0x91, 0x9e, 0xfe, 0x6d, 0xb1, 0xe9, 0xbb, 0x73, 0xab, 0xe1, 0xff, 0xe4, 0xb3, 0x3f, 0xf0, 0x36, 0x3d, - 0xeb, 0xcf, 0xd8, 0x48, 0xe5, 0x5d, 0xc2, 0xe5, 0x36, 0xb1, 0xf9, 0x02, 0x86, 0xe1, 0x71, 0x7b, 0x9e, 0x08, 0x89, - 0xfd, 0xa6, 0x30, 0xb3, 0xc7, 0xb1, 0x68, 0x25, 0xc2, 0xdf, 0xee, 0x46, 0xde, 0xf9, 0x4f, 0x87, 0x25, 0x08, 0xc3, - 0xb9, 0x71, 0xa6, 0xdf, 0x33, 0xda, 0x7f, 0x9a, 0xa7, 0x4f, 0x7f, 0x77, 0xc9, 0xe9, 0x8f, 0xfe, 0x69, 0xf6, 0xbd, - 0x7d, 0x55, 0xa2, 0x77, 0xc0, 0x66, 0xdf, 0x44, 0x8c, 0x9a, 0xbc, 0x9e, 0x53, 0x0e, 0x7a, 0x44, 0x57, 0x33, 0xe1, - 0xe5, 0x09, 0x5c, 0xa0, 0x61, 0x54, 0xe7, 0x3d, 0xcf, 0xc1, 0x0b, 0x65, 0xbb, 0xa3, 0x58, 0x92, 0x68, 0xb3, 0x90, - 0x3b, 0xf4, 0x53, 0x83, 0x28, 0xc1, 0xac, 0xfb, 0x49, 0xb2, 0x47, 0x6d, 0x35, 0x4c, 0xac, 0x52, 0x5d, 0x7c, 0xe7, - 0x5a, 0x26, 0x29, 0xe5, 0x55, 0xbc, 0x53, 0x89, 0xbc, 0xf9, 0x21, 0xcc, 0x98, 0x0d, 0x46, 0x2f, 0x84, 0xb0, 0xdf, - 0x29, 0x02, 0x23, 0x47, 0x15, 0x2c, 0x24, 0x7e, 0xbb, 0x03, 0x24, 0xde, 0xbe, 0x0b, 0xd2, 0x57, 0x12, 0x20, 0x5f, - 0xcb, 0x96, 0x53, 0x9b, 0x9d, 0x1b, 0xe1, 0xb0, 0x47, 0xe9, 0x1b, 0xef, 0x91, 0x6f, 0x64, 0xd2, 0x56, 0xa9, 0x1f, - 0x03, 0xcc, 0xce, 0xd6, 0x61, 0x64, 0xc4, 0x0e, 0xe4, 0x10, 0x53, 0xb1, 0x03, 0x04, 0xb3, 0x0e, 0xfd, 0x1c, 0xf8, - 0x63, 0xd7, 0x0d, 0x40, 0x34, 0x6b, 0x2e, 0x7d, 0x92, 0xb1, 0x9d, 0x1c, 0x8e, 0x4d, 0x04, 0xe3, 0x7d, 0xa9, 0xfb, - 0xac, 0x79, 0x8a, 0x94, 0x6a, 0x89, 0x14, 0x34, 0x20, 0xbd, 0x8a, 0x3b, 0xf7, 0x6c, 0x0e, 0x46, 0x9c, 0xec, 0xef, - 0x4a, 0xa9, 0x3e, 0xdc, 0xb8, 0xcb, 0xa1, 0x71, 0x5e, 0x1e, 0xb0, 0x8b, 0xcd, 0xa0, 0x04, 0xda, 0xe9, 0x34, 0x4f, - 0xd6, 0x1a, 0xcc, 0xb9, 0x26, 0x25, 0x29, 0x0b, 0x9f, 0x90, 0x19, 0xb9, 0xf9, 0xbe, 0xbc, 0xbe, 0xe5, 0xc3, 0x68, - 0x4e, 0x29, 0xd8, 0x2b, 0x7d, 0xd3, 0xa7, 0xfb, 0xba, 0xfc, 0xdc, 0x05, 0xdd, 0xda, 0x41, 0x2b, 0x17, 0x0f, 0xfb, - 0x93, 0x47, 0x02, 0xc8, 0x04, 0xf1, 0xc3, 0x0d, 0xcb, 0xee, 0xbe, 0x4f, 0x60, 0xf6, 0x8d, 0x5f, 0xec, 0xa7, 0x0c, - 0x83, 0x6f, 0xec, 0x66, 0x95, 0x60, 0x39, 0xfc, 0x3f, 0xf7, 0xcf, 0xb6, 0x5e, 0xec, 0x26, 0x87, 0xab, 0xfd, 0xba, - 0x7d, 0x06, 0x18, 0x7b, 0xbf, 0x5c, 0x27, 0x54, 0xc2, 0x48, 0x6d, 0xd1, 0xe4, 0xab, 0xc2, 0x99, 0x3d, 0x9c, 0x4c, - 0xd9, 0x4e, 0xa1, 0x16, 0x69, 0x1c, 0xd7, 0x39, 0x47, 0x5a, 0xa0, 0x8d, 0x65, 0xb1, 0x68, 0x14, 0x09, 0x9d, 0x60, - 0x8b, 0x8d, 0x1c, 0xf7, 0xc3, 0xfa, 0x6c, 0x98, 0xf1, 0x96, 0x28, 0xb4, 0xe0, 0x6c, 0xc4, 0x44, 0x90, 0x51, 0x35, - 0x06, 0xa1, 0x1d, 0x72, 0xb0, 0x00, 0xd5, 0xd0, 0x29, 0x82, 0xe7, 0xc6, 0x9f, 0x16, 0x3f, 0x2e, 0x0c, 0x5e, 0x42, - 0x32, 0x0c, 0x12, 0x40, 0x8a, 0xc9, 0x4a, 0xba, 0x71, 0x6f, 0xb7, 0x70, 0xbc, 0x2f, 0x98, 0x6a, 0xec, 0xa7, 0xdd, - 0xa3, 0x9b, 0x0e, 0xd4, 0x8b, 0x8f, 0x06, 0x86, 0xed, 0x8e, 0x21, 0xf3, 0xca, 0x88, 0xce, 0x44, 0xcf, 0xfb, 0x38, - 0xe9, 0xb1, 0x55, 0x98, 0x23, 0xcc, 0x08, 0xbe, 0x31, 0x99, 0x8d, 0x3c, 0xc2, 0xdd, 0x6e, 0x3f, 0x9a, 0xe3, 0xd8, - 0x1a, 0x7b, 0x85, 0x50, 0xa8, 0x78, 0xcb, 0x74, 0x37, 0xa1, 0x59, 0x87, 0xcd, 0x3d, 0xd4, 0xd9, 0x55, 0x06, 0xfa, - 0x2c, 0xab, 0x04, 0x27, 0xf2, 0xf6, 0xdb, 0xe8, 0x42, 0x03, 0x27, 0x68, 0x6b, 0xa3, 0x87, 0x7f, 0x88, 0xd0, 0xb7, - 0xa0, 0x4e, 0x38, 0x29, 0xdf, 0x19, 0x8f, 0x89, 0x41, 0xd4, 0x38, 0x4e, 0x95, 0x59, 0x4e, 0x4f, 0x76, 0x23, 0x57, - 0x4a, 0xae, 0xb0, 0x9c, 0x59, 0x5a, 0x36, 0x4b, 0x05, 0x78, 0xff, 0x51, 0x17, 0xc7, 0x84, 0x94, 0xab, 0x46, 0x6d, - 0xea, 0x81, 0x86, 0x4f, 0xa3, 0x95, 0x54, 0x56, 0x36, 0xf1, 0x87, 0x1e, 0xee, 0xf4, 0x07, 0xd1, 0xdd, 0x8a, 0x6a, - 0x93, 0xdb, 0xd0, 0x78, 0x42, 0x8f, 0x29, 0xec, 0x83, 0x45, 0xa0, 0xce, 0xa3, 0xf0, 0xf0, 0xf8, 0x3b, 0x26, 0x6f, - 0x24, 0xd1, 0xad, 0xc0, 0xcd, 0xe2, 0x07, 0x2e, 0x58, 0x24, 0x39, 0x5a, 0xc5, 0xd2, 0xbb, 0xd3, 0xb2, 0x35, 0xa9, - 0xfc, 0x84, 0xb6, 0xaf, 0xaf, 0xe5, 0x55, 0x0b, 0xac, 0xc4, 0xec, 0x55, 0x23, 0xf9, 0x45, 0x29, 0x0e, 0xec, 0x80, - 0x69, 0x91, 0x6b, 0x34, 0xcc, 0xd4, 0xb2, 0x79, 0x30, 0xee, 0xe9, 0x36, 0x1c, 0x4a, 0x67, 0x77, 0x7f, 0xa1, 0x09, - 0x0e, 0xa1, 0x29, 0xa9, 0x09, 0x93, 0x7c, 0x3c, 0xb5, 0x71, 0x62, 0x15, 0xb5, 0x60, 0xb2, 0xe5, 0xb8, 0xe5, 0xb5, - 0x3a, 0xa6, 0xea, 0xa5, 0xf7, 0x31, 0x90, 0x24, 0xd3, 0x38, 0xa1, 0x72, 0x70, 0x43, 0xbc, 0x42, 0xc1, 0x69, 0x7b, - 0x1a, 0x27, 0x76, 0x28, 0x6f, 0xff, 0x2a, 0xde, 0x56, 0x68, 0xfe, 0x15, 0x4e, 0xde, 0xcb, 0xf5, 0xbb, 0x6e, 0xb8, - 0x99, 0xd8, 0x0d, 0xbb, 0xfd, 0xab, 0x69, 0xab, 0x54, 0xec, 0xe9, 0xa4, 0xe7, 0x23, 0x1f, 0x00, 0xf8, 0xf3, 0xca, - 0x04, 0xf9, 0x64, 0x98, 0x11, 0xb5, 0x09, 0xc2, 0x4c, 0x65, 0xc4, 0xf8, 0xa6, 0x2a, 0x37, 0xb5, 0x68, 0x45, 0x62, - 0x49, 0x69, 0x1a, 0x67, 0xe7, 0x8e, 0x34, 0x3b, 0xee, 0x8e, 0xd8, 0x6d, 0x89, 0xb9, 0x7e, 0x9a, 0xf4, 0x34, 0x58, - 0x85, 0x22, 0x54, 0x9e, 0x50, 0xae, 0x29, 0x47, 0x7b, 0xd0, 0x8d, 0xba, 0x86, 0x0c, 0x86, 0x54, 0xa1, 0x8c, 0x5e, - 0xec, 0x3c, 0x22, 0x70, 0x54, 0xa1, 0x87, 0x0c, 0xa4, 0xa8, 0x88, 0x66, 0x33, 0x7e, 0x7c, 0xfe, 0x95, 0xa2, 0x2d, - 0xea, 0x06, 0xe1, 0x10, 0x80, 0xac, 0x77, 0x87, 0x43, 0x08, 0x5c, 0xff, 0x0e, 0xcb, 0xd6, 0xa8, 0x51, 0x46, 0x06, - 0x36, 0x64, 0x3d, 0x45, 0xfa, 0x8f, 0x51, 0x5d, 0x91, 0x49, 0xdd, 0xac, 0x50, 0x46, 0x90, 0x41, 0xcc, 0x3b, 0x4a, - 0x9b, 0x6f, 0x86, 0xd1, 0x91, 0x35, 0x8a, 0x30, 0x15, 0xbb, 0x41, 0xe1, 0xaa, 0x3f, 0x48, 0x91, 0x5d, 0x88, 0x38, - 0x05, 0x78, 0x77, 0x6a, 0x48, 0xd4, 0xac, 0xa9, 0x68, 0xf8, 0x18, 0x7a, 0xee, 0xcc, 0xbb, 0x0d, 0x07, 0x12, 0xc2, - 0x22, 0x35, 0xd8, 0x81, 0x68, 0x0b, 0x32, 0x16, 0xe1, 0x8d, 0x48, 0x34, 0xd4, 0x7b, 0x02, 0xf0, 0x6e, 0xdd, 0xa7, - 0xbc, 0x03, 0x80, 0x3e, 0x59, 0x39, 0x91, 0xee, 0x8f, 0x07, 0x72, 0x88, 0xb9, 0xd9, 0x91, 0xba, 0x43, 0x5c, 0x8a, - 0xf3, 0x89, 0x62, 0xbd, 0x20, 0x07, 0x91, 0xa0, 0x15, 0xaf, 0xc9, 0x45, 0x99, 0xb4, 0xf3, 0xae, 0x33, 0xd7, 0xb9, - 0x26, 0x9e, 0xe4, 0xa8, 0x33, 0x51, 0x4c, 0xee, 0x99, 0x7c, 0xad, 0xdb, 0xb0, 0xda, 0x41, 0x9f, 0x10, 0xe3, 0xc9, - 0x58, 0xa6, 0x1e, 0xd9, 0xd9, 0x78, 0x36, 0xe2, 0x50, 0x01, 0x2d, 0x1d, 0xdc, 0x72, 0xd9, 0xac, 0xf9, 0x19, 0x77, - 0xfc, 0xb0, 0x09, 0x1f, 0xad, 0xe2, 0xda, 0xf4, 0xe9, 0x65, 0x90, 0x06, 0xf3, 0xa1, 0xa4, 0xe0, 0x4a, 0xaa, 0xb1, - 0xef, 0x4d, 0x25, 0xb5, 0x7f, 0xb7, 0x99, 0x9a, 0xb5, 0x58, 0xf1, 0x64, 0x5c, 0x04, 0x91, 0xf9, 0xfa, 0xdd, 0xd4, - 0x8c, 0xa3, 0xdd, 0xb4, 0x20, 0x42, 0x5f, 0xe5, 0x62, 0x64, 0x39, 0xfd, 0xa6, 0x89, 0x37, 0x37, 0x84, 0x3e, 0x62, - 0xfa, 0xb3, 0x8d, 0x39, 0x3e, 0x3b, 0xbc, 0x50, 0x43, 0x0f, 0xda, 0x20, 0x22, 0x35, 0x4e, 0x77, 0xb0, 0x48, 0x64, - 0x4b, 0x78, 0x45, 0xd1, 0x8a, 0xb9, 0xfa, 0xe1, 0x90, 0xb1, 0x44, 0x26, 0x88, 0x34, 0xfa, 0xf1, 0xc3, 0x2e, 0x1d, - 0xb6, 0x1e, 0x86, 0xb1, 0x02, 0x5c, 0xe6, 0x25, 0x25, 0x6f, 0xac, 0xe0, 0xb7, 0x9f, 0x03, 0xd3, 0xbc, 0xdf, 0xde, - 0x35, 0xbd, 0x11, 0x2f, 0xd5, 0x8d, 0xd3, 0x3b, 0x14, 0x4a, 0x42, 0x94, 0xd3, 0xc6, 0xc5, 0xc5, 0x9c, 0x3d, 0x0d, - 0x2c, 0xf2, 0x72, 0xc5, 0xd2, 0x2e, 0x7e, 0x0d, 0xa2, 0x61, 0xc5, 0x3b, 0x08, 0xe9, 0x22, 0xbb, 0xce, 0xf0, 0x00, - 0x8d, 0xea, 0xe1, 0x1e, 0x6d, 0xd1, 0x05, 0x04, 0x99, 0x63, 0xf4, 0x68, 0xa0, 0x04, 0x14, 0x7c, 0xc5, 0x09, 0x74, - 0x95, 0xd6, 0xcc, 0xb3, 0x35, 0x32, 0x63, 0x02, 0x84, 0xd3, 0xfa, 0x93, 0x08, 0x2e, 0x21, 0x73, 0xb8, 0x54, 0xd8, - 0x82, 0x8c, 0x5a, 0x29, 0x4e, 0x46, 0x01, 0x4d, 0x9f, 0x88, 0xe3, 0x17, 0xbd, 0x4b, 0x01, 0x38, 0x7a, 0x2c, 0xac, - 0x24, 0xf0, 0x99, 0xc6, 0x15, 0xb3, 0xcb, 0xa0, 0x39, 0xd0, 0xb8, 0xf6, 0xb5, 0xd5, 0x18, 0x8b, 0x8d, 0xd7, 0xdf, - 0x43, 0x84, 0x0d, 0xf6, 0x94, 0x42, 0xac, 0x48, 0x74, 0x80, 0xac, 0x5c, 0x43, 0x27, 0xef, 0xd9, 0xd3, 0xb1, 0xb5, - 0x5c, 0x41, 0x17, 0x3a, 0x92, 0x70, 0xad, 0xc1, 0x66, 0xff, 0x11, 0xe0, 0x4c, 0x43, 0x5a, 0xcf, 0x0c, 0x2b, 0x72, - 0x99, 0x82, 0x1a, 0xf1, 0xaf, 0x53, 0x07, 0x8b, 0x7a, 0x48, 0x17, 0x71, 0x2a, 0xea, 0x99, 0x56, 0x16, 0xe8, 0x84, - 0x3a, 0x52, 0x43, 0x6c, 0x00, 0x05, 0x6f, 0x94, 0x9e, 0x70, 0xfa, 0xdd, 0xa5, 0xe7, 0xa8, 0x2c, 0xb8, 0x0e, 0xcd, - 0xe2, 0x0f, 0x51, 0x6d, 0x3c, 0xfd, 0xf8, 0x60, 0x06, 0x0f, 0xe2, 0xed, 0x59, 0xc0, 0x87, 0x89, 0xb7, 0x63, 0xe7, - 0x79, 0x67, 0x37, 0x01, 0xc1, 0xac, 0x34, 0x11, 0x92, 0x11, 0xe6, 0xce, 0xbd, 0xc3, 0xd6, 0xf8, 0x2b, 0x76, 0x7f, - 0x29, 0x14, 0x06, 0xdb, 0x91, 0x08, 0xf3, 0xb1, 0x18, 0x45, 0xa8, 0xed, 0xe5, 0xd7, 0x2c, 0x19, 0xc9, 0xef, 0xce, - 0x9b, 0x8b, 0xb8, 0x1d, 0xd8, 0xaa, 0x54, 0xa9, 0x1f, 0x10, 0x55, 0xed, 0xf7, 0xb2, 0x61, 0x9b, 0x85, 0x8f, 0x17, - 0x3d, 0x3b, 0xf1, 0xc1, 0x72, 0x3d, 0xc7, 0x92, 0xdf, 0x3f, 0x43, 0x40, 0xcd, 0x66, 0xfb, 0xd5, 0xe2, 0xa0, 0xcf, - 0xb5, 0xf5, 0x1b, 0xb5, 0x81, 0x7e, 0x42, 0x58, 0xe0, 0xfb, 0x79, 0x8d, 0x5c, 0x3c, 0xca, 0xe6, 0xfa, 0x81, 0xdf, - 0x78, 0xb5, 0xc0, 0x3e, 0xbb, 0x33, 0x37, 0x9c, 0x1b, 0xc2, 0xd0, 0xf6, 0x44, 0xe3, 0xfe, 0x89, 0x49, 0x08, 0xaf, - 0xb3, 0x8a, 0x29, 0x9d, 0xc8, 0xac, 0xf2, 0x4f, 0xfa, 0x9d, 0xbb, 0x9b, 0xf9, 0x08, 0x25, 0xda, 0xdf, 0x80, 0xf3, - 0x72, 0xd5, 0x7e, 0x4d, 0xf2, 0x8c, 0x96, 0x1e, 0xb0, 0xa9, 0xa5, 0x9f, 0xeb, 0x95, 0xea, 0x40, 0xe9, 0xbe, 0x03, - 0x09, 0x30, 0x50, 0x87, 0x19, 0xbf, 0x8f, 0xcd, 0x10, 0x6e, 0x4a, 0x30, 0x06, 0x9e, 0xe9, 0x3f, 0x7c, 0x81, 0x83, - 0xb3, 0x92, 0x81, 0x39, 0xa2, 0xe6, 0x15, 0x41, 0xc0, 0xe7, 0x12, 0x54, 0xc8, 0x6e, 0x05, 0xf2, 0xf3, 0xbc, 0x72, - 0xe4, 0x06, 0x90, 0x5b, 0x21, 0xa8, 0xb8, 0x27, 0xcf, 0x5c, 0x1a, 0xd0, 0x03, 0x50, 0xfe, 0xe1, 0x9c, 0x93, 0x84, - 0xfe, 0x26, 0xa0, 0xa8, 0xd1, 0x49, 0x7f, 0xfe, 0xb5, 0x66, 0x64, 0xf2, 0xe7, 0xb1, 0x5f, 0x79, 0xbc, 0xec, 0xe6, - 0x2d, 0xc8, 0x48, 0x1b, 0xdf, 0x86, 0x19, 0x99, 0x81, 0x8e, 0x55, 0x50, 0x5b, 0xf8, 0x42, 0xaa, 0x55, 0x40, 0xae, - 0x2e, 0x42, 0x8b, 0x14, 0xb7, 0x90, 0xd3, 0x9f, 0xb6, 0xb3, 0x90, 0x7f, 0x9a, 0x01, 0x8e, 0x59, 0xf9, 0xcf, 0xc6, - 0x15, 0x45, 0xf6, 0x10, 0x18, 0xcd, 0x8f, 0x2e, 0x15, 0xd4, 0xb4, 0x72, 0x12, 0x7f, 0x02, 0x72, 0x09, 0x12, 0x30, - 0x3e, 0xbf, 0x51, 0x7b, 0xff, 0x9d, 0xce, 0x52, 0x8b, 0xaa, 0x63, 0xa4, 0x9f, 0xfc, 0x1a, 0xf2, 0x1f, 0xe0, 0x47, - 0x1f, 0x91, 0xd2, 0xd9, 0x3c, 0x5b, 0xfd, 0x89, 0xab, 0xd8, 0x65, 0x41, 0x75, 0x02, 0x2a, 0x48, 0x58, 0x05, 0xb5, - 0x06, 0x23, 0xfb, 0x1f, 0x16, 0xae, 0x46, 0x4c, 0xf3, 0xa7, 0x5b, 0xb4, 0x1a, 0xba, 0x57, 0xa0, 0xea, 0x70, 0x03, - 0x44, 0x0e, 0xdd, 0xa3, 0xea, 0x62, 0xc7, 0x99, 0xfe, 0x5b, 0x09, 0xd8, 0x38, 0x73, 0x82, 0xd3, 0xfd, 0x87, 0x97, - 0x2f, 0xd6, 0xf6, 0xa4, 0x5f, 0x32, 0xc3, 0xf8, 0x92, 0xba, 0x78, 0x70, 0x5f, 0xd3, 0xe2, 0x5b, 0xc2, 0xe4, 0xd3, - 0xfc, 0xf3, 0x49, 0xff, 0xea, 0x4b, 0xfe, 0xfc, 0xe8, 0x17, 0xbe, 0x95, 0xaf, 0x79, 0xf6, 0x4d, 0x5a, 0xa3, 0x1d, - 0xf6, 0x7a, 0x88, 0xbb, 0x37, 0xfd, 0xa1, 0x0e, 0xf9, 0x5a, 0xc5, 0xf8, 0xaf, 0x9e, 0xe9, 0xd3, 0x1f, 0x1e, 0x1f, - 0xdc, 0xa4, 0x77, 0x09, 0x39, 0xcd, 0x94, 0x57, 0xe7, 0xd6, 0xbe, 0xc1, 0x12, 0xb6, 0xf5, 0x26, 0xc1, 0xde, 0xa0, - 0x20, 0xd2, 0x48, 0xbb, 0x13, 0x21, 0x02, 0x95, 0x41, 0xae, 0x60, 0xc8, 0xcd, 0x71, 0xd4, 0xf0, 0x3f, 0x71, 0xc0, - 0x28, 0x97, 0x11, 0x55, 0xa5, 0x8a, 0xd3, 0xd1, 0xc1, 0x4c, 0xc0, 0x29, 0x44, 0x18, 0x21, 0xf9, 0x5e, 0xcd, 0x62, - 0x81, 0xce, 0x24, 0x0d, 0x3e, 0x7e, 0x27, 0x1d, 0x4b, 0x56, 0x5c, 0x5b, 0xe6, 0xeb, 0xfd, 0x27, 0xd9, 0x58, 0xf9, - 0x28, 0x90, 0x59, 0x79, 0x87, 0x02, 0xd5, 0x21, 0x05, 0x93, 0x8b, 0xd4, 0xf9, 0x88, 0x99, 0xf3, 0x91, 0x4a, 0x2f, - 0xd8, 0xaf, 0xe6, 0x06, 0xda, 0x8d, 0x3d, 0x1c, 0xec, 0x5b, 0x65, 0x6c, 0xc2, 0x90, 0xe4, 0x26, 0xbf, 0x46, 0x06, - 0xe5, 0xe4, 0xa6, 0x0d, 0x5b, 0xe0, 0x9b, 0x5f, 0x9f, 0xa1, 0x49, 0x0a, 0x9d, 0x8d, 0x7c, 0xcf, 0xc8, 0x83, 0xeb, - 0xfb, 0xb3, 0xd7, 0xfe, 0xd1, 0x94, 0x45, 0x13, 0xd6, 0x6e, 0xa9, 0x7d, 0x42, 0x28, 0x05, 0x2a, 0x08, 0x10, 0xa6, - 0xc2, 0x1a, 0x58, 0xd6, 0x21, 0x35, 0x87, 0x9a, 0xae, 0x3f, 0x67, 0x90, 0x23, 0xb5, 0xc3, 0xc4, 0xbe, 0x0d, 0x03, - 0x5f, 0x2b, 0xa5, 0xb7, 0x37, 0x50, 0xa5, 0x16, 0xf6, 0x59, 0x64, 0xa8, 0x33, 0x39, 0x57, 0x1c, 0x81, 0xd7, 0x2d, - 0x35, 0x33, 0x51, 0xe8, 0x2c, 0x1b, 0x69, 0x7e, 0x4a, 0x78, 0x45, 0x7f, 0x55, 0x04, 0x4c, 0x74, 0xd0, 0x99, 0xdc, - 0x9a, 0x8a, 0x02, 0x93, 0x90, 0xaa, 0xba, 0x62, 0xeb, 0x78, 0x0a, 0x84, 0x9f, 0xa7, 0x88, 0xed, 0x1a, 0x9f, 0x87, - 0xa2, 0x3c, 0xc9, 0xfb, 0x34, 0x77, 0x7d, 0xe8, 0x9c, 0x6b, 0x03, 0x91, 0x6c, 0x46, 0x74, 0xe1, 0x87, 0xd7, 0x54, - 0xa7, 0xc5, 0x6d, 0x4b, 0xf7, 0x69, 0x5e, 0x7c, 0xd2, 0xac, 0x4b, 0x2e, 0x7e, 0xf4, 0x97, 0x6c, 0x9b, 0x65, 0x08, - 0x45, 0x2a, 0x53, 0xf0, 0x6a, 0x9f, 0x2f, 0x8a, 0xc9, 0xf6, 0x7b, 0x58, 0xf2, 0xc4, 0x97, 0x41, 0x83, 0x89, 0x7e, - 0x71, 0xe7, 0x11, 0x1c, 0xaf, 0xba, 0xc8, 0x6a, 0x0e, 0x9c, 0xeb, 0x7a, 0x36, 0xe6, 0xb2, 0x35, 0x2e, 0x34, 0x42, - 0xa2, 0xae, 0x1a, 0x79, 0xd9, 0xbb, 0x80, 0x0c, 0x23, 0x29, 0x7b, 0x20, 0xc0, 0x9c, 0x5f, 0x5b, 0x46, 0xc3, 0xb3, - 0x90, 0x7c, 0xdd, 0x74, 0xba, 0xa0, 0x21, 0x54, 0x40, 0x83, 0x9f, 0xbf, 0x97, 0xd0, 0x9e, 0x0a, 0x7b, 0x7d, 0xfa, - 0x0b, 0xcf, 0x4c, 0x5a, 0x51, 0xc6, 0x33, 0x7d, 0x16, 0x4b, 0x9a, 0x27, 0x9d, 0xb1, 0x25, 0xcf, 0xfb, 0x58, 0xbe, - 0x4f, 0xe5, 0x58, 0xee, 0xee, 0x69, 0xba, 0xe4, 0x24, 0x35, 0xc7, 0x4a, 0x67, 0x42, 0x6d, 0x7c, 0x99, 0x4f, 0x22, - 0x12, 0x37, 0x78, 0x8a, 0x81, 0x58, 0xcf, 0x7d, 0x3a, 0x18, 0x4e, 0x15, 0xcd, 0xb7, 0xa7, 0xbb, 0x55, 0xe9, 0x9b, - 0x4d, 0xb5, 0x08, 0x71, 0x79, 0xc8, 0x62, 0xe2, 0xc3, 0x40, 0xd9, 0xd9, 0xa6, 0x8d, 0x9b, 0x04, 0x0f, 0xa4, 0xce, - 0xe5, 0xf4, 0x60, 0xb8, 0x88, 0xbd, 0xce, 0x3c, 0xa4, 0x57, 0x5c, 0xdc, 0x05, 0xe2, 0xbc, 0x42, 0x38, 0xa8, 0x57, - 0x8c, 0x6b, 0xf9, 0xa6, 0xd9, 0xbf, 0x9c, 0x4a, 0xe2, 0x92, 0x87, 0x6b, 0xd0, 0x4a, 0x35, 0x6b, 0x9d, 0x62, 0xab, - 0xa3, 0xf5, 0xf0, 0xdf, 0x37, 0x88, 0xac, 0xd8, 0x7c, 0xe1, 0x5b, 0xf9, 0xca, 0x76, 0x41, 0xc8, 0xec, 0x2f, 0xc7, - 0x17, 0x68, 0x3f, 0xcb, 0xd6, 0xda, 0x8b, 0xd3, 0xee, 0x74, 0xe3, 0x2e, 0xaf, 0x0f, 0xdb, 0x60, 0x7c, 0x85, 0x0e, - 0xdb, 0x05, 0x99, 0x7e, 0x62, 0xbd, 0xbe, 0xa7, 0x12, 0xfe, 0xe1, 0xfa, 0x87, 0xdf, 0xf4, 0xb9, 0x3f, 0xe6, 0x2a, - 0xe2, 0x00, 0x99, 0x97, 0xd4, 0x86, 0x71, 0xcd, 0x62, 0x2f, 0xe8, 0x56, 0x42, 0x7d, 0x6e, 0x9f, 0x01, 0x07, 0x37, - 0x37, 0xbd, 0xa7, 0x56, 0x03, 0x80, 0x45, 0x1c, 0x5d, 0xc3, 0x8e, 0x27, 0xe0, 0x13, 0x4a, 0x05, 0x61, 0x8f, 0x63, - 0x54, 0x29, 0x5d, 0xaa, 0x47, 0x1d, 0x3f, 0x0f, 0xa3, 0x3a, 0x10, 0x20, 0xe0, 0xf1, 0x98, 0xc7, 0x82, 0x44, 0x0d, - 0xea, 0x3c, 0x9a, 0xf2, 0x0a, 0x3e, 0x44, 0x02, 0xf6, 0x5d, 0xaf, 0xef, 0xc6, 0x37, 0xc3, 0x2b, 0x02, 0x5b, 0xf8, - 0x25, 0x8d, 0x6c, 0x23, 0x34, 0x8a, 0x47, 0xb9, 0x75, 0x4d, 0xf4, 0x45, 0x6d, 0xc7, 0xcc, 0x0b, 0x41, 0x56, 0x4f, - 0x78, 0x06, 0x0b, 0xe5, 0x82, 0xe0, 0x0b, 0xab, 0x80, 0xfb, 0x73, 0xa2, 0x1f, 0x83, 0x94, 0x1e, 0x8a, 0xe8, 0x88, - 0xd6, 0x91, 0xa9, 0xc1, 0x71, 0x8f, 0x65, 0x89, 0xe1, 0x3c, 0x42, 0xb0, 0xdb, 0x96, 0x35, 0x22, 0xab, 0xd5, 0x08, - 0x7e, 0xf3, 0x52, 0xd1, 0x3a, 0xa4, 0x24, 0x85, 0x0a, 0xd6, 0xd4, 0xf4, 0x5a, 0x10, 0xa9, 0x45, 0xe7, 0x7f, 0x02, - 0xc4, 0x69, 0x4f, 0x34, 0xad, 0xf6, 0x9c, 0x5a, 0x54, 0x1c, 0xda, 0x46, 0xc2, 0xdc, 0xa5, 0xc0, 0x95, 0x38, 0x70, - 0x00, 0xb1, 0xf4, 0xae, 0x48, 0xe4, 0x3d, 0xb4, 0x3f, 0xb8, 0x42, 0x9a, 0x4e, 0x8d, 0x77, 0x72, 0xca, 0x0d, 0x52, - 0x75, 0x61, 0xe4, 0x34, 0x12, 0x93, 0x2a, 0x27, 0x8c, 0x50, 0xc5, 0xed, 0x5a, 0x2d, 0xe1, 0xd4, 0x1b, 0xb7, 0x03, - 0x4f, 0x01, 0xef, 0x92, 0x21, 0x6c, 0xaf, 0x35, 0xe2, 0xcc, 0x18, 0xba, 0x7c, 0xf3, 0x9f, 0xba, 0x9d, 0x53, 0xfb, - 0x65, 0x70, 0x45, 0x87, 0x81, 0xaf, 0xc6, 0xab, 0x30, 0x79, 0x4a, 0x61, 0x5a, 0xfd, 0xa5, 0xeb, 0x33, 0x18, 0xf2, - 0x27, 0xf9, 0x4c, 0x43, 0x22, 0x48, 0xf1, 0x36, 0x7c, 0x78, 0x3f, 0xda, 0x06, 0xe4, 0x21, 0x70, 0x98, 0x8f, 0xc1, - 0xef, 0x44, 0xf6, 0x41, 0x6b, 0x44, 0x77, 0x8a, 0xb0, 0x20, 0x35, 0x77, 0xf8, 0xe8, 0x90, 0x6f, 0x1e, 0xea, 0x91, - 0x5c, 0x5e, 0x83, 0x00, 0x0a, 0x56, 0xd3, 0xc2, 0x9e, 0x3e, 0xb7, 0x79, 0xc6, 0x7b, 0xd0, 0x44, 0x47, 0xe1, 0x10, - 0x13, 0x3c, 0xe7, 0x0c, 0xed, 0x68, 0x27, 0x87, 0xe1, 0x31, 0xf4, 0x4a, 0x61, 0xee, 0x3f, 0x23, 0x72, 0xc3, 0xf9, - 0xb9, 0x9e, 0x31, 0x8d, 0x72, 0x9e, 0xb2, 0xaf, 0x57, 0x8d, 0x1e, 0xff, 0xb1, 0x03, 0x70, 0xff, 0xf4, 0xd7, 0x84, - 0xe4, 0x4f, 0x75, 0x0a, 0xdf, 0x57, 0x96, 0x84, 0xb7, 0x02, 0xff, 0x06, 0xaf, 0x59, 0x62, 0x70, 0x98, 0x82, 0x42, - 0xf9, 0x6b, 0x0b, 0x42, 0x6e, 0x73, 0x72, 0x6d, 0x0e, 0x97, 0xcf, 0x99, 0xe4, 0x0b, 0xb6, 0x09, 0xb5, 0x3e, 0x2b, - 0x70, 0xf0, 0xa6, 0xc9, 0x72, 0x3a, 0x8e, 0x9c, 0xf9, 0xad, 0xd8, 0x5c, 0x37, 0x26, 0x79, 0x14, 0x29, 0xfa, 0xcd, - 0xf4, 0x46, 0xde, 0x78, 0xb3, 0x10, 0x6d, 0x87, 0x5e, 0x9a, 0xd6, 0x8f, 0x2f, 0x08, 0x3f, 0x0d, 0xcb, 0x89, 0xd9, - 0x1f, 0x7c, 0x2f, 0xb0, 0xba, 0xc4, 0xc5, 0x80, 0x0c, 0xc3, 0xee, 0x58, 0xb0, 0x0e, 0x57, 0xd7, 0x68, 0xca, 0xb8, - 0x1c, 0xa4, 0x8a, 0x96, 0xee, 0x08, 0xa1, 0x9b, 0xb8, 0x28, 0xed, 0x4c, 0xd9, 0x7b, 0xf9, 0x3b, 0xb4, 0xfa, 0xb5, - 0x2a, 0xde, 0x5d, 0x12, 0x3e, 0xf8, 0xee, 0x5d, 0xd0, 0xdf, 0x74, 0xc8, 0xc6, 0xba, 0x5f, 0x3e, 0xbe, 0x54, 0x4d, - 0x16, 0x46, 0x83, 0x99, 0x4f, 0x79, 0x73, 0x76, 0x57, 0x65, 0x94, 0xd4, 0x35, 0x14, 0x46, 0x62, 0x8f, 0x1c, 0xe7, - 0xbd, 0x33, 0x59, 0xd7, 0xbb, 0x8e, 0x55, 0xe9, 0xf2, 0xb3, 0x04, 0x8b, 0xd6, 0x72, 0xef, 0xfe, 0x2c, 0xd5, 0xa7, - 0x50, 0x03, 0x69, 0xb3, 0x81, 0x0e, 0xdd, 0x46, 0x9b, 0x68, 0x9c, 0x49, 0xa0, 0xb4, 0x87, 0x2b, 0x2f, 0x6a, 0xfa, - 0x2c, 0x26, 0xd0, 0xba, 0x9d, 0x2d, 0x74, 0xb6, 0x0b, 0x4a, 0x83, 0xdb, 0x3f, 0xee, 0x76, 0xe9, 0xcc, 0xe0, 0xe3, - 0xfd, 0x83, 0x0c, 0xcb, 0xff, 0x1b, 0x55, 0xec, 0x9e, 0x1c, 0x80, 0x86, 0x35, 0x6f, 0x9b, 0x44, 0x44, 0x48, 0x58, - 0xdc, 0x7c, 0x72, 0xec, 0xfb, 0xc6, 0x97, 0xe8, 0xb9, 0xa1, 0x27, 0xe3, 0xc4, 0xf5, 0x52, 0x9d, 0xb2, 0x1e, 0x89, - 0x01, 0x7f, 0xd2, 0x39, 0x90, 0x68, 0x6b, 0x9a, 0xdd, 0x0e, 0xca, 0x81, 0xdd, 0x9b, 0x03, 0xeb, 0x8f, 0xf9, 0x06, - 0x23, 0x07, 0x2b, 0x9b, 0x3f, 0xb5, 0xb9, 0xed, 0xb4, 0x0e, 0x9f, 0x4d, 0xc6, 0xd2, 0xe3, 0xe1, 0x2b, 0xab, 0x23, - 0xb4, 0x35, 0x92, 0x15, 0x83, 0x6a, 0x6f, 0xf7, 0x63, 0x0f, 0x22, 0x7e, 0xa6, 0xee, 0xde, 0x45, 0xdd, 0xa1, 0xa5, - 0x67, 0xf6, 0xf6, 0xe0, 0xb1, 0x7f, 0x60, 0x8d, 0x43, 0xdd, 0xcb, 0x05, 0x08, 0x4b, 0xdc, 0x51, 0xd6, 0x56, 0x71, - 0x71, 0xfb, 0xe7, 0xd7, 0x0f, 0x9a, 0x83, 0x40, 0xe5, 0x70, 0x30, 0xd1, 0x8b, 0x11, 0xeb, 0xc8, 0xb1, 0x63, 0x18, - 0x23, 0x76, 0x73, 0x80, 0x94, 0x11, 0x23, 0xcd, 0x29, 0xdf, 0x07, 0x63, 0x5c, 0xf4, 0x46, 0xed, 0xc2, 0x86, 0x79, - 0x80, 0x15, 0xee, 0xa4, 0xaa, 0xc3, 0xc2, 0xc4, 0xfc, 0xba, 0xb5, 0x49, 0x72, 0xde, 0x91, 0xf5, 0xa9, 0xd9, 0xbb, - 0x12, 0x84, 0x3e, 0x1f, 0xfe, 0x8d, 0x8a, 0x78, 0xae, 0xb3, 0x87, 0x60, 0x02, 0x7e, 0xac, 0x3a, 0xec, 0x6f, 0xc1, - 0xa7, 0x0d, 0x27, 0xea, 0xe8, 0x93, 0xd1, 0x59, 0xe1, 0x80, 0x5d, 0x6b, 0xfa, 0x50, 0xc6, 0x43, 0x8f, 0x59, 0x18, - 0x2b, 0xd3, 0x5b, 0x15, 0x94, 0x0d, 0x9b, 0xa9, 0x2e, 0xa9, 0x06, 0xaa, 0x4c, 0x26, 0x99, 0x4c, 0xd9, 0x42, 0xce, - 0x00, 0xb6, 0xf7, 0x41, 0x72, 0x85, 0x88, 0x7a, 0x5f, 0x5a, 0x8f, 0xcc, 0x22, 0xae, 0x91, 0x23, 0xda, 0x63, 0x50, - 0x8b, 0x88, 0x77, 0x6a, 0x75, 0x94, 0xe4, 0xa3, 0x2f, 0x1f, 0x82, 0xd0, 0xb5, 0xa4, 0x3f, 0x9b, 0xa1, 0x84, 0x65, - 0x46, 0x2e, 0xdb, 0x4f, 0xdc, 0xbd, 0x3f, 0x8d, 0x7f, 0x9a, 0x08, 0x6d, 0x97, 0x67, 0xeb, 0xc1, 0xc8, 0xb5, 0x34, - 0x95, 0xd7, 0xb8, 0xa5, 0xc6, 0xb8, 0xe0, 0xa7, 0x38, 0xd2, 0xe6, 0x6b, 0xcd, 0xd3, 0x43, 0xdd, 0x7a, 0x1e, 0xb5, - 0x0f, 0xb2, 0xb6, 0x0e, 0xec, 0xc5, 0x42, 0x7b, 0x0a, 0x7b, 0xe7, 0xf8, 0xd0, 0xfd, 0xc5, 0xad, 0xcb, 0x4d, 0x95, - 0x8f, 0xce, 0x5c, 0x48, 0x64, 0x8e, 0x8a, 0xb7, 0x38, 0xc8, 0x07, 0xa0, 0x22, 0x92, 0xe1, 0xbd, 0x5b, 0x1e, 0x36, - 0xcf, 0xba, 0x47, 0x3d, 0xf6, 0xa0, 0x8c, 0x84, 0x8f, 0x77, 0x08, 0x89, 0x52, 0x21, 0xf6, 0xfc, 0x67, 0x92, 0x72, - 0x16, 0x0d, 0x95, 0xb7, 0x65, 0xe5, 0xf4, 0xf5, 0x3c, 0x92, 0x6a, 0x19, 0x0f, 0x78, 0x4f, 0x6e, 0xb6, 0x96, 0x13, - 0xc5, 0xad, 0xbe, 0xda, 0x5c, 0x82, 0xa0, 0x6c, 0xf4, 0x86, 0xdb, 0xb7, 0x11, 0x3b, 0x4e, 0xa0, 0x6d, 0xdb, 0x9f, - 0x5c, 0x2c, 0x45, 0xa9, 0x70, 0xc2, 0x58, 0x37, 0x39, 0x8a, 0xe6, 0x10, 0x86, 0x37, 0x6b, 0xab, 0x09, 0x1f, 0x70, - 0xc3, 0x31, 0x6f, 0x6f, 0x29, 0x87, 0x55, 0x2d, 0x9c, 0xa3, 0x48, 0xc6, 0xc4, 0xde, 0x2e, 0xa3, 0xdb, 0x5b, 0x85, - 0xfe, 0x13, 0xb2, 0xeb, 0xac, 0x56, 0xde, 0x04, 0x5f, 0x29, 0x88, 0x6c, 0xee, 0xc7, 0x67, 0xc6, 0x01, 0xd2, 0x0d, - 0xf0, 0xd7, 0x0a, 0x92, 0x55, 0x9e, 0xa8, 0xbc, 0x0a, 0x4c, 0xd3, 0x90, 0x82, 0xe1, 0x53, 0x7a, 0x0f, 0x96, 0xbc, - 0xe6, 0xcb, 0x66, 0xd7, 0x37, 0x17, 0x3f, 0xac, 0xf5, 0x10, 0x2f, 0x3b, 0xbd, 0xb5, 0x2a, 0x9c, 0xe0, 0x31, 0x49, - 0xfc, 0xba, 0xf4, 0xb3, 0xfd, 0x60, 0xe3, 0x96, 0x42, 0xed, 0x07, 0x9c, 0xd9, 0xba, 0xe7, 0x30, 0xb3, 0x49, 0x9f, - 0x01, 0x12, 0x16, 0x68, 0xdd, 0xc7, 0x22, 0x53, 0x60, 0xab, 0x01, 0x6e, 0x00, 0x23, 0xb6, 0x7d, 0xc8, 0x1e, 0xbd, - 0x29, 0x92, 0x2d, 0xe4, 0x7b, 0x3a, 0x72, 0xfb, 0x53, 0x4c, 0xef, 0x17, 0x75, 0x20, 0x9a, 0xaf, 0x03, 0x6e, 0xeb, - 0x81, 0x77, 0x1c, 0xa4, 0x48, 0x5c, 0x21, 0xa6, 0x49, 0xf7, 0x15, 0x5a, 0xb5, 0xba, 0x9b, 0x5c, 0xf6, 0xe7, 0x8e, - 0x93, 0xb5, 0xde, 0x86, 0xbb, 0xd8, 0xcf, 0xaa, 0x1d, 0xd2, 0x51, 0x03, 0xf8, 0xd2, 0xaf, 0x0c, 0x74, 0x7a, 0x9a, - 0xc2, 0x77, 0x25, 0x96, 0x4d, 0x08, 0x98, 0x3b, 0x28, 0xec, 0x2c, 0x90, 0x04, 0x2b, 0x9c, 0x38, 0x96, 0x77, 0x58, - 0x93, 0x17, 0xfa, 0x7a, 0x1c, 0x19, 0x18, 0x98, 0xb2, 0x27, 0x11, 0x61, 0xef, 0x2c, 0x52, 0x34, 0x6b, 0x19, 0xde, - 0x32, 0xd1, 0x93, 0x0f}; + 0x5b, 0x7b, 0x7b, 0x53, 0xc1, 0x6e, 0x19, 0x03, 0xf5, 0x04, 0xe0, 0xf8, 0xbb, 0x25, 0x3d, 0x34, 0x51, 0x94, 0xb1, + 0xe6, 0x22, 0x2f, 0x61, 0xbb, 0xc2, 0x70, 0x9e, 0x80, 0x65, 0x6b, 0x78, 0xad, 0x47, 0x01, 0x40, 0x55, 0x13, 0x0e, + 0xd8, 0x18, 0x92, 0x41, 0xc7, 0x81, 0x6a, 0xd1, 0xee, 0xdb, 0xf0, 0xa6, 0x22, 0xc8, 0x31, 0x97, 0xd9, 0x3c, 0xcc, + 0xe5, 0xa4, 0xd5, 0x13, 0x8c, 0x3a, 0x44, 0xf9, 0x1a, 0x8c, 0x4b, 0x4c, 0x51, 0x7e, 0x10, 0x4b, 0xea, 0xba, 0xe6, + 0x24, 0x45, 0x6e, 0xf3, 0x72, 0x99, 0x36, 0xac, 0xdb, 0x79, 0x37, 0x0a, 0x4f, 0x92, 0xc8, 0x21, 0xb3, 0xaa, 0x10, + 0x8e, 0x77, 0x8e, 0xb5, 0x29, 0xe5, 0xbf, 0x10, 0x83, 0x6d, 0xc3, 0xba, 0x2d, 0x5d, 0x36, 0x49, 0x12, 0x9a, 0x5b, + 0x46, 0xa5, 0x0a, 0x42, 0x62, 0xf0, 0x33, 0xd7, 0x94, 0x37, 0x3f, 0x57, 0x6b, 0x9c, 0x30, 0x61, 0xbd, 0xf1, 0x5b, + 0x28, 0xf2, 0xda, 0x5a, 0xe3, 0x0b, 0x44, 0xe0, 0x2e, 0xa4, 0xbe, 0x68, 0xed, 0xa7, 0x5b, 0x5f, 0x57, 0x34, 0xde, + 0x43, 0xba, 0xff, 0x8d, 0xbf, 0x8b, 0x43, 0x72, 0x99, 0xdb, 0x59, 0x36, 0xac, 0xdb, 0xf2, 0xa6, 0xee, 0x77, 0xca, + 0x1c, 0x5e, 0x9c, 0x1b, 0x62, 0x68, 0x1d, 0x10, 0x8f, 0xcd, 0xa1, 0x4a, 0x44, 0x8b, 0x11, 0x2b, 0xf6, 0xda, 0x13, + 0xf3, 0x5f, 0x65, 0xcd, 0x7a, 0x7d, 0x47, 0xb5, 0x8c, 0x9c, 0x1d, 0x4d, 0x37, 0x55, 0x02, 0x7c, 0x97, 0xc6, 0xd7, + 0x09, 0x1e, 0xf0, 0xb1, 0x63, 0x19, 0x43, 0x51, 0x9d, 0xdd, 0x4a, 0x10, 0x59, 0x4d, 0x65, 0x8a, 0x4b, 0xf2, 0xff, + 0xa6, 0xaa, 0xe7, 0xf8, 0x72, 0xa2, 0xbf, 0xfa, 0x1c, 0xb2, 0x16, 0x10, 0x7c, 0x80, 0xe0, 0x90, 0xca, 0x4c, 0xc9, + 0x59, 0xb2, 0xbb, 0x24, 0x27, 0x5b, 0x06, 0x49, 0x70, 0xa4, 0x1c, 0x29, 0x01, 0x6a, 0x44, 0xce, 0xfd, 0x92, 0x7d, + 0x55, 0xed, 0xa7, 0x3d, 0x4f, 0xd7, 0xa6, 0xdf, 0xb6, 0xdf, 0xdd, 0x7b, 0xe2, 0x85, 0x47, 0x51, 0x90, 0x08, 0x99, + 0x06, 0x14, 0x02, 0x6a, 0xf6, 0xb5, 0xb7, 0xb4, 0xff, 0xaf, 0xdf, 0x91, 0x10, 0xe0, 0x4a, 0x70, 0x16, 0x75, 0xe6, + 0x2d, 0x5b, 0xcf, 0x85, 0xb3, 0x2c, 0x5b, 0x5f, 0xc3, 0x61, 0x58, 0x47, 0x5d, 0x35, 0xa1, 0xc9, 0x7e, 0x24, 0x15, + 0xbb, 0x23, 0xd8, 0xcf, 0x7d, 0x96, 0x7d, 0x7d, 0x2f, 0x5a, 0x3f, 0xd7, 0x0a, 0xcf, 0x78, 0x8a, 0x0b, 0xb1, 0xfe, + 0x2e, 0xa4, 0x84, 0xe9, 0x5a, 0x35, 0xa7, 0x16, 0xc8, 0xc0, 0x38, 0x3e, 0xfb, 0x6c, 0x5f, 0x55, 0xa7, 0x6b, 0x47, + 0x9a, 0x25, 0x26, 0x47, 0xae, 0x17, 0x3d, 0x73, 0x14, 0x38, 0x9a, 0xfd, 0x5e, 0x2e, 0x1a, 0x1d, 0xe8, 0x0c, 0xe5, + 0x04, 0xb6, 0x31, 0x50, 0x0b, 0x44, 0x55, 0xb7, 0x19, 0xb2, 0xea, 0x7d, 0xf5, 0xfd, 0xaf, 0x5f, 0x72, 0xa3, 0x40, + 0x3d, 0xc6, 0x2c, 0x69, 0x29, 0xef, 0x81, 0x47, 0x88, 0x2c, 0x47, 0xc7, 0x8a, 0x1f, 0xf2, 0x95, 0x74, 0x1e, 0x2e, + 0x86, 0x45, 0x43, 0xc4, 0x92, 0x42, 0x0e, 0xb4, 0xe0, 0xc5, 0x2e, 0x2d, 0xd0, 0xc4, 0x06, 0xfe, 0xbf, 0xaa, 0x59, + 0xfd, 0xbe, 0x37, 0x2b, 0x09, 0x4b, 0x21, 0x2e, 0x71, 0x82, 0x40, 0xdd, 0xf3, 0x7b, 0x38, 0xde, 0xf3, 0x9f, 0x87, + 0xec, 0x30, 0x05, 0xad, 0xa2, 0x32, 0xc9, 0x41, 0xa4, 0x01, 0x35, 0x7a, 0x5c, 0x4b, 0xd3, 0xca, 0xd7, 0x57, 0x10, + 0xb0, 0x03, 0x97, 0xa6, 0xd8, 0xd3, 0x0c, 0x4d, 0x51, 0x24, 0x6c, 0x3a, 0xa5, 0xb7, 0xb3, 0x2e, 0x6b, 0x2f, 0x27, + 0xf3, 0xd0, 0xa9, 0x4d, 0xbb, 0x87, 0x49, 0x14, 0xd1, 0x36, 0x97, 0xb4, 0x86, 0x49, 0xc1, 0xdb, 0x49, 0x77, 0xc2, + 0x8a, 0x51, 0x79, 0x24, 0x4c, 0xf8, 0x87, 0x87, 0xe4, 0x03, 0x6a, 0xf5, 0x0d, 0xff, 0x69, 0x6a, 0xf6, 0xfa, 0xc6, + 0x0b, 0x3d, 0xe4, 0x54, 0x2e, 0xf2, 0x76, 0x80, 0xac, 0xee, 0xba, 0xb5, 0x69, 0x4e, 0x72, 0x9d, 0x98, 0x61, 0x93, + 0x02, 0xb6, 0x1b, 0x0e, 0x25, 0xd2, 0x46, 0x2c, 0x2d, 0xd5, 0xd7, 0x3b, 0x79, 0x1d, 0x25, 0x4a, 0x86, 0xf2, 0x0a, + 0x16, 0xd9, 0xb4, 0x5f, 0x29, 0x6d, 0xe0, 0xdb, 0xf8, 0xc6, 0x85, 0x03, 0x50, 0x4b, 0xf7, 0x84, 0x48, 0xea, 0xa0, + 0x10, 0x15, 0x28, 0x6c, 0xb0, 0xfc, 0xff, 0xbd, 0x95, 0x96, 0xdb, 0x1f, 0x91, 0xae, 0x08, 0x91, 0x3d, 0x00, 0x39, + 0xce, 0x70, 0x64, 0xfd, 0xbe, 0x33, 0xb3, 0x0a, 0x9c, 0x06, 0x48, 0xf6, 0x38, 0xb3, 0x92, 0xd6, 0x5a, 0x6c, 0x2a, + 0xee, 0xbd, 0xef, 0x5d, 0xe6, 0x77, 0xd1, 0x19, 0x3f, 0x0c, 0x2b, 0x4c, 0x66, 0x23, 0xed, 0xb0, 0x6c, 0x57, 0x96, + 0xd3, 0x00, 0x20, 0x78, 0xdf, 0x7b, 0x3f, 0x0a, 0xff, 0xff, 0xc8, 0xe2, 0xfc, 0x88, 0x2c, 0x50, 0x91, 0x59, 0xc5, + 0x39, 0x99, 0x05, 0xcc, 0xa8, 0x0a, 0xe0, 0x8c, 0x0a, 0x60, 0x1f, 0x1d, 0x90, 0x63, 0x41, 0xd0, 0x68, 0x9a, 0x6c, + 0xf6, 0xd1, 0x90, 0x2d, 0xef, 0x57, 0x5a, 0xac, 0x20, 0xca, 0xf5, 0x8c, 0x6c, 0x4b, 0x7e, 0xb7, 0x03, 0x65, 0xcd, + 0x52, 0x5a, 0xe9, 0xe8, 0xff, 0xe6, 0xd4, 0x66, 0x40, 0x8e, 0x40, 0x75, 0x53, 0x57, 0x21, 0xdc, 0xf2, 0xff, 0x5d, + 0xf2, 0x7a, 0x97, 0x52, 0xd2, 0xd1, 0xa5, 0x78, 0x19, 0x26, 0x1d, 0x95, 0x60, 0xe0, 0x2a, 0x27, 0xa7, 0xe7, 0x66, + 0x2c, 0xb2, 0x71, 0x53, 0x7f, 0x0c, 0xbf, 0x87, 0xd2, 0xc2, 0x60, 0xea, 0x4c, 0xd3, 0xf4, 0xdf, 0x6d, 0xa2, 0xab, + 0xae, 0x2c, 0x2c, 0xbf, 0xed, 0x66, 0x12, 0x57, 0x59, 0x96, 0x25, 0xb9, 0x24, 0x31, 0x01, 0xfe, 0x21, 0xba, 0xef, + 0x6f, 0xa8, 0xcf, 0x1b, 0x4b, 0xdb, 0x0c, 0x42, 0x26, 0x04, 0x48, 0xdb, 0xff, 0x37, 0x99, 0xeb, 0x9c, 0xb8, 0x78, + 0x7c, 0x49, 0xd3, 0x34, 0xb3, 0x6c, 0x7f, 0xae, 0xa3, 0xb4, 0x94, 0x6e, 0x9b, 0xed, 0x36, 0x7d, 0xa4, 0x0e, 0xdf, + 0xc0, 0x6f, 0x0c, 0x18, 0xc3, 0xe4, 0x6e, 0x15, 0x53, 0x43, 0x76, 0x69, 0xb6, 0xa5, 0xcd, 0x02, 0xd4, 0xb2, 0xfe, + 0x87, 0xa4, 0xdc, 0x5b, 0x42, 0x27, 0xf6, 0x34, 0xac, 0x62, 0x92, 0x7c, 0x83, 0x6f, 0x4c, 0xdf, 0x5a, 0x48, 0x2c, + 0x43, 0xd3, 0xda, 0xfe, 0x65, 0xb6, 0xf9, 0x75, 0x18, 0xb3, 0xcc, 0x14, 0x5b, 0x92, 0x63, 0x5b, 0x09, 0x76, 0xef, + 0x8a, 0x4c, 0xac, 0x3d, 0x0e, 0x00, 0xa7, 0xe5, 0x88, 0xb6, 0xe0, 0x33, 0x10, 0x8e, 0x73, 0x17, 0xbf, 0xfc, 0x55, + 0x09, 0xa6, 0xa3, 0x3d, 0x14, 0x7c, 0x75, 0x8c, 0x42, 0x2c, 0x41, 0x14, 0x79, 0xee, 0xe2, 0xde, 0x07, 0x26, 0xdf, + 0x0e, 0xaa, 0xe8, 0x1f, 0xba, 0xa6, 0x27, 0x9c, 0x21, 0x50, 0x8f, 0x5e, 0xf2, 0x0b, 0x07, 0xde, 0xfd, 0x7b, 0x00, + 0xa2, 0x12, 0x51, 0xea, 0xdb, 0x6f, 0xb8, 0x4e, 0x30, 0x7d, 0xdf, 0x4d, 0xdb, 0x03, 0xee, 0x0e, 0x1e, 0x12, 0x78, + 0x52, 0x0a, 0xcb, 0xfd, 0x97, 0xaa, 0x2b, 0x6e, 0x96, 0xa1, 0xd7, 0x31, 0x9d, 0xef, 0x26, 0xd8, 0x14, 0x2d, 0x6b, + 0x29, 0x18, 0x7a, 0xe6, 0xf1, 0xd6, 0x58, 0xfd, 0x0c, 0x56, 0xc9, 0xc0, 0x2d, 0x2d, 0xcc, 0xe4, 0xd4, 0xcf, 0xd7, + 0x54, 0xf5, 0xc1, 0x48, 0x12, 0x01, 0x90, 0xbc, 0xf9, 0x10, 0x27, 0x44, 0xe2, 0xfa, 0x7a, 0x3e, 0x5f, 0x95, 0x97, + 0xd9, 0x7e, 0x98, 0x60, 0x20, 0xd9, 0x20, 0x03, 0x98, 0xed, 0x3d, 0x5c, 0x7d, 0xb8, 0x57, 0xf3, 0x32, 0x6a, 0xfa, + 0xd7, 0x79, 0xb4, 0xa1, 0x33, 0x6d, 0x40, 0x1e, 0xb7, 0x69, 0x59, 0x9a, 0x92, 0x82, 0xc4, 0x86, 0x43, 0x06, 0x77, + 0x83, 0x39, 0xad, 0xc7, 0xa4, 0xe6, 0x9c, 0xac, 0xc9, 0x15, 0x97, 0x06, 0x37, 0xeb, 0xa3, 0x0f, 0xf7, 0xbe, 0xa4, + 0xc3, 0x2d, 0x3e, 0x6c, 0xfa, 0x24, 0x93, 0x7b, 0xde, 0x84, 0xcf, 0x4d, 0xb9, 0xbe, 0x1c, 0x02, 0x7b, 0xf3, 0x13, + 0x76, 0x85, 0xa0, 0x59, 0xdf, 0xea, 0xc8, 0x37, 0xde, 0xb5, 0xeb, 0xa1, 0x44, 0x32, 0x1a, 0x7d, 0xef, 0x41, 0xf3, + 0xa2, 0xdc, 0x88, 0x47, 0xd8, 0x2b, 0xd4, 0xb7, 0x3f, 0xb1, 0xc2, 0xb2, 0xbb, 0x99, 0x3f, 0x6c, 0x74, 0x7b, 0xf6, + 0xdd, 0xcb, 0xc1, 0xa3, 0x2f, 0xc2, 0x5c, 0x7d, 0xb8, 0xbf, 0xdd, 0x3a, 0xc1, 0x63, 0x42, 0x29, 0x76, 0x43, 0xc2, + 0xa1, 0xe6, 0xf7, 0x6e, 0xf6, 0x6e, 0xf2, 0x73, 0x59, 0x8b, 0x59, 0x4d, 0xfe, 0x93, 0xdf, 0xfe, 0xea, 0x77, 0x6a, + 0x9b, 0x8f, 0xf0, 0xed, 0x09, 0xc2, 0xd3, 0xbb, 0xa3, 0x8c, 0xb0, 0xe6, 0x30, 0xfe, 0xac, 0xa7, 0xca, 0x3f, 0xdb, + 0x2c, 0x6c, 0x73, 0x98, 0xaf, 0x4b, 0xda, 0x9e, 0xc3, 0xa4, 0x75, 0x57, 0xa2, 0xde, 0x4d, 0x94, 0xf2, 0xa0, 0x09, + 0xf2, 0xf2, 0xb9, 0x03, 0x7d, 0xe3, 0x7c, 0xcd, 0xa0, 0xc8, 0x6e, 0xa9, 0xe5, 0xd2, 0x9a, 0xc7, 0x9b, 0xf9, 0x60, + 0x59, 0xa2, 0x40, 0xbf, 0x4a, 0xbd, 0x77, 0xad, 0xbb, 0x7e, 0x21, 0xaa, 0x1f, 0x6d, 0xe6, 0x6a, 0x04, 0x42, 0xa4, + 0x5c, 0x37, 0x01, 0x22, 0x4b, 0xa4, 0xc8, 0x33, 0xf1, 0x5c, 0xa7, 0x4d, 0x86, 0x1e, 0xb9, 0xbf, 0xf2, 0x6b, 0xa4, + 0xe1, 0xf9, 0x84, 0x76, 0xf8, 0xd1, 0x66, 0x25, 0xd4, 0x2b, 0x54, 0xc8, 0x1b, 0x67, 0xc5, 0x7f, 0xee, 0x43, 0xa9, + 0xd6, 0x44, 0x0c, 0xcf, 0xcd, 0x64, 0x90, 0xf7, 0x2c, 0xbb, 0x92, 0xea, 0x58, 0x5b, 0x5b, 0x55, 0xd7, 0xb7, 0x50, + 0xde, 0xcc, 0x50, 0xee, 0x45, 0x95, 0x22, 0xf9, 0x60, 0x18, 0xd2, 0x73, 0xfc, 0xdb, 0xd6, 0x37, 0x3f, 0x15, 0x88, + 0x73, 0x91, 0x37, 0x28, 0x75, 0x43, 0x4d, 0x96, 0x12, 0xfb, 0x59, 0x9d, 0xb2, 0xdd, 0x23, 0xed, 0xa0, 0x23, 0x57, + 0x03, 0x98, 0xc2, 0x54, 0xb0, 0xe7, 0xd5, 0x4b, 0x56, 0x9d, 0xe7, 0x05, 0xf9, 0xb6, 0xe2, 0x47, 0x04, 0x40, 0xa3, + 0x78, 0x43, 0x34, 0x2b, 0xa0, 0x2a, 0x91, 0x26, 0x0b, 0xc7, 0x4e, 0xf3, 0x4f, 0x68, 0x43, 0xcd, 0x7e, 0xbf, 0xed, + 0x64, 0x50, 0xc2, 0xc5, 0x37, 0x9f, 0x7d, 0xa0, 0x09, 0x7e, 0xfb, 0x99, 0x8c, 0xac, 0x95, 0xa0, 0xa3, 0x9c, 0xbc, + 0xee, 0x40, 0xca, 0x2c, 0x53, 0x61, 0xa1, 0x8b, 0xa4, 0x84, 0x6e, 0x98, 0x9c, 0x2f, 0x78, 0x7b, 0x83, 0xf3, 0xb5, + 0xac, 0x56, 0x5a, 0xbe, 0x99, 0xaa, 0x85, 0x79, 0x07, 0x54, 0x7d, 0xec, 0x04, 0x9e, 0xd0, 0x6d, 0x32, 0xef, 0x96, + 0xd2, 0x23, 0x5a, 0xf9, 0xde, 0x4b, 0x91, 0x66, 0xb7, 0xfe, 0x84, 0xe8, 0xd5, 0x11, 0x81, 0xfb, 0x22, 0xd9, 0x8a, + 0xbe, 0x37, 0x8c, 0x88, 0xe2, 0xfe, 0x1e, 0xfd, 0x12, 0x3f, 0xcb, 0xaf, 0x5d, 0x21, 0x34, 0x56, 0xc0, 0x23, 0x69, + 0x7d, 0xef, 0x6a, 0x8f, 0x21, 0x80, 0x4e, 0xaf, 0x42, 0x31, 0xec, 0xb6, 0x22, 0x66, 0xc7, 0x99, 0x38, 0xee, 0xf3, + 0x19, 0x96, 0xf9, 0xda, 0x34, 0xa1, 0x1b, 0xaa, 0x4f, 0x71, 0x21, 0x65, 0x92, 0x36, 0x45, 0x55, 0x17, 0x8d, 0xbf, + 0x35, 0xc8, 0x98, 0x62, 0xde, 0x7a, 0x34, 0xe8, 0x2f, 0xf6, 0x05, 0xf1, 0xe0, 0x48, 0xd0, 0xcb, 0x74, 0xf4, 0xc6, + 0xb1, 0x6a, 0x2c, 0x6f, 0x2c, 0x3b, 0x30, 0x13, 0x36, 0x09, 0xd1, 0xd8, 0x60, 0xeb, 0xc8, 0x82, 0x55, 0xcf, 0x18, + 0x9b, 0x77, 0xe9, 0x2d, 0x12, 0xde, 0x95, 0x2d, 0x1c, 0xa6, 0xfa, 0x42, 0xc6, 0x59, 0x2f, 0xd7, 0xf2, 0xe9, 0x3a, + 0x01, 0x0e, 0x12, 0x86, 0x17, 0xc4, 0x18, 0xfe, 0xe2, 0xbc, 0x49, 0x55, 0xb0, 0x28, 0xb4, 0x6d, 0x7c, 0x51, 0x7b, + 0x10, 0xcf, 0x4a, 0x10, 0xdf, 0xca, 0xb8, 0xea, 0xa0, 0x1b, 0x8e, 0x30, 0x57, 0xc3, 0x26, 0x84, 0x56, 0x10, 0x81, + 0x9a, 0xfa, 0x33, 0x0d, 0xd5, 0xb5, 0xae, 0xf2, 0x42, 0xa2, 0xe4, 0xb3, 0x68, 0x1c, 0x41, 0x21, 0x07, 0x83, 0xc2, + 0x09, 0x3d, 0xd8, 0x3d, 0xf8, 0x8d, 0x83, 0x71, 0xc1, 0x71, 0x43, 0xfe, 0xda, 0x2d, 0x6b, 0xdc, 0x33, 0x30, 0x95, + 0x97, 0x2b, 0xcd, 0xe6, 0x00, 0x2a, 0x83, 0x5d, 0x6c, 0x48, 0x3e, 0x5b, 0xf4, 0x84, 0xbe, 0xbb, 0xa1, 0x01, 0x0c, + 0x0f, 0x8f, 0xbc, 0x99, 0x7f, 0x23, 0x01, 0x0f, 0x0e, 0x66, 0xe5, 0x97, 0x0b, 0xa1, 0x58, 0x7d, 0x11, 0x20, 0x40, + 0x7c, 0x8d, 0xee, 0x07, 0x41, 0x74, 0x84, 0x60, 0x45, 0x1d, 0x0b, 0xe0, 0x44, 0xc5, 0x29, 0x39, 0x22, 0xc0, 0x38, + 0x41, 0x7d, 0x13, 0x34, 0xf3, 0x7b, 0xa3, 0xfc, 0x0b, 0xb7, 0x9b, 0x79, 0xe2, 0x59, 0x3f, 0x9b, 0xd7, 0x8b, 0x24, + 0x4f, 0xe0, 0x51, 0xd3, 0x81, 0x12, 0x85, 0xd2, 0x0d, 0xee, 0xa6, 0x14, 0x71, 0x9a, 0x88, 0xd5, 0x42, 0x00, 0xb6, + 0xb5, 0xb2, 0x96, 0x7e, 0xa3, 0x74, 0x8e, 0x3a, 0x87, 0x3d, 0x0b, 0xbe, 0x50, 0x7e, 0x6f, 0x09, 0x5d, 0xd5, 0x68, + 0x3b, 0x97, 0x9a, 0x1f, 0xae, 0x36, 0xb2, 0xa1, 0x75, 0xcd, 0xde, 0x42, 0x50, 0x53, 0x54, 0x86, 0x9a, 0xf2, 0x22, + 0x19, 0xdb, 0x9d, 0x98, 0xfd, 0xd0, 0x48, 0xf2, 0x1a, 0x79, 0x65, 0x7f, 0x43, 0x3b, 0xd3, 0x26, 0x1e, 0xbb, 0x12, + 0x0c, 0xbf, 0x68, 0x29, 0x7d, 0x2d, 0x9c, 0x5d, 0xcb, 0xcf, 0x97, 0xb0, 0x36, 0xa6, 0x00, 0x82, 0x90, 0x7e, 0x36, + 0xda, 0xaa, 0x31, 0xba, 0xd5, 0x63, 0xca, 0x3e, 0xea, 0x31, 0xdf, 0xfd, 0x1e, 0xa9, 0x92, 0x85, 0x20, 0x39, 0x34, + 0xf4, 0xd7, 0x63, 0x64, 0x18, 0xa0, 0x48, 0x22, 0xe4, 0x5b, 0x29, 0x03, 0xf7, 0xef, 0x57, 0x8c, 0x0e, 0xb6, 0xd4, + 0x9c, 0x49, 0xb3, 0xab, 0x67, 0x34, 0x20, 0x6c, 0xb4, 0x1e, 0x26, 0xce, 0x08, 0xe1, 0xa4, 0xb1, 0x7d, 0xaa, 0x22, + 0x12, 0xe9, 0xbd, 0x14, 0x31, 0xd8, 0xb8, 0x52, 0xba, 0xc4, 0x08, 0x6b, 0x66, 0x2c, 0xc7, 0x06, 0x50, 0x39, 0x73, + 0x5b, 0x94, 0xc6, 0x37, 0xad, 0xa0, 0x04, 0xb8, 0x47, 0x0c, 0xf6, 0x41, 0x23, 0x40, 0xae, 0x0b, 0x2a, 0x48, 0x68, + 0x9f, 0x0b, 0xc8, 0x84, 0x06, 0x19, 0x19, 0x13, 0xeb, 0x46, 0x20, 0xb9, 0x7b, 0x7a, 0xd3, 0x2e, 0x01, 0xa6, 0x72, + 0xb2, 0x9a, 0x21, 0x62, 0xe2, 0x78, 0x5d, 0x2d, 0x9c, 0xc0, 0x58, 0x0a, 0xd8, 0x31, 0x76, 0x54, 0x72, 0x2e, 0x76, + 0x68, 0xb4, 0x69, 0xe6, 0x17, 0xba, 0x3e, 0x43, 0xe1, 0x87, 0xb5, 0x0b, 0xc8, 0xc8, 0xa9, 0xdb, 0x4b, 0x0f, 0x46, + 0x06, 0x12, 0x57, 0xeb, 0x4e, 0x8b, 0xa4, 0x15, 0x91, 0xcf, 0x8a, 0x7e, 0x75, 0x6c, 0x72, 0x25, 0x2e, 0xd6, 0x8a, + 0x1a, 0x43, 0x91, 0x07, 0xb7, 0xc1, 0x3f, 0x76, 0xf4, 0xb8, 0x75, 0xc2, 0x02, 0x80, 0xf5, 0x58, 0x4e, 0x06, 0x9c, + 0xab, 0xee, 0xe0, 0xd7, 0x40, 0x95, 0xc0, 0x2b, 0x47, 0x9d, 0x45, 0x1c, 0x5f, 0x58, 0xa0, 0x18, 0xfc, 0xeb, 0x14, + 0x79, 0x0c, 0x76, 0x83, 0x2c, 0xe9, 0xa6, 0x59, 0x04, 0x7b, 0x4a, 0x79, 0x26, 0x62, 0xfe, 0xaa, 0x91, 0x34, 0x2a, + 0xac, 0x78, 0x9a, 0x6a, 0xa9, 0x13, 0x3e, 0x55, 0x09, 0x05, 0xc2, 0x1e, 0x82, 0xa6, 0x00, 0xde, 0x9b, 0x12, 0xf3, + 0xf8, 0xa6, 0x85, 0xc4, 0xf9, 0xc9, 0x3a, 0x9b, 0x35, 0x63, 0x06, 0xba, 0x92, 0x80, 0x6e, 0x4e, 0x35, 0x0d, 0xb7, + 0xe8, 0xba, 0x2c, 0x85, 0xa5, 0x64, 0x85, 0x5a, 0x82, 0x89, 0x30, 0x19, 0xde, 0x06, 0x17, 0x90, 0xbc, 0x37, 0x69, + 0x66, 0xdc, 0x3c, 0xbd, 0xaa, 0xf2, 0x04, 0x9a, 0xc7, 0x7d, 0x99, 0x2f, 0x34, 0xa5, 0xb9, 0xc2, 0x01, 0x48, 0x7b, + 0xc1, 0x3c, 0x16, 0x1a, 0x67, 0x52, 0x32, 0xfd, 0x8e, 0x8b, 0x99, 0xd4, 0x54, 0x71, 0x17, 0xd6, 0x09, 0x2b, 0x40, + 0x22, 0x59, 0x32, 0x18, 0x3c, 0x03, 0x8a, 0xf7, 0x05, 0xe0, 0x88, 0x68, 0x14, 0xbe, 0xb3, 0xa3, 0x1c, 0xad, 0x4a, + 0x42, 0x88, 0xcc, 0x56, 0xec, 0xbc, 0x78, 0xa3, 0x1c, 0x45, 0xce, 0x38, 0xda, 0x01, 0x6c, 0x5e, 0x7f, 0xc8, 0x7c, + 0x06, 0x81, 0xac, 0x1f, 0x27, 0x3a, 0x9b, 0x9b, 0xa6, 0x4b, 0x91, 0xce, 0x46, 0x73, 0x96, 0x17, 0x78, 0xc6, 0x29, + 0x13, 0x3c, 0x96, 0x8d, 0xe2, 0x86, 0xa8, 0xf3, 0x4f, 0xd4, 0x01, 0xa7, 0xda, 0x66, 0x7b, 0x33, 0x58, 0x3d, 0x2d, + 0x4e, 0x0e, 0x18, 0x95, 0x9c, 0xcd, 0xa3, 0xd5, 0xeb, 0xfd, 0x5f, 0x4e, 0xbe, 0x2a, 0x63, 0x81, 0xc6, 0xab, 0x9c, + 0xaa, 0xc8, 0xc8, 0x74, 0xc0, 0x89, 0x97, 0x9a, 0xcf, 0xc5, 0x00, 0x2d, 0x32, 0xaf, 0x4a, 0x32, 0x14, 0x92, 0xd5, + 0xb0, 0xf2, 0x06, 0x1a, 0x64, 0xd3, 0xd5, 0x50, 0xa3, 0xe0, 0x08, 0x59, 0xd2, 0x62, 0x63, 0xb6, 0x58, 0xac, 0x79, + 0xad, 0x99, 0x36, 0xc7, 0x08, 0x22, 0xb0, 0x38, 0x20, 0xae, 0x3f, 0xab, 0x35, 0x36, 0x30, 0x89, 0x57, 0xbb, 0x11, + 0x06, 0xdd, 0x0d, 0x2d, 0xa9, 0x4e, 0x8c, 0xa5, 0x12, 0x44, 0x4e, 0x1d, 0x69, 0xec, 0x47, 0x9e, 0xaf, 0xf9, 0xe3, + 0x9e, 0x05, 0xc6, 0xb2, 0x3c, 0x18, 0x19, 0xaa, 0x18, 0x52, 0xda, 0x0d, 0x66, 0x1e, 0xa2, 0x7e, 0x7a, 0xa4, 0x56, + 0xe5, 0x4c, 0xed, 0x31, 0x3c, 0x16, 0x9d, 0x4b, 0xf2, 0xe1, 0x51, 0x37, 0x04, 0x64, 0x5b, 0x81, 0xd5, 0xa7, 0x0e, + 0x22, 0x8a, 0x40, 0xd8, 0xcf, 0xe9, 0x1f, 0xbb, 0x91, 0xff, 0x14, 0xb0, 0x54, 0xaf, 0x87, 0xba, 0xa5, 0xcc, 0x31, + 0x25, 0x6b, 0x79, 0xcb, 0x29, 0xa8, 0xb8, 0x74, 0x55, 0x3a, 0x79, 0x20, 0xb6, 0x10, 0x29, 0x58, 0xcc, 0x3e, 0x9f, + 0x2e, 0x1c, 0xa8, 0xa4, 0x50, 0x7d, 0xdf, 0x05, 0x79, 0x7e, 0xb8, 0x71, 0x50, 0x8b, 0x31, 0xc6, 0x43, 0xaa, 0xad, + 0xaf, 0x1d, 0xdc, 0x8a, 0xbd, 0x0d, 0x3c, 0x3f, 0xb1, 0xdf, 0xef, 0xb7, 0x6c, 0x94, 0x91, 0x95, 0xc2, 0x8a, 0xdc, + 0xd4, 0xa2, 0xf3, 0xc3, 0x49, 0x38, 0x79, 0x42, 0x19, 0x90, 0x97, 0x33, 0xd8, 0x02, 0x59, 0x74, 0x53, 0xf4, 0xc2, + 0x68, 0xb3, 0xbe, 0xe5, 0xe2, 0x5b, 0xbf, 0xc3, 0x21, 0x93, 0x94, 0x2e, 0x69, 0x3a, 0xdf, 0xeb, 0x52, 0x7d, 0x17, + 0x59, 0xb4, 0x48, 0x67, 0xd5, 0xb6, 0xfb, 0x45, 0xaa, 0xb5, 0x17, 0x22, 0x59, 0x32, 0x1c, 0xc5, 0xde, 0xb9, 0x20, + 0xb5, 0x74, 0x06, 0xd5, 0xf4, 0x63, 0x32, 0x8e, 0x5d, 0x02, 0xad, 0x15, 0x4c, 0x6f, 0xe1, 0xc5, 0x20, 0xdb, 0x48, + 0x61, 0x91, 0x16, 0x82, 0x35, 0x9a, 0x39, 0xd2, 0x7e, 0x90, 0x28, 0x2c, 0x03, 0x74, 0x96, 0xf4, 0x39, 0xb3, 0x87, + 0xa3, 0x78, 0x84, 0x2a, 0xa2, 0xd4, 0xfd, 0x61, 0x42, 0x85, 0x54, 0xa7, 0x79, 0x82, 0xa2, 0x3d, 0x1f, 0xb9, 0x63, + 0x03, 0xe6, 0xa7, 0x33, 0xd1, 0xae, 0xbf, 0x5a, 0x02, 0x16, 0x5e, 0x7e, 0x48, 0x71, 0x9b, 0xd2, 0xdb, 0xf9, 0x1f, + 0xf3, 0x39, 0xa5, 0x3c, 0x33, 0x74, 0x4a, 0xa9, 0xd0, 0xcc, 0xe6, 0xc2, 0x0a, 0x49, 0xa5, 0xf9, 0x70, 0x67, 0x0d, + 0xba, 0x19, 0x82, 0x12, 0x09, 0xc5, 0x8d, 0x60, 0x16, 0xa3, 0x18, 0x6b, 0xa0, 0x72, 0x37, 0x6f, 0xd5, 0x49, 0xa5, + 0xb9, 0x53, 0x95, 0x5c, 0xf1, 0xdd, 0xcf, 0x8d, 0x82, 0x61, 0x08, 0xb1, 0xe9, 0xf8, 0x22, 0x25, 0xcb, 0x4b, 0x39, + 0xac, 0xc6, 0x95, 0x21, 0x82, 0x96, 0x41, 0x9c, 0x10, 0xac, 0xe4, 0x12, 0xd4, 0x56, 0x98, 0xee, 0x54, 0x89, 0x52, + 0x41, 0x1f, 0x28, 0xbd, 0xba, 0x83, 0xe6, 0xc4, 0x36, 0x84, 0xb7, 0xa6, 0xa1, 0x80, 0x98, 0xf5, 0xdf, 0x07, 0x19, + 0x1d, 0x3a, 0x7e, 0x2b, 0x19, 0x53, 0x21, 0x50, 0x33, 0x47, 0xcb, 0xcb, 0x80, 0x4d, 0x0a, 0x71, 0xa5, 0x28, 0x4e, + 0x04, 0x71, 0xd8, 0xc7, 0xa6, 0xe6, 0xd3, 0xc7, 0x41, 0x62, 0x1a, 0xd6, 0x65, 0x03, 0xa4, 0xd6, 0x0b, 0x91, 0xf8, + 0x35, 0xf5, 0xe6, 0xa0, 0x09, 0xe3, 0x75, 0xa8, 0x20, 0x66, 0xc5, 0x69, 0x23, 0x05, 0x63, 0x95, 0x86, 0x43, 0x50, + 0x8e, 0x0c, 0xcb, 0xc4, 0xfa, 0xd2, 0x2c, 0xd1, 0x1b, 0x26, 0x06, 0xb6, 0x63, 0xa5, 0x84, 0x00, 0x38, 0x33, 0x66, + 0xdd, 0x8e, 0x49, 0xab, 0x0a, 0xea, 0x81, 0xea, 0xb3, 0xbe, 0xe8, 0x78, 0x86, 0x98, 0xf0, 0x21, 0x01, 0x47, 0xf3, + 0x45, 0xd2, 0x73, 0x6b, 0xa2, 0x25, 0x6d, 0x6a, 0x88, 0x3f, 0x31, 0xd2, 0x5a, 0xb4, 0xd2, 0x81, 0xaf, 0x00, 0x54, + 0x90, 0xa9, 0xe0, 0x12, 0x55, 0x52, 0x36, 0x15, 0x95, 0x07, 0x93, 0x72, 0x6d, 0x99, 0x95, 0x55, 0xee, 0x5d, 0x1f, + 0xe1, 0xcf, 0xb4, 0x50, 0xd2, 0xba, 0x43, 0x7c, 0xa9, 0xe0, 0xaf, 0x51, 0x48, 0x11, 0xf5, 0x99, 0x91, 0x5d, 0x1d, + 0xf3, 0xec, 0x91, 0x95, 0xff, 0xda, 0xc6, 0xaf, 0x5d, 0x28, 0x31, 0xca, 0xdd, 0x7b, 0x64, 0x32, 0xb2, 0x85, 0x80, + 0xa8, 0xdb, 0xd8, 0x0f, 0x47, 0xea, 0xf8, 0xe3, 0x90, 0xe2, 0x3f, 0x5d, 0x05, 0x51, 0x7b, 0xd2, 0x42, 0xaa, 0x83, + 0x9e, 0x03, 0x6b, 0xd0, 0x9a, 0x34, 0x7a, 0xd0, 0xbd, 0x07, 0x2a, 0x57, 0x04, 0xe7, 0x8f, 0x6e, 0xc2, 0x44, 0x05, + 0x9e, 0x02, 0xfe, 0xc2, 0x14, 0x84, 0x59, 0x23, 0x50, 0xdd, 0x2e, 0xda, 0x3e, 0x6f, 0x33, 0x66, 0x90, 0xf7, 0x6e, + 0xdf, 0x08, 0x3f, 0xa2, 0x1e, 0x36, 0x5b, 0xfd, 0x9b, 0x6f, 0x79, 0x94, 0xa8, 0x2c, 0x84, 0xa9, 0x91, 0x50, 0x53, + 0x67, 0x49, 0xe0, 0x47, 0x37, 0xb1, 0x86, 0xd9, 0x7e, 0xb2, 0x56, 0xb8, 0x54, 0x08, 0x99, 0x22, 0x10, 0x9d, 0x21, + 0xcc, 0xa8, 0xf3, 0x44, 0x01, 0xbc, 0xad, 0x00, 0xb4, 0x04, 0xfd, 0x18, 0x6c, 0x73, 0xfb, 0x84, 0xd0, 0x5c, 0xcc, + 0xf3, 0x47, 0x4c, 0x42, 0x41, 0x8a, 0x9f, 0xe5, 0xd3, 0xac, 0x79, 0xa1, 0x12, 0x15, 0xd7, 0x50, 0xb4, 0x15, 0xd7, + 0xc1, 0x03, 0x63, 0xd6, 0x47, 0xd1, 0xa9, 0x8d, 0x29, 0xcd, 0xe2, 0x66, 0x97, 0x68, 0x47, 0xea, 0x6e, 0x3c, 0x9f, + 0x34, 0xdc, 0x89, 0x24, 0xa1, 0x2c, 0x43, 0xab, 0xdb, 0xa6, 0x4b, 0x71, 0x0a, 0xe7, 0x74, 0x5e, 0x7e, 0xcb, 0x10, + 0xef, 0xbf, 0xe6, 0xf8, 0xf4, 0x39, 0xab, 0x66, 0x9e, 0x1f, 0x3d, 0x12, 0x5a, 0x60, 0x66, 0x2d, 0x76, 0xf3, 0x28, + 0x8b, 0x35, 0x24, 0xb0, 0xc3, 0x86, 0xa1, 0x13, 0x5e, 0x6f, 0x59, 0x3c, 0x54, 0x5b, 0x0f, 0x36, 0xde, 0x53, 0x18, + 0xba, 0x5b, 0xd2, 0x46, 0xd6, 0x35, 0x51, 0xd1, 0xcf, 0x6f, 0x91, 0xcd, 0xdd, 0xfe, 0xb8, 0x4c, 0x9a, 0x14, 0x15, + 0xa7, 0xa3, 0xdd, 0xe9, 0xc5, 0xdf, 0x18, 0x33, 0xf3, 0x18, 0x39, 0x65, 0x32, 0xd6, 0xd6, 0x19, 0x6d, 0xb9, 0xb5, + 0x73, 0x95, 0xf5, 0x64, 0xe0, 0x77, 0x82, 0xe4, 0xe7, 0x47, 0x39, 0x68, 0x44, 0x20, 0xa8, 0xdd, 0xae, 0x51, 0xc8, + 0x86, 0x03, 0x33, 0xc7, 0x7f, 0x67, 0x6d, 0xfb, 0x3e, 0x79, 0x9b, 0xf5, 0x62, 0x76, 0xc0, 0xf5, 0xc6, 0x46, 0x73, + 0x64, 0x6e, 0x57, 0x23, 0x1b, 0x3a, 0xdc, 0x91, 0x50, 0xdf, 0x1c, 0x99, 0x67, 0xc7, 0x7c, 0x69, 0x44, 0x70, 0x36, + 0x3a, 0x02, 0xc3, 0x41, 0x9b, 0xeb, 0xbf, 0x49, 0xf2, 0x3d, 0x93, 0x18, 0xe0, 0x80, 0x0d, 0xb2, 0x7a, 0x27, 0x0b, + 0x42, 0xc5, 0x2d, 0x1d, 0xf0, 0x72, 0x9f, 0x07, 0x14, 0x6f, 0xa2, 0x52, 0x72, 0xbd, 0x39, 0x13, 0x15, 0x9a, 0xbb, + 0x7b, 0xe3, 0x6d, 0xab, 0xf1, 0x42, 0xfd, 0xfb, 0x7a, 0x97, 0xda, 0xdf, 0xfc, 0xb1, 0xe9, 0xbb, 0x2c, 0x3f, 0x6b, + 0x51, 0xa7, 0xe7, 0x22, 0xe3, 0x79, 0xd3, 0x2e, 0xd1, 0x1e, 0x46, 0xaa, 0xc9, 0x6a, 0x09, 0xcd, 0x8d, 0xd8, 0xe6, + 0x1d, 0xb7, 0x3a, 0xe0, 0x55, 0x98, 0x55, 0xcd, 0x89, 0x78, 0xef, 0x49, 0xe0, 0x7a, 0xea, 0xc9, 0xda, 0x84, 0xdb, + 0xa1, 0x57, 0x76, 0xb3, 0x33, 0x86, 0xce, 0xa7, 0xd5, 0x3f, 0xd9, 0x47, 0xf0, 0x9b, 0x93, 0xcd, 0xbf, 0x37, 0x35, + 0xa5, 0xc9, 0xdb, 0x93, 0x69, 0xd6, 0x93, 0xa7, 0x1b, 0xc3, 0xd9, 0x96, 0xf6, 0xd3, 0xe6, 0xc3, 0x6c, 0xda, 0x7f, + 0x29, 0xdf, 0x54, 0x85, 0x49, 0xa9, 0xff, 0x04, 0x96, 0x7a, 0x64, 0xa1, 0xf7, 0x1a, 0x43, 0xfc, 0xaa, 0x0a, 0x07, + 0x17, 0x35, 0xdd, 0x0f, 0xe3, 0xdd, 0x68, 0xe2, 0xd6, 0xe5, 0x65, 0x69, 0xce, 0x6a, 0xc4, 0x49, 0xee, 0x49, 0xab, + 0xeb, 0x5d, 0xe6, 0x39, 0xb4, 0xcb, 0xbf, 0x17, 0x08, 0xb7, 0x26, 0x28, 0x68, 0x5d, 0x6a, 0x9b, 0x75, 0x7e, 0x16, + 0x58, 0xfe, 0x1b, 0x59, 0x4f, 0xd7, 0x57, 0xb1, 0xeb, 0x97, 0x2a, 0x3f, 0xff, 0x14, 0xfe, 0x5e, 0xf4, 0xa4, 0xf9, + 0xeb, 0xc1, 0xd9, 0xe7, 0xf9, 0x9f, 0xa3, 0x4c, 0x9b, 0x5a, 0x75, 0xeb, 0x29, 0xfc, 0xf3, 0x78, 0x28, 0x66, 0xb3, + 0xf1, 0xd7, 0x56, 0xf3, 0x3b, 0x84, 0x57, 0xff, 0xf1, 0xe2, 0xe7, 0x2f, 0xcd, 0xc0, 0x7c, 0xe8, 0x9f, 0xe6, 0x6c, + 0xea, 0x62, 0xfd, 0x17, 0x29, 0xeb, 0xeb, 0x3b, 0x6f, 0x4c, 0x34, 0xac, 0xf8, 0xb6, 0xe9, 0xd6, 0x6c, 0x56, 0x47, + 0x95, 0x7f, 0xe1, 0xda, 0xbf, 0x3d, 0xf5, 0x19, 0x04, 0xf9, 0xbc, 0x93, 0x7a, 0xde, 0x18, 0xee, 0x96, 0x14, 0xf6, + 0xd9, 0x72, 0xef, 0xd7, 0x0b, 0x3f, 0x1e, 0x2f, 0xca, 0xd6, 0x51, 0x37, 0x59, 0x59, 0x35, 0xd7, 0x7e, 0xb1, 0x26, + 0x39, 0xdb, 0x15, 0x38, 0xff, 0xb4, 0x7c, 0x3c, 0xfe, 0xe7, 0xe4, 0x69, 0x5d, 0x8e, 0x66, 0x30, 0xe3, 0x3d, 0x9a, + 0x27, 0x9a, 0x37, 0x26, 0xd3, 0x66, 0xbf, 0xfd, 0x10, 0xdf, 0x9a, 0x6e, 0xdd, 0x9b, 0xaf, 0xf8, 0x01, 0x57, 0xcc, + 0xa9, 0xef, 0x5a, 0xf9, 0x5d, 0x4f, 0x0d, 0x71, 0xc1, 0xd8, 0x04, 0x12, 0x8f, 0xfd, 0xdf, 0xc1, 0xd8, 0x0f, 0xbd, + 0x97, 0xde, 0x6c, 0x11, 0xdf, 0x09, 0x61, 0xd7, 0xac, 0x94, 0x73, 0x91, 0x8e, 0xd8, 0xc6, 0x70, 0x9c, 0xf9, 0x40, + 0x45, 0xdb, 0x27, 0xef, 0x37, 0x3e, 0xea, 0x77, 0xa1, 0xf6, 0xe0, 0xfa, 0xe1, 0xf2, 0xb5, 0x78, 0xef, 0x9f, 0x09, + 0xe1, 0x65, 0x03, 0x62, 0x5e, 0xf1, 0xee, 0xf6, 0x3f, 0x46, 0xc5, 0x29, 0x14, 0x75, 0xa2, 0xb2, 0x66, 0xdb, 0x66, + 0xf6, 0x7d, 0xb4, 0x78, 0xe8, 0x40, 0xcd, 0xc7, 0xfb, 0x84, 0xc8, 0xa3, 0x8b, 0x74, 0x97, 0xef, 0xa7, 0x12, 0x08, + 0xec, 0x11, 0x05, 0x76, 0x0d, 0xf2, 0x69, 0xd7, 0x83, 0xbf, 0xc0, 0x9b, 0xb0, 0xb1, 0xb9, 0x7a, 0xb7, 0x73, 0xc8, + 0x1e, 0xae, 0xe6, 0xc5, 0xfa, 0x14, 0x40, 0x12, 0x9b, 0x84, 0x80, 0xf9, 0x3f, 0xb9, 0xa0, 0x86, 0xad, 0xd7, 0xe9, + 0xe7, 0x17, 0xa3, 0xee, 0xd4, 0xa4, 0x59, 0x47, 0x18, 0xe9, 0x5e, 0x78, 0xb5, 0xa1, 0x13, 0x1e, 0x72, 0x8c, 0xf5, + 0x8a, 0xd2, 0x4a, 0x0b, 0xee, 0xd4, 0x47, 0x9d, 0x95, 0x9f, 0x1b, 0x90, 0x88, 0x6c, 0x95, 0x0d, 0xee, 0x32, 0xb3, + 0xf7, 0x0a, 0x6e, 0x58, 0xa5, 0x36, 0x2f, 0xa1, 0xce, 0xf8, 0x3d, 0x57, 0x53, 0x6a, 0xeb, 0xab, 0x6e, 0xde, 0xc4, + 0xcf, 0xed, 0xc5, 0x42, 0x7a, 0xc3, 0x2e, 0xad, 0x0d, 0x48, 0xe0, 0xea, 0x1b, 0xda, 0xed, 0xd4, 0x68, 0xc4, 0x40, + 0x3e, 0x4e, 0x82, 0xf3, 0x5c, 0x33, 0x53, 0x18, 0xef, 0x1a, 0x6a, 0x65, 0xd1, 0x2d, 0xc7, 0x45, 0x93, 0xb7, 0xed, + 0xff, 0x5d, 0x46, 0x8e, 0xeb, 0xe1, 0x0c, 0xe0, 0x36, 0x0f, 0xa0, 0x9f, 0x55, 0x17, 0x56, 0xb9, 0x99, 0x3f, 0xdd, + 0x1a, 0x0c, 0x2a, 0x1f, 0x7a, 0x98, 0x72, 0xfc, 0x46, 0x4e, 0xff, 0x11, 0xb0, 0x6b, 0xeb, 0xb9, 0x96, 0x7b, 0xde, + 0xec, 0xc5, 0x7b, 0x33, 0x9d, 0xcd, 0x4c, 0x99, 0xf5, 0x58, 0xc5, 0x94, 0x13, 0x5f, 0xdb, 0xd5, 0xb7, 0x8b, 0xef, + 0xf6, 0xe1, 0x93, 0x0d, 0x4e, 0x01, 0x2b, 0x68, 0x28, 0x88, 0x83, 0xca, 0xcf, 0xf7, 0x6f, 0xee, 0x98, 0xdc, 0x06, + 0xc9, 0xf4, 0x6a, 0xe1, 0x3a, 0x9e, 0xf4, 0x17, 0x16, 0xfd, 0x59, 0x2b, 0xa1, 0xbf, 0x00, 0xc3, 0x07, 0x6e, 0xcf, + 0x99, 0xe9, 0xdc, 0xc5, 0xa2, 0x7c, 0x9a, 0x71, 0x2b, 0xb9, 0x9b, 0x33, 0x6a, 0x4d, 0x73, 0x00, 0x90, 0x16, 0x4a, + 0x8d, 0x3f, 0x51, 0xd5, 0xa1, 0xa4, 0xc6, 0xb7, 0x91, 0x2a, 0x74, 0x74, 0x59, 0xe4, 0xa7, 0x8b, 0x2b, 0x62, 0xe1, + 0x75, 0x70, 0x7b, 0x8a, 0xe1, 0x8b, 0xef, 0x99, 0xd3, 0xf2, 0x83, 0xca, 0x37, 0x85, 0xe2, 0x6c, 0xd8, 0x18, 0x79, + 0xeb, 0xfe, 0xd4, 0x12, 0x34, 0x7c, 0x8b, 0xde, 0x9a, 0x57, 0xff, 0xed, 0x69, 0x15, 0xa0, 0x5b, 0x1c, 0xe1, 0xe9, + 0x0e, 0x8a, 0x66, 0xee, 0xa9, 0x78, 0x51, 0x06, 0xca, 0xe4, 0x75, 0x3f, 0xe3, 0x45, 0x70, 0x52, 0x68, 0x9f, 0xd7, + 0xcf, 0xeb, 0x05, 0x55, 0x33, 0xc9, 0xe9, 0xed, 0xc2, 0xbc, 0xe1, 0xfb, 0xeb, 0x16, 0x5f, 0x67, 0x29, 0x53, 0xb1, + 0x1d, 0x94, 0xd1, 0x6f, 0x0b, 0x60, 0xbe, 0xfa, 0x92, 0x89, 0x05, 0x9d, 0xac, 0xb9, 0x59, 0xcd, 0xf7, 0xb6, 0x67, + 0x7e, 0x8f, 0xe0, 0xe5, 0x5b, 0xa5, 0x68, 0xeb, 0x67, 0x95, 0x67, 0x2d, 0x30, 0x77, 0x08, 0x31, 0x37, 0x91, 0x06, + 0x8b, 0x02, 0xf2, 0xdd, 0xcd, 0x4b, 0xcb, 0x28, 0xf3, 0x68, 0xde, 0xfc, 0xb3, 0x5e, 0x50, 0x07, 0xa4, 0x17, 0xdf, + 0xb9, 0xdc, 0x40, 0x42, 0x8b, 0x7b, 0xa1, 0xc6, 0xdb, 0xe8, 0x71, 0xca, 0xac, 0x3c, 0x40, 0xb2, 0x86, 0x0e, 0x5a, + 0xdd, 0x87, 0x74, 0x5c, 0x1c, 0x5f, 0xa3, 0xe9, 0xfb, 0x26, 0xde, 0x4e, 0x74, 0x35, 0x79, 0x4f, 0xd9, 0x6d, 0x96, + 0x81, 0x12, 0xcb, 0xcb, 0x0b, 0x79, 0x27, 0xac, 0xa5, 0xa4, 0xb9, 0x0e, 0x13, 0x67, 0x83, 0xfa, 0xeb, 0x99, 0x97, + 0x5e, 0xd6, 0x3e, 0xe0, 0x1b, 0x85, 0x0a, 0xee, 0xe7, 0x09, 0x35, 0x82, 0xfd, 0x20, 0x45, 0xea, 0x41, 0x9d, 0x30, + 0xa3, 0x06, 0x23, 0x69, 0xba, 0xb4, 0x14, 0x67, 0x4e, 0xfa, 0x8b, 0x8a, 0xd2, 0x85, 0x5d, 0xbe, 0xad, 0x62, 0xa9, + 0x4e, 0x6f, 0x63, 0xa4, 0x3c, 0x6a, 0xde, 0x9b, 0xb7, 0x45, 0xde, 0x4e, 0x1b, 0x12, 0x22, 0xe9, 0x05, 0x72, 0xd9, + 0x3a, 0x3f, 0x84, 0xee, 0xe7, 0x2c, 0x1e, 0xc1, 0xa6, 0x84, 0x51, 0x90, 0xeb, 0x32, 0xd7, 0x7b, 0x43, 0x03, 0x13, + 0xf2, 0x63, 0x7e, 0x96, 0x80, 0xa5, 0x6d, 0xdd, 0x7a, 0x67, 0x7c, 0x68, 0x99, 0x43, 0xef, 0x96, 0x00, 0x32, 0xb7, + 0x6b, 0xf6, 0xae, 0xd6, 0x39, 0x99, 0x38, 0x24, 0x35, 0xa0, 0xef, 0x19, 0x75, 0xfa, 0xc6, 0x32, 0xb1, 0x48, 0xa4, + 0xa6, 0x37, 0x89, 0x95, 0xe6, 0xb1, 0xa7, 0xaf, 0x4e, 0x3d, 0x03, 0xbe, 0x36, 0xf7, 0x9a, 0xdd, 0xc7, 0x06, 0xec, + 0xb0, 0xd0, 0xc6, 0xee, 0x02, 0xe6, 0xf2, 0xa6, 0xdd, 0x4e, 0xa8, 0x3c, 0xba, 0x71, 0xcc, 0x0d, 0xc1, 0x40, 0x7a, + 0x11, 0x8d, 0xa2, 0xfc, 0xbe, 0xea, 0x49, 0xec, 0x75, 0x07, 0x76, 0xb7, 0xfb, 0xb3, 0x23, 0x55, 0xb8, 0xbd, 0x4c, + 0x06, 0x7e, 0xb2, 0x3d, 0x23, 0x79, 0x68, 0x2a, 0xf6, 0x80, 0x26, 0x1b, 0xde, 0x05, 0xe2, 0x86, 0xf1, 0xbe, 0x0f, + 0xfb, 0xfb, 0x92, 0xaf, 0x09, 0xa8, 0x71, 0x20, 0x41, 0xb5, 0x64, 0xcf, 0x5f, 0xae, 0xcc, 0xe6, 0x32, 0xcc, 0x26, + 0x5e, 0xb9, 0xa8, 0xf3, 0xfe, 0xe9, 0xb5, 0x83, 0xfb, 0x2d, 0x35, 0x94, 0x9b, 0xf1, 0xcc, 0xff, 0x47, 0x4d, 0x61, + 0x43, 0xe0, 0x01, 0x59, 0x69, 0x21, 0xb9, 0xb2, 0xc0, 0xa7, 0x6f, 0x0e, 0x75, 0x3e, 0x8c, 0xe7, 0x2d, 0x66, 0x65, + 0x46, 0xe4, 0x62, 0x7c, 0x80, 0x48, 0x36, 0x50, 0x0c, 0x13, 0x2e, 0x60, 0xf4, 0xd1, 0x65, 0xda, 0xa2, 0x79, 0x20, + 0xed, 0xca, 0xd6, 0x1f, 0xcf, 0x0c, 0xbc, 0x92, 0xff, 0xc6, 0x79, 0x5c, 0x86, 0x39, 0xbe, 0xd2, 0xd8, 0x9e, 0x92, + 0xe7, 0xc2, 0x15, 0x19, 0xe5, 0xa1, 0xaa, 0x3c, 0xe9, 0x9c, 0xbb, 0xbb, 0x7a, 0x32, 0x1d, 0xd9, 0x0c, 0x60, 0x6e, + 0x69, 0xda, 0xd8, 0xb1, 0x52, 0x5d, 0xf2, 0x10, 0x6f, 0x30, 0x18, 0xec, 0xcb, 0xd6, 0xad, 0x3f, 0xdb, 0x28, 0x68, + 0xb8, 0x42, 0x10, 0x58, 0x82, 0x81, 0xab, 0x92, 0x04, 0xe9, 0x0f, 0x45, 0xde, 0xb9, 0x29, 0x79, 0x4f, 0x3d, 0xb9, + 0x78, 0x25, 0x79, 0x70, 0x68, 0x09, 0x70, 0xd1, 0x7f, 0xd6, 0x5a, 0xc9, 0xda, 0x52, 0xbe, 0x3b, 0xce, 0x3e, 0x76, + 0xba, 0x99, 0x05, 0xd9, 0xd2, 0x87, 0x51, 0x6c, 0xcf, 0xbd, 0x1e, 0xe6, 0xa1, 0x25, 0xb0, 0x90, 0xb9, 0x59, 0xda, + 0x01, 0xf1, 0x2d, 0x9a, 0xd4, 0x66, 0xc9, 0xff, 0xc4, 0x2d, 0x6f, 0x20, 0x44, 0xd4, 0xb6, 0xbe, 0x6b, 0x68, 0x74, + 0x12, 0x27, 0xb9, 0x41, 0xde, 0x7f, 0x53, 0x0a, 0x28, 0x50, 0xb6, 0x54, 0x76, 0x92, 0xdf, 0x7f, 0xe2, 0x21, 0x84, + 0x66, 0x36, 0x5e, 0x5a, 0xb5, 0x6e, 0x33, 0x6b, 0x09, 0xa7, 0x91, 0x30, 0xb3, 0x9b, 0x83, 0xae, 0x2a, 0x12, 0x8e, + 0x92, 0x34, 0xa6, 0x48, 0x47, 0x38, 0xdc, 0x69, 0xbe, 0xbb, 0x93, 0xba, 0x63, 0x01, 0x6b, 0x9b, 0x39, 0x6e, 0x01, + 0x02, 0x8c, 0xfa, 0x5d, 0x03, 0xd1, 0x44, 0x93, 0x53, 0xa8, 0xe5, 0x8d, 0xdc, 0xd5, 0xa3, 0x5b, 0xf3, 0x58, 0x83, + 0xf6, 0x59, 0xfd, 0x29, 0x21, 0xe0, 0xb6, 0xa2, 0xde, 0x93, 0x81, 0x15, 0xa9, 0x0b, 0xc1, 0x35, 0x10, 0x58, 0xef, + 0x8c, 0xd6, 0x3e, 0x35, 0x26, 0xd2, 0xfe, 0xa2, 0xc1, 0x05, 0x24, 0x04, 0x02, 0x98, 0x97, 0x65, 0xb3, 0x84, 0x4f, + 0x22, 0x39, 0x80, 0xaa, 0xc7, 0xa5, 0xb7, 0x5a, 0x4a, 0x44, 0xc3, 0xa3, 0x1a, 0x01, 0xd7, 0xed, 0x02, 0xe5, 0x03, + 0x46, 0x58, 0x39, 0x85, 0x79, 0x26, 0xa4, 0x6a, 0x52, 0x8c, 0xba, 0x99, 0x4d, 0xa4, 0x3c, 0x33, 0xce, 0x53, 0x49, + 0xd4, 0x69, 0xfd, 0x6b, 0xe5, 0x4b, 0x1b, 0x44, 0xdb, 0xf0, 0xd9, 0x70, 0x7d, 0xac, 0xb9, 0x1e, 0x6d, 0x06, 0xa6, + 0xb5, 0xab, 0x59, 0x04, 0x88, 0x7a, 0x2a, 0xbb, 0xab, 0xcf, 0x5c, 0x90, 0x87, 0x1a, 0x3f, 0xf2, 0xe2, 0x14, 0xec, + 0x4a, 0x3f, 0xbf, 0x69, 0x28, 0x40, 0x18, 0x2f, 0x1d, 0xf1, 0x92, 0x55, 0x5e, 0x6c, 0x8a, 0x36, 0xee, 0x30, 0xf6, + 0x7a, 0xb4, 0x02, 0x52, 0x8f, 0x4d, 0xdd, 0x49, 0x96, 0xac, 0x8b, 0x73, 0xca, 0xab, 0xb8, 0x67, 0xba, 0x34, 0x7d, + 0x4c, 0xfd, 0x87, 0x4a, 0xe7, 0xc4, 0x0a, 0xe1, 0x7f, 0x4b, 0xca, 0xce, 0x2a, 0x65, 0x5a, 0x90, 0x88, 0xb5, 0x20, + 0x0a, 0x9c, 0xef, 0x04, 0xc9, 0xc2, 0xb2, 0x88, 0x24, 0x4f, 0x63, 0x79, 0xad, 0x4b, 0xf0, 0x24, 0x7b, 0xa0, 0xc8, + 0x87, 0x5d, 0xd9, 0x25, 0xc1, 0xdc, 0xf3, 0x83, 0xb4, 0x61, 0xa2, 0xb0, 0x0f, 0x5a, 0xf2, 0xb8, 0x66, 0x01, 0x38, + 0x3d, 0xf4, 0x6b, 0xef, 0xf9, 0xd8, 0x36, 0x7e, 0x8b, 0xe0, 0x5d, 0x4e, 0x84, 0xfb, 0x39, 0x97, 0x04, 0xcb, 0xaf, + 0xae, 0x53, 0x66, 0xb1, 0x5a, 0x83, 0x8a, 0x97, 0x3b, 0xbc, 0x6d, 0xdd, 0x5f, 0x96, 0xf0, 0xbe, 0x93, 0xcd, 0x70, + 0x37, 0x1d, 0x91, 0xcd, 0xc4, 0x39, 0x92, 0x8a, 0x44, 0x5c, 0x75, 0x32, 0x8d, 0xc5, 0x87, 0x39, 0x01, 0x04, 0x93, + 0xfa, 0x37, 0x2a, 0x84, 0x36, 0x24, 0x74, 0x7c, 0xec, 0xf2, 0xb5, 0x61, 0xed, 0xd6, 0xd7, 0xca, 0xd6, 0xbe, 0x75, + 0x23, 0x8a, 0x0a, 0xed, 0x58, 0x2c, 0x86, 0x64, 0x8c, 0x5e, 0xe9, 0x37, 0xd6, 0x34, 0xc9, 0xe2, 0xe1, 0xab, 0xdb, + 0x68, 0x31, 0x0e, 0x62, 0x17, 0x78, 0xfb, 0xd1, 0xec, 0x6d, 0x2d, 0x29, 0x7e, 0xff, 0xea, 0x8c, 0xa2, 0x56, 0xfc, + 0x43, 0xe9, 0xcf, 0xba, 0xc0, 0x25, 0x2a, 0x03, 0x2d, 0x66, 0xf8, 0x83, 0x48, 0xab, 0x57, 0xc8, 0xb9, 0xcf, 0xb9, + 0x3e, 0x24, 0xff, 0xc5, 0x03, 0x6f, 0x28, 0x8b, 0x42, 0xa8, 0xeb, 0x11, 0x37, 0x52, 0xc4, 0x62, 0xdd, 0x7d, 0x79, + 0xd0, 0x16, 0x39, 0x0b, 0x66, 0xcd, 0x6e, 0xca, 0x34, 0xdc, 0x85, 0x4b, 0x8b, 0x6e, 0xd3, 0x6c, 0x13, 0xbc, 0x0c, + 0x3b, 0xe9, 0x38, 0x7a, 0x67, 0x03, 0xa1, 0x28, 0x08, 0x10, 0x4a, 0x1a, 0xfa, 0x67, 0x28, 0x6d, 0xa5, 0x98, 0x87, + 0x96, 0x72, 0xca, 0x65, 0x21, 0xe6, 0x7e, 0x42, 0x86, 0x81, 0xfb, 0xc5, 0x8d, 0xdc, 0xb4, 0x16, 0x48, 0x16, 0x89, + 0x1e, 0xf5, 0xbc, 0x7b, 0x72, 0x95, 0xc5, 0xa0, 0x07, 0x44, 0x0e, 0x70, 0xbd, 0x9b, 0xaa, 0x67, 0x25, 0xc1, 0xc0, + 0xd1, 0x7d, 0xc0, 0x5a, 0x5f, 0x5b, 0xc3, 0x44, 0x2b, 0x04, 0x5e, 0x42, 0x8d, 0x19, 0x12, 0xed, 0x03, 0xf5, 0x90, + 0x98, 0x00, 0x34, 0x05, 0xaf, 0xb1, 0x25, 0xd0, 0xb6, 0x6b, 0x4c, 0x09, 0x14, 0xb0, 0x32, 0xd5, 0x88, 0xc6, 0xcc, + 0x43, 0x47, 0x8c, 0xc4, 0x71, 0xee, 0x47, 0xe4, 0xc1, 0x86, 0xd4, 0x21, 0xda, 0xfe, 0xa6, 0x7e, 0xb0, 0xc6, 0x99, + 0x31, 0x8d, 0x5c, 0x20, 0x1c, 0xaf, 0x41, 0xe1, 0x86, 0xb1, 0x61, 0xfb, 0xaa, 0x26, 0xab, 0x3a, 0x23, 0x32, 0xab, + 0x9e, 0x39, 0xec, 0x57, 0xf1, 0x47, 0x97, 0x58, 0x49, 0xb3, 0xe1, 0x9b, 0xa4, 0xd4, 0xb3, 0xe5, 0xd5, 0x37, 0x46, + 0x22, 0x3d, 0xdd, 0x07, 0x5c, 0x70, 0x0d, 0xa2, 0x9b, 0x92, 0x9f, 0x7d, 0x32, 0x6a, 0x00, 0x8f, 0xda, 0xb4, 0x43, + 0x15, 0x14, 0x83, 0x81, 0x91, 0xa6, 0xd3, 0xd2, 0x98, 0x2e, 0xd1, 0x6c, 0xa0, 0x99, 0xc7, 0x78, 0x22, 0xd2, 0x89, + 0xed, 0x1d, 0xcf, 0x57, 0x2d, 0x1a, 0x59, 0xad, 0xda, 0x20, 0xcb, 0x6f, 0xd3, 0x7a, 0xad, 0x32, 0x32, 0xde, 0x96, + 0x01, 0xf1, 0x47, 0x28, 0x0b, 0x86, 0x8a, 0x8a, 0x24, 0xc5, 0x14, 0x15, 0x97, 0xc6, 0x47, 0xae, 0x02, 0x74, 0x19, + 0x56, 0xad, 0xcd, 0xab, 0xf0, 0xf6, 0x49, 0x0c, 0xf7, 0x41, 0xa9, 0xc2, 0xe9, 0xe5, 0x62, 0xb6, 0x3c, 0x56, 0xe1, + 0x8f, 0x5d, 0x75, 0x12, 0x3c, 0x6d, 0xcf, 0xde, 0x39, 0xd5, 0xe8, 0x54, 0x5f, 0x1c, 0xb2, 0x63, 0x2f, 0xce, 0x18, + 0x88, 0x90, 0x93, 0xd9, 0x6a, 0x17, 0x7d, 0x92, 0xee, 0x35, 0x02, 0x7d, 0x39, 0xc2, 0x55, 0xcf, 0x9b, 0x13, 0xca, + 0x6c, 0x35, 0xd2, 0x51, 0x50, 0x9a, 0x21, 0x8a, 0xe1, 0x29, 0x12, 0x07, 0x9e, 0xe6, 0xc4, 0x61, 0xc2, 0x00, 0x25, + 0x6c, 0x73, 0xa2, 0x8b, 0xf6, 0x9f, 0x61, 0x96, 0xef, 0x59, 0xc6, 0x96, 0xe6, 0xd1, 0x80, 0x14, 0x01, 0x26, 0x95, + 0x62, 0x15, 0xff, 0x60, 0x2e, 0x1c, 0x0f, 0x13, 0x83, 0xc9, 0xcf, 0xb0, 0x0f, 0xe5, 0x4d, 0x0f, 0x2f, 0x8f, 0xca, + 0x81, 0x34, 0xb1, 0x4a, 0x3d, 0x45, 0x6b, 0xa4, 0x76, 0xdb, 0x0d, 0x6c, 0xb9, 0xd2, 0x0d, 0xd5, 0xf8, 0xa2, 0x08, + 0x46, 0xff, 0x52, 0x03, 0xe1, 0xe3, 0x93, 0x18, 0x63, 0x30, 0x29, 0x7a, 0x53, 0x3b, 0x30, 0xed, 0x9b, 0x52, 0x75, + 0x2d, 0x80, 0x8f, 0x4d, 0x15, 0xf8, 0xcf, 0xc1, 0x29, 0x22, 0xe6, 0xce, 0x58, 0x4c, 0x56, 0x67, 0x50, 0x97, 0xfb, + 0xdf, 0x0f, 0x1d, 0x41, 0xd8, 0xbf, 0x4e, 0xe7, 0xe8, 0x2c, 0x40, 0x26, 0x7b, 0xe0, 0x82, 0x58, 0x2a, 0xc6, 0x31, + 0x8f, 0x46, 0x84, 0xa5, 0x22, 0x6b, 0xbc, 0x8f, 0x4b, 0x49, 0xf3, 0xb5, 0x0e, 0x1c, 0x10, 0x85, 0x83, 0xf9, 0xad, + 0x41, 0xdf, 0x42, 0xc8, 0xbc, 0xaa, 0x72, 0x00, 0xa8, 0x8b, 0x71, 0x31, 0xae, 0x25, 0x24, 0x23, 0x3f, 0xee, 0xa8, + 0x1d, 0xa3, 0xa1, 0xc9, 0xc7, 0xa7, 0xeb, 0x54, 0xd3, 0xbd, 0xfa, 0x87, 0x1a, 0x8a, 0xf9, 0x7b, 0x99, 0x18, 0x24, + 0x6a, 0x96, 0xec, 0xbd, 0xf8, 0xe9, 0x3c, 0x72, 0x9e, 0x9a, 0x9e, 0x1a, 0xc6, 0xac, 0x56, 0x37, 0x26, 0x5b, 0xa6, + 0x76, 0xe4, 0x0e, 0xb4, 0x3a, 0xe3, 0xeb, 0xf4, 0x06, 0xe2, 0x78, 0x2f, 0x24, 0x6e, 0x45, 0x47, 0x8a, 0xd2, 0x8f, + 0x2b, 0x23, 0xa0, 0x46, 0xd1, 0xa1, 0x2a, 0x99, 0xe6, 0x6f, 0x86, 0x5c, 0x55, 0x41, 0x87, 0x55, 0x50, 0x4d, 0x31, + 0x33, 0xcd, 0xca, 0xa1, 0x91, 0x06, 0x14, 0x4a, 0x69, 0x0c, 0x8a, 0x5a, 0xaa, 0x90, 0xec, 0x79, 0x89, 0xa5, 0xe7, + 0x38, 0x09, 0x1d, 0xca, 0xa6, 0x83, 0xe7, 0x51, 0xb8, 0x24, 0xec, 0x79, 0xcd, 0x0c, 0xd3, 0x64, 0x2b, 0x2d, 0xab, + 0x5a, 0x54, 0x42, 0x21, 0xd7, 0xe7, 0xa5, 0x52, 0x9e, 0x46, 0xb8, 0x8d, 0xa7, 0x34, 0x5a, 0x45, 0xf9, 0x0a, 0xfb, + 0x38, 0xf9, 0x14, 0xf9, 0x77, 0xa0, 0xac, 0xbe, 0x14, 0x40, 0x06, 0x22, 0x09, 0x56, 0x02, 0xf9, 0x7e, 0xf1, 0x82, + 0x8b, 0xf0, 0x8b, 0x00, 0x5e, 0x45, 0xbc, 0xce, 0x74, 0x43, 0x9e, 0xaf, 0x7f, 0xfd, 0x9f, 0xea, 0xf5, 0x9f, 0x29, + 0x1c, 0x6e, 0x80, 0xf4, 0x06, 0xd2, 0x2c, 0xe8, 0x1f, 0xad, 0x57, 0x5f, 0xa9, 0x4b, 0x99, 0xbd, 0x8e, 0xc2, 0x77, + 0xb7, 0x74, 0x6d, 0xf4, 0x6c, 0x24, 0x42, 0xb3, 0x52, 0xfa, 0x5e, 0x48, 0x5a, 0x06, 0x6a, 0xe4, 0x8b, 0xbd, 0xd9, + 0x80, 0x69, 0x6b, 0x9c, 0xc2, 0xed, 0xbd, 0xa4, 0xc6, 0x5b, 0x8b, 0x13, 0xa0, 0xca, 0x62, 0x8a, 0xef, 0xd8, 0x79, + 0x20, 0xf7, 0xc1, 0xa3, 0x36, 0x7e, 0xbb, 0x73, 0x7b, 0x3e, 0x0d, 0xec, 0x12, 0x51, 0x0e, 0xa2, 0x6d, 0x58, 0x65, + 0xec, 0xf5, 0x45, 0x84, 0xcd, 0x65, 0x49, 0x83, 0x92, 0x0a, 0xbc, 0xf1, 0xca, 0x5d, 0xb8, 0xb9, 0x3d, 0x82, 0x00, + 0xfa, 0x4d, 0x13, 0xe6, 0x76, 0x88, 0x54, 0x18, 0x77, 0xe9, 0x71, 0x52, 0xe6, 0xf9, 0x77, 0x7a, 0x1c, 0x33, 0xc6, + 0xce, 0xcc, 0x33, 0xab, 0xd0, 0xd0, 0xb2, 0xa1, 0xf1, 0x53, 0xb0, 0x5b, 0x64, 0x14, 0x6b, 0x45, 0x01, 0xfb, 0xa0, + 0x14, 0x68, 0x79, 0x10, 0x8a, 0xea, 0x22, 0x3e, 0xc1, 0xf1, 0xe1, 0x8f, 0x86, 0x03, 0x25, 0x86, 0x16, 0x09, 0xb6, + 0xd8, 0x23, 0x1d, 0x36, 0xe5, 0xa6, 0xde, 0xa9, 0xb3, 0x0a, 0xe7, 0x4d, 0x63, 0x59, 0x07, 0xa5, 0xdf, 0xd5, 0xe3, + 0x75, 0xfd, 0x84, 0x37, 0xf8, 0x5b, 0x29, 0xd5, 0xe3, 0x17, 0xf5, 0x7e, 0x8d, 0x5d, 0xa5, 0x3a, 0x8c, 0xd1, 0xe2, + 0x4f, 0x26, 0xa4, 0x31, 0x2e, 0xec, 0xa1, 0x7e, 0x25, 0x1d, 0x7c, 0x41, 0xd9, 0xf5, 0xc8, 0xc6, 0x64, 0x3d, 0x28, + 0x80, 0xfb, 0xbc, 0x7f, 0xfb, 0xa8, 0x9f, 0x05, 0x39, 0x34, 0x22, 0x45, 0x4d, 0xfc, 0x6e, 0xc8, 0x4d, 0xaa, 0x51, + 0x10, 0xbb, 0x36, 0xa5, 0x76, 0x78, 0x0f, 0xb5, 0xf7, 0x6f, 0x32, 0xa8, 0x00, 0x6a, 0x7b, 0xd3, 0x8f, 0x65, 0x70, + 0x5a, 0x3d, 0x4d, 0x4e, 0x18, 0xa9, 0x01, 0x52, 0x53, 0xc4, 0x66, 0xc2, 0xca, 0xc5, 0xe7, 0xc0, 0x6c, 0xd6, 0xa4, + 0xb3, 0xf6, 0x16, 0x5c, 0x5a, 0x46, 0xdd, 0xef, 0x59, 0xb8, 0xfb, 0x58, 0x06, 0x9f, 0x17, 0x6e, 0xa9, 0x3b, 0x68, + 0x85, 0x2c, 0x46, 0xad, 0xdc, 0x84, 0x43, 0x7b, 0x55, 0x25, 0x30, 0xd6, 0x6f, 0xd3, 0xc6, 0x59, 0x2f, 0x70, 0x60, + 0xe8, 0xbd, 0x1f, 0xb8, 0xac, 0xfc, 0x14, 0x88, 0x61, 0x78, 0xd5, 0xbc, 0x39, 0xe6, 0x8c, 0x17, 0xef, 0x79, 0x7b, + 0x86, 0x73, 0xfb, 0x5c, 0xf1, 0x47, 0xcf, 0x37, 0x65, 0xa3, 0x7a, 0x92, 0x38, 0x33, 0xeb, 0x58, 0x52, 0xf5, 0xc8, + 0x50, 0x2e, 0xee, 0x01, 0xa0, 0x42, 0x32, 0x2a, 0x82, 0x48, 0x23, 0x8d, 0xf2, 0x53, 0xe5, 0x95, 0xea, 0x7d, 0xc2, + 0x44, 0x89, 0x80, 0x19, 0x7c, 0xff, 0xa4, 0xd2, 0x15, 0xbb, 0x1e, 0xe0, 0x1f, 0x11, 0x2b, 0x88, 0x68, 0x16, 0x49, + 0x28, 0x0a, 0x48, 0xc6, 0xef, 0x8e, 0xe5, 0x91, 0x9d, 0x49, 0x88, 0xe0, 0xa0, 0xee, 0x06, 0x08, 0x10, 0xf3, 0x35, + 0x42, 0xbb, 0xfc, 0x2b, 0x3d, 0xae, 0xd7, 0xac, 0x50, 0x87, 0x59, 0x76, 0xa1, 0x01, 0x6f, 0xb3, 0xe8, 0x97, 0xca, + 0x85, 0xef, 0xb5, 0x76, 0xb2, 0xbe, 0xbc, 0xfd, 0xb8, 0x5c, 0x93, 0xd2, 0xc1, 0xd2, 0x02, 0x50, 0xb2, 0xb1, 0xcc, + 0xc6, 0xa9, 0x5c, 0xb5, 0x5e, 0x59, 0x8a, 0xd2, 0x09, 0xc3, 0x76, 0x08, 0x29, 0x1e, 0x8c, 0x6a, 0xc4, 0xcc, 0xb1, + 0xa6, 0xc7, 0xbd, 0xf4, 0x60, 0x8f, 0x7b, 0x3f, 0x84, 0xce, 0x05, 0x3d, 0x62, 0x1e, 0x01, 0xe7, 0x65, 0xe5, 0xa9, + 0x90, 0x69, 0x42, 0x85, 0x38, 0x08, 0x20, 0x33, 0xae, 0x7b, 0x60, 0x4c, 0x99, 0x16, 0x3b, 0x2c, 0x26, 0xb3, 0x81, + 0x82, 0x90, 0x1b, 0x9b, 0x44, 0x0a, 0x39, 0x32, 0x89, 0xa5, 0x07, 0xf6, 0x33, 0x20, 0x6b, 0x3d, 0x8a, 0xd3, 0x9a, + 0x56, 0x44, 0x97, 0x22, 0x70, 0xb9, 0x91, 0xf2, 0x4d, 0x9f, 0xd0, 0x2b, 0x33, 0x47, 0xc3, 0xf7, 0xdb, 0x59, 0x09, + 0xc3, 0x72, 0x7c, 0xec, 0xec, 0x65, 0xfd, 0xe3, 0x39, 0x85, 0x6a, 0x6e, 0x67, 0x2e, 0x5f, 0x32, 0xf9, 0xef, 0x75, + 0x18, 0x48, 0x5e, 0x28, 0x7c, 0x56, 0x13, 0x88, 0xb4, 0x24, 0xa5, 0xe0, 0xad, 0xe1, 0xef, 0x45, 0x15, 0xc6, 0xfd, + 0x87, 0xef, 0xc2, 0xc5, 0x8d, 0xef, 0xaf, 0xea, 0xbe, 0x8a, 0xae, 0xbd, 0x11, 0x90, 0x74, 0xce, 0x96, 0x3b, 0x6c, + 0xa0, 0xd6, 0x5b, 0x84, 0xb2, 0xce, 0xeb, 0x8b, 0xfb, 0x9a, 0x3c, 0xbb, 0x6e, 0x3f, 0xee, 0x02, 0x8f, 0x98, 0xac, + 0xcd, 0xda, 0x42, 0xf3, 0xc8, 0x1a, 0xdc, 0xfd, 0x0c, 0xc3, 0x3d, 0x80, 0x1d, 0x4d, 0xdd, 0xe2, 0x17, 0xde, 0x8b, + 0xf4, 0x3e, 0x65, 0xab, 0xb7, 0xfa, 0xa7, 0xcd, 0x2f, 0x7f, 0x6e, 0x1c, 0x53, 0xa8, 0x61, 0xed, 0xa6, 0xba, 0x27, + 0x33, 0x7b, 0x30, 0x2d, 0x83, 0x14, 0xae, 0x74, 0xf5, 0x55, 0xc0, 0x51, 0xd0, 0x73, 0x42, 0x07, 0x9b, 0x28, 0x34, + 0x8f, 0x5f, 0x10, 0xaa, 0x64, 0xfe, 0xf1, 0x72, 0x65, 0x0c, 0x82, 0xf0, 0xb7, 0x23, 0xd6, 0x8a, 0xa8, 0xb3, 0x63, + 0x7f, 0xcc, 0xd5, 0x04, 0xbf, 0xa4, 0x1e, 0x8e, 0x16, 0xe1, 0x5f, 0xea, 0xb0, 0xdd, 0x61, 0x96, 0x1e, 0x68, 0xdc, + 0xec, 0x37, 0xf0, 0x8d, 0xe8, 0xcc, 0xc2, 0x8e, 0x2f, 0x4b, 0xb5, 0x43, 0x87, 0x43, 0xcd, 0xb0, 0x04, 0x7a, 0x1e, + 0x06, 0xe8, 0xa1, 0x1b, 0x7b, 0xbb, 0x54, 0x07, 0xe5, 0x20, 0x11, 0xbd, 0x87, 0x42, 0xe8, 0xd1, 0x5c, 0x9d, 0xf6, + 0xa8, 0x07, 0x6e, 0x2b, 0x0c, 0xc8, 0x83, 0xfe, 0xe0, 0x63, 0x56, 0x48, 0xd5, 0x45, 0x75, 0x1d, 0x35, 0xad, 0x31, + 0x23, 0x1f, 0xd3, 0x77, 0xbf, 0xbc, 0x21, 0xa2, 0x1d, 0x59, 0xaf, 0x31, 0xce, 0xb0, 0xf2, 0xa1, 0x4c, 0x85, 0x29, + 0xd5, 0x05, 0xdb, 0x63, 0x43, 0x7f, 0xd6, 0x76, 0x19, 0x59, 0xa1, 0x88, 0x8e, 0x60, 0xe1, 0x7f, 0x7c, 0x59, 0xdc, + 0x0a, 0x32, 0xb2, 0xe6, 0xb7, 0x25, 0x39, 0x63, 0x1c, 0xfa, 0xba, 0x5c, 0xce, 0xbb, 0x58, 0x3d, 0xfa, 0xf0, 0x24, + 0xa4, 0x48, 0xd6, 0x3e, 0x86, 0x56, 0x03, 0x43, 0x04, 0x21, 0xf9, 0x66, 0xad, 0xf5, 0x1c, 0x70, 0x12, 0xf3, 0xbb, + 0x0e, 0xec, 0xb7, 0xf3, 0x3c, 0xef, 0x10, 0x10, 0x20, 0xff, 0x1a, 0x62, 0x9c, 0x55, 0xd4, 0x3b, 0xd3, 0xa2, 0xaa, + 0x97, 0x8b, 0x59, 0x61, 0x4d, 0xc7, 0x98, 0x34, 0x54, 0x5e, 0xca, 0xa6, 0x52, 0x17, 0x32, 0x9a, 0xc7, 0x82, 0x7e, + 0x74, 0x79, 0x9d, 0xe1, 0xac, 0xa1, 0x3d, 0x4d, 0xbf, 0x19, 0x00, 0x23, 0x6d, 0x17, 0x61, 0xa2, 0x72, 0x58, 0x95, + 0x23, 0x23, 0x57, 0x59, 0x81, 0x8f, 0x32, 0x3e, 0x6f, 0xa0, 0x05, 0x2e, 0xac, 0x2e, 0x39, 0x92, 0x15, 0xa2, 0xa3, + 0xb8, 0xf1, 0x7e, 0x42, 0x0c, 0x1f, 0xc5, 0x4c, 0x74, 0xd2, 0x8c, 0x63, 0xde, 0xfd, 0x39, 0x08, 0xad, 0x39, 0xa2, + 0xc1, 0xc2, 0x5b, 0x1a, 0x72, 0x98, 0x25, 0xaf, 0xac, 0x48, 0x25, 0xc3, 0x6f, 0x05, 0x2a, 0xd3, 0x29, 0x44, 0x6b, + 0x5c, 0x02, 0xa7, 0xed, 0x27, 0xf3, 0x2e, 0x78, 0x66, 0x0a, 0xe7, 0xc2, 0xf1, 0x62, 0xc6, 0x9a, 0x12, 0x43, 0xb1, + 0x1c, 0x95, 0x0e, 0x79, 0xaa, 0x50, 0x77, 0xab, 0x88, 0x5a, 0x5b, 0xf7, 0x93, 0x7e, 0x52, 0x10, 0x4f, 0x5b, 0x82, + 0x8c, 0x9a, 0x1c, 0xef, 0x7a, 0x34, 0x7a, 0x62, 0x51, 0x6a, 0xa4, 0xb8, 0xf9, 0xee, 0x13, 0x96, 0x31, 0x02, 0xcf, + 0x55, 0x4a, 0x8e, 0x0d, 0x55, 0x99, 0xfd, 0x81, 0xfa, 0x66, 0x82, 0x83, 0xbd, 0x84, 0x22, 0xb5, 0x55, 0x72, 0x82, + 0xe9, 0x83, 0x2e, 0xe5, 0x78, 0x14, 0xf6, 0x8d, 0xda, 0xfc, 0x25, 0x82, 0x0b, 0x2c, 0xb9, 0xcb, 0xa7, 0x33, 0xb5, + 0x45, 0x79, 0x26, 0xb7, 0x88, 0xb8, 0x58, 0x87, 0xda, 0xa3, 0x86, 0x0c, 0xe2, 0x4d, 0xd7, 0x56, 0x0c, 0xc3, 0x27, + 0x29, 0x45, 0x38, 0xef, 0x8a, 0xc1, 0x7d, 0xdb, 0x35, 0xe6, 0x12, 0x8a, 0xc9, 0xdf, 0xdb, 0xfd, 0x2c, 0x2d, 0x15, + 0x5f, 0xb5, 0xdd, 0x1c, 0xe5, 0xf9, 0x23, 0x81, 0xee, 0x71, 0x2c, 0xb7, 0x37, 0x69, 0xe2, 0xb3, 0x3c, 0x7d, 0x9b, + 0x8d, 0xc1, 0x42, 0xfe, 0x7f, 0xb3, 0x14, 0x2f, 0xb0, 0x7a, 0x60, 0x52, 0x90, 0x3b, 0x1a, 0x53, 0xb9, 0x76, 0x6c, + 0x6c, 0x2b, 0xdf, 0x5d, 0x8c, 0x75, 0x32, 0xb5, 0xf2, 0x6d, 0xec, 0xd8, 0xf0, 0xab, 0x68, 0xbe, 0xbb, 0xd8, 0xac, + 0x2b, 0x5e, 0xdb, 0xea, 0x17, 0xdc, 0xf1, 0x9f, 0xc3, 0x71, 0xeb, 0x3c, 0x6f, 0x1e, 0x47, 0x1f, 0xf7, 0x6c, 0xdf, + 0xa5, 0x45, 0x88, 0xf5, 0x97, 0x8c, 0x3d, 0x52, 0xe7, 0xc7, 0xc4, 0xdb, 0xf1, 0xb5, 0xdf, 0xae, 0xe3, 0x88, 0x3a, + 0x55, 0xfe, 0x87, 0x85, 0xe9, 0x53, 0xb3, 0x1e, 0xed, 0xf9, 0x34, 0x4d, 0xdf, 0x09, 0xd2, 0x6d, 0x9a, 0xa6, 0xbf, + 0x15, 0x1d, 0x6d, 0xbc, 0x58, 0xd7, 0x80, 0xd9, 0x3b, 0xa0, 0x6e, 0xf6, 0x41, 0xac, 0xe4, 0x58, 0x62, 0x31, 0xac, + 0xf5, 0x38, 0x6c, 0x44, 0xde, 0x34, 0xfa, 0xe0, 0x62, 0x61, 0x62, 0x07, 0x8c, 0xfc, 0x18, 0x16, 0x86, 0x0e, 0x49, + 0x55, 0xdb, 0x35, 0x7e, 0x38, 0xa9, 0x8f, 0xb0, 0x30, 0x56, 0x13, 0xd9, 0xff, 0x2c, 0xc8, 0x7b, 0x50, 0x60, 0x8b, + 0xeb, 0x4e, 0xe3, 0x52, 0x3a, 0xf0, 0xe5, 0x2b, 0x41, 0x33, 0x39, 0xa0, 0x49, 0x6f, 0x31, 0xb6, 0x73, 0x9e, 0x44, + 0x2f, 0x0e, 0x29, 0x4d, 0xa1, 0x88, 0xae, 0xaa, 0xa4, 0xa9, 0x2d, 0xfb, 0x38, 0x1a, 0xac, 0x7d, 0xe9, 0x70, 0xf4, + 0x58, 0x01, 0xc3, 0xca, 0x7f, 0xa7, 0x29, 0x07, 0xea, 0x2e, 0xd8, 0x7c, 0xf4, 0x15, 0x0e, 0x13, 0x7c, 0x1d, 0x34, + 0x59, 0x59, 0xa2, 0x9b, 0xda, 0x50, 0x78, 0x4c, 0xfb, 0x6d, 0x0c, 0x38, 0x54, 0xe1, 0x25, 0x37, 0x61, 0xd5, 0x2d, + 0xc7, 0xfd, 0xad, 0x4c, 0x78, 0xb9, 0x1d, 0x26, 0x5b, 0xc3, 0xd6, 0x40, 0x3c, 0x63, 0x18, 0x0c, 0xa2, 0xa1, 0xc5, + 0x25, 0x89, 0x57, 0x30, 0x6b, 0x64, 0xcf, 0x45, 0xa3, 0x64, 0x58, 0x63, 0xdc, 0x98, 0x50, 0xf1, 0x7a, 0x21, 0x86, + 0xf3, 0x69, 0x9a, 0xa6, 0x28, 0x1f, 0x58, 0x70, 0x83, 0x05, 0xad, 0x0a, 0x87, 0x03, 0x9a, 0x6d, 0x8b, 0x46, 0x8b, + 0xd2, 0xa4, 0x4d, 0x25, 0x9d, 0xc4, 0x57, 0xfa, 0xb9, 0x8c, 0x75, 0xb6, 0xaa, 0x26, 0x8c, 0x38, 0xda, 0x0f, 0x8d, + 0x52, 0x75, 0x10, 0xa1, 0x3a, 0x00, 0xce, 0x26, 0x18, 0xf0, 0xd0, 0x46, 0xf7, 0x03, 0x54, 0x17, 0x32, 0xb4, 0x6b, + 0x58, 0xe4, 0xba, 0x99, 0x38, 0xe2, 0x95, 0x7e, 0xa6, 0x6f, 0xe7, 0x68, 0x68, 0x23, 0x49, 0x9b, 0x20, 0x46, 0x1c, + 0xcd, 0x98, 0xfe, 0x60, 0xd3, 0x46, 0xfb, 0xd8, 0xc4, 0x83, 0x1d, 0xf4, 0x72, 0x5c, 0x90, 0x46, 0x9f, 0x55, 0x72, + 0x50, 0xb8, 0x0c, 0xac, 0x79, 0x25, 0xe5, 0xde, 0x2f, 0xf6, 0x65, 0xac, 0xf1, 0xad, 0x5a, 0x99, 0xad, 0x9e, 0x31, + 0x12, 0x23, 0x7b, 0x21, 0x0c, 0x7e, 0x25, 0x7b, 0x3d, 0x6f, 0x79, 0x4d, 0x71, 0xdf, 0xcf, 0x21, 0x3b, 0x26, 0x0c, + 0x18, 0xe8, 0xa2, 0x4c, 0x4e, 0xbb, 0xfa, 0xe8, 0xd5, 0xe7, 0x77, 0xc3, 0xe5, 0x05, 0xe9, 0xf2, 0xc9, 0x5e, 0x47, + 0xae, 0xfb, 0xe1, 0xcf, 0xbc, 0x22, 0xd8, 0x8f, 0x95, 0xff, 0x1c, 0xa2, 0x88, 0x00, 0x60, 0x05, 0x89, 0x8d, 0x66, + 0x73, 0xb0, 0xaf, 0x8b, 0x8e, 0x76, 0x9d, 0xc8, 0x14, 0x95, 0xe1, 0x25, 0x7b, 0x11, 0x61, 0x17, 0xd1, 0x70, 0xb0, + 0x21, 0x6c, 0x62, 0x8b, 0x69, 0xe8, 0x62, 0xf9, 0x66, 0x7e, 0x5a, 0xe3, 0x76, 0xcc, 0xad, 0x43, 0xa1, 0x93, 0xd4, + 0xe8, 0x36, 0x03, 0x9f, 0xe3, 0x4f, 0xe1, 0x84, 0x63, 0x57, 0x69, 0x83, 0x0a, 0xcb, 0xb1, 0x59, 0xcd, 0xa3, 0x28, + 0x78, 0x3e, 0x5b, 0xe7, 0x50, 0xcc, 0x6d, 0x59, 0x2d, 0x58, 0x91, 0x23, 0xde, 0x71, 0xbd, 0x6e, 0xdb, 0x66, 0x17, + 0x9a, 0x1c, 0x51, 0x45, 0x0e, 0xcc, 0xb2, 0xa5, 0x02, 0x6a, 0xb1, 0xf0, 0xa4, 0x1d, 0x06, 0x13, 0xca, 0x22, 0x9e, + 0x5e, 0x74, 0x99, 0x2f, 0x4a, 0x93, 0xb2, 0x30, 0x17, 0x5b, 0x61, 0x0e, 0x6c, 0xed, 0x23, 0x6b, 0x18, 0x2c, 0x25, + 0x20, 0x8d, 0x7d, 0x58, 0xde, 0xa2, 0x4d, 0xb9, 0x63, 0xb4, 0xff, 0x99, 0xa5, 0xf6, 0xb1, 0x4b, 0x9b, 0x94, 0xbe, + 0xea, 0x0f, 0x2b, 0x13, 0x3e, 0x74, 0xfd, 0xaa, 0xdf, 0x6c, 0x8e, 0x4d, 0x50, 0x3f, 0x84, 0x2f, 0x49, 0x26, 0x35, + 0x38, 0x58, 0x18, 0xe8, 0xad, 0x0a, 0x1b, 0x83, 0x35, 0x01, 0x8f, 0xd2, 0x25, 0xd2, 0x44, 0x5c, 0x1b, 0x15, 0x55, + 0xf9, 0x22, 0x6b, 0xaf, 0xf4, 0x92, 0x7d, 0x40, 0xf0, 0xc6, 0x77, 0xb7, 0xd5, 0x68, 0xf8, 0x8e, 0x35, 0x89, 0x72, + 0x10, 0x1f, 0x56, 0xc3, 0x93, 0x66, 0x70, 0xf9, 0x8b, 0x76, 0xe2, 0x53, 0xb2, 0x5b, 0x5c, 0xa0, 0x81, 0xb3, 0xe0, + 0xe8, 0x9f, 0x11, 0xac, 0xaa, 0xab, 0xc8, 0x6a, 0xb3, 0x21, 0x41, 0x34, 0x0d, 0x96, 0x31, 0xb3, 0x36, 0x47, 0xd5, + 0x26, 0xb1, 0xc6, 0x38, 0x1a, 0xaf, 0xff, 0xce, 0x26, 0xf0, 0xf2, 0xac, 0x41, 0x7b, 0xe2, 0xba, 0xed, 0x52, 0x8b, + 0xc7, 0xe3, 0x3f, 0x1f, 0x79, 0x4c, 0xe0, 0xa0, 0xc5, 0x50, 0xcc, 0x0e, 0xc7, 0x7a, 0xd5, 0xe9, 0x55, 0x7c, 0x15, + 0x7a, 0x68, 0x7d, 0xbd, 0x99, 0xa0, 0xc8, 0xd1, 0x16, 0x66, 0xd9, 0xcc, 0x8d, 0x64, 0x6b, 0x8e, 0xbe, 0x59, 0x5b, + 0x24, 0x7b, 0x07, 0x0d, 0x96, 0x33, 0xf1, 0xc5, 0xa7, 0xd8, 0xbc, 0xd3, 0xd6, 0x31, 0x79, 0xc2, 0xb0, 0x23, 0xf8, + 0xa2, 0xa3, 0x2d, 0xc6, 0xe0, 0x7a, 0x8b, 0x75, 0xec, 0x61, 0x82, 0x94, 0x60, 0xb6, 0x00, 0x17, 0x1d, 0x3b, 0x9f, + 0x0c, 0x5f, 0xc5, 0xde, 0x19, 0xb0, 0x0d, 0xb4, 0x90, 0x3d, 0xf9, 0x45, 0x29, 0x4d, 0xe3, 0xe5, 0x53, 0x8b, 0xe0, + 0xc7, 0x0d, 0x75, 0x4e, 0xe5, 0xff, 0x8c, 0x46, 0x29, 0xe5, 0x49, 0x3a, 0xa1, 0x86, 0xc7, 0x56, 0xc0, 0x00, 0xb5, + 0xec, 0x59, 0xa5, 0xcf, 0x3e, 0xa4, 0x09, 0x5b, 0x88, 0x2d, 0xa9, 0xba, 0x79, 0x82, 0xf8, 0x3b, 0xbc, 0xe2, 0x22, + 0x06, 0x18, 0x39, 0xac, 0x1b, 0xfd, 0xe3, 0x16, 0xe9, 0x3c, 0x9e, 0xae, 0x0f, 0xe6, 0x84, 0xa3, 0xe1, 0xd7, 0x23, + 0x55, 0x26, 0x36, 0x1f, 0xaf, 0xdb, 0xd7, 0xa4, 0xb0, 0x83, 0x97, 0x4a, 0xb6, 0x7f, 0x1d, 0xbd, 0xf5, 0x66, 0x2b, + 0x23, 0x56, 0x24, 0xaf, 0x9c, 0xa2, 0x0a, 0xfa, 0xd5, 0xa7, 0xac, 0x92, 0x41, 0x0d, 0x18, 0xd6, 0x90, 0x51, 0x8d, + 0x18, 0xd7, 0x98, 0xcf, 0x04, 0x95, 0xcf, 0x0d, 0x3d, 0x5f, 0x18, 0x06, 0x2e, 0x0c, 0x23, 0x97, 0x82, 0x89, 0x57, + 0x86, 0x46, 0x85, 0x51, 0x4d, 0xab, 0x59, 0x35, 0xaf, 0xea, 0x4a, 0xd1, 0x1f, 0xf4, 0x6f, 0x81, 0xf1, 0x2f, 0x08, + 0x30, 0x1f, 0x62, 0xb2, 0xbc, 0x96, 0xed, 0x9b, 0xe7, 0x2f, 0x16, 0xcb, 0x2b, 0x18, 0x39, 0x42, 0x49, 0xeb, 0xb3, + 0x5f, 0xd4, 0x19, 0xda, 0xce, 0x01, 0xf4, 0x2d, 0x5d, 0xca, 0x78, 0x52, 0xec, 0xf7, 0x3f, 0xdf, 0xbb, 0xfd, 0x13, + 0xcf, 0x43, 0x5c, 0x36, 0xbe, 0x2c, 0x89, 0x7b, 0xec, 0x83, 0xdd, 0x06, 0x2d, 0x5e, 0x5c, 0x98, 0x51, 0x59, 0x5e, + 0xf4, 0xcc, 0xbb, 0x79, 0xcc, 0x82, 0xa1, 0x4e, 0xed, 0xa1, 0xe6, 0x5a, 0xf1, 0xf6, 0x07, 0x1d, 0xd6, 0x53, 0x71, + 0x6a, 0x25, 0xfb, 0xe6, 0x04, 0x56, 0xa2, 0x69, 0x26, 0xfe, 0x8c, 0xaa, 0x7f, 0xb0, 0xb2, 0x6b, 0x1d, 0xb2, 0x71, + 0xb3, 0xd3, 0xdb, 0x1f, 0xf5, 0x02, 0xf7, 0x71, 0x8d, 0x2b, 0x4b, 0xe0, 0x2c, 0x5f, 0x48, 0x67, 0x45, 0x53, 0x09, + 0x05, 0x68, 0x67, 0x7c, 0xc0, 0x4a, 0x46, 0xd0, 0x9f, 0x0d, 0xfd, 0x78, 0xed, 0x2e, 0xec, 0x14, 0xf9, 0xed, 0xdd, + 0xd3, 0x9d, 0xff, 0x09, 0x27, 0x94, 0x09, 0x8b, 0x44, 0xc5, 0x9f, 0x49, 0x17, 0x49, 0x2f, 0x50, 0xc5, 0xcd, 0xc4, + 0x99, 0x30, 0xd9, 0x8b, 0xb0, 0xd8, 0xed, 0x63, 0x53, 0x02, 0x2e, 0x50, 0x7f, 0xcc, 0x4f, 0x59, 0x3d, 0x8d, 0xa7, + 0xdf, 0xbd, 0x6c, 0x2a, 0x7a, 0xa3, 0xa7, 0xc5, 0x27, 0xff, 0xaa, 0xff, 0x96, 0x7a, 0x3b, 0xcb, 0xcd, 0x7c, 0xbf, + 0x26, 0x85, 0x3f, 0x98, 0x5c, 0xf5, 0x8e, 0x2f, 0x67, 0x9d, 0x87, 0x8d, 0xf3, 0x59, 0xe5, 0x6d, 0xea, 0xe6, 0x52, + 0xaa, 0xd4, 0xc6, 0x06, 0x9b, 0xdc, 0xfc, 0x53, 0xd5, 0x1e, 0x6d, 0x55, 0xb2, 0xfd, 0xf5, 0x38, 0xee, 0xc7, 0x77, + 0xfa, 0x0b, 0xfc, 0x92, 0x5e, 0x9a, 0xd9, 0x74, 0x7e, 0xfc, 0x73, 0x2b, 0xdd, 0x64, 0xf5, 0xcf, 0xe3, 0x52, 0xb7, + 0x54, 0x9b, 0xd4, 0x34, 0x8f, 0xba, 0x66, 0xc4, 0x03, 0xb4, 0xa6, 0xb7, 0x77, 0x3f, 0x65, 0xf5, 0x37, 0xea, 0xa4, + 0xda, 0xc3, 0xfd, 0x5f, 0x93, 0x37, 0x5b, 0x73, 0x31, 0x22, 0x85, 0xb1, 0x78, 0x3b, 0xa0, 0x7a, 0xbf, 0x7b, 0x0e, + 0xe9, 0xdc, 0xf8, 0x4f, 0x4f, 0x08, 0x12, 0xb3, 0x20, 0xf9, 0x7a, 0x7f, 0x43, 0xf1, 0xe0, 0x03, 0x4a, 0x7d, 0x0c, + 0xad, 0x0f, 0xfc, 0x6f, 0x9e, 0xc3, 0x1b, 0x8c, 0x5d, 0xa6, 0x03, 0xb7, 0xdc, 0x5c, 0xe8, 0xe7, 0x2f, 0xc4, 0x59, + 0x10, 0xee, 0xe1, 0x8b, 0xa9, 0x1d, 0x8c, 0x41, 0x39, 0x71, 0x04, 0x0e, 0xbe, 0x1d, 0x08, 0x93, 0x40, 0x7c, 0xbd, + 0xbf, 0xad, 0x78, 0xc8, 0x85, 0xdd, 0xcb, 0xfb, 0xd5, 0x9c, 0x4f, 0xdc, 0x71, 0x69, 0x57, 0x9f, 0x8e, 0x4f, 0xb2, + 0xdb, 0x3d, 0x0b, 0xaa, 0xdb, 0x39, 0xb7, 0x5b, 0x3e, 0x41, 0xd9, 0x2f, 0x3a, 0x52, 0xc3, 0x66, 0x35, 0xb4, 0x8c, + 0x7a, 0xd3, 0xfb, 0xf4, 0xb4, 0x70, 0xad, 0xe1, 0x2e, 0x80, 0x7f, 0x66, 0x40, 0xf6, 0x26, 0xc4, 0xde, 0x04, 0x84, + 0x6c, 0x33, 0xe3, 0x76, 0x33, 0x3e, 0x4e, 0x5e, 0xb3, 0x94, 0xb5, 0x77, 0x4e, 0x83, 0xf3, 0xb8, 0xde, 0x79, 0x5d, + 0xf9, 0x58, 0x94, 0x5c, 0xdd, 0xf1, 0x3a, 0x7d, 0xda, 0x43, 0xbe, 0x6f, 0x79, 0x2f, 0x49, 0x34, 0x38, 0x06, 0xf6, + 0xa2, 0x23, 0xa6, 0xb7, 0x2b, 0x43, 0x64, 0xda, 0x87, 0x31, 0xd4, 0x3d, 0xa9, 0xba, 0x14, 0x56, 0x5f, 0xf6, 0x4d, + 0x8d, 0x79, 0x2d, 0x8b, 0xad, 0x83, 0xae, 0xa6, 0x7b, 0x32, 0x63, 0x77, 0xcc, 0xb8, 0x8a, 0x99, 0xc1, 0x4e, 0x2f, + 0x9d, 0x11, 0x07, 0x2d, 0x1c, 0xfa, 0x23, 0x8b, 0xf7, 0xc9, 0xa8, 0x3b, 0x03, 0x43, 0xb5, 0x98, 0xbe, 0xcd, 0x56, + 0x0e, 0x98, 0x2d, 0x11, 0xe4, 0x35, 0x34, 0xbf, 0xd7, 0x14, 0x06, 0x3b, 0x85, 0x69, 0x63, 0xbd, 0xbd, 0x4b, 0xe5, + 0x52, 0x18, 0x88, 0x7a, 0xcf, 0x7d, 0x11, 0xd6, 0x3e, 0x28, 0x6e, 0xb0, 0x65, 0x82, 0xfd, 0xa2, 0xc4, 0xfe, 0x6d, + 0x3d, 0xcf, 0x0d, 0xec, 0xe2, 0x75, 0x61, 0x73, 0xd1, 0x52, 0x99, 0x22, 0x56, 0xa5, 0xe8, 0xb3, 0xfd, 0xbd, 0x72, + 0x36, 0x2a, 0x39, 0x5d, 0x4f, 0xe0, 0x2c, 0xa8, 0xba, 0xeb, 0xaa, 0x5d, 0xd1, 0x5c, 0xb6, 0x40, 0xef, 0x16, 0x38, + 0xbd, 0x3c, 0x49, 0xcb, 0xb3, 0x4d, 0x91, 0xc4, 0x52, 0x7a, 0xff, 0x89, 0xaf, 0x12, 0xf5, 0xe3, 0xec, 0xf1, 0xec, + 0x1b, 0xe1, 0x74, 0x83, 0xd3, 0xbc, 0x2c, 0x7f, 0xa6, 0x39, 0x7f, 0x57, 0xd1, 0x67, 0x96, 0xf5, 0xfc, 0xf6, 0xd1, + 0x23, 0x10, 0x4b, 0x13, 0xd8, 0x6b, 0x4b, 0xfd, 0x67, 0x6c, 0xfb, 0x10, 0xd3, 0x46, 0xd8, 0x68, 0x3c, 0xda, 0x04, + 0x9c, 0xb7, 0xf7, 0x6e, 0xe4, 0xdd, 0x75, 0x4b, 0x02, 0xae, 0xf1, 0x9e, 0xaf, 0xf9, 0xfe, 0x5e, 0xdb, 0x8a, 0x69, + 0xca, 0xb6, 0x62, 0x3e, 0xfb, 0xea, 0x5a, 0xc4, 0xdc, 0xb8, 0xdc, 0xc0, 0x5e, 0x55, 0x6b, 0xb5, 0x20, 0xd9, 0xde, + 0x87, 0x79, 0xfe, 0xd0, 0xcd, 0xec, 0xf0, 0x0c, 0x1e, 0xb5, 0x81, 0xc4, 0xcf, 0xfd, 0xf4, 0xeb, 0xd1, 0x54, 0xd6, + 0x17, 0x40, 0x98, 0x98, 0x11, 0x89, 0x4f, 0x1c, 0xdf, 0x17, 0x9e, 0x6f, 0xab, 0xf6, 0xdb, 0xe1, 0x3c, 0x6b, 0x8e, + 0x6c, 0x93, 0xee, 0x3e, 0x72, 0xb3, 0xf2, 0x03, 0x7a, 0xd5, 0x34, 0x45, 0x5c, 0xab, 0xfe, 0x89, 0x05, 0xd4, 0x52, + 0xe0, 0x40, 0x9e, 0xba, 0x9a, 0x28, 0x04, 0x3e, 0xe1, 0xf5, 0xf9, 0x4e, 0x01, 0xe8, 0xee, 0x45, 0xd0, 0x8c, 0x04, + 0xaf, 0x05, 0x15, 0x57, 0x75, 0x15, 0xcc, 0x56, 0xae, 0x12, 0x8c, 0xf5, 0x07, 0x0a, 0x9a, 0x27, 0xa5, 0x4c, 0x2a, + 0xa0, 0x07, 0xe4, 0x27, 0x1f, 0x55, 0xf1, 0x01, 0xcf, 0x35, 0x89, 0x5e, 0xaf, 0xe2, 0x9a, 0x38, 0x31, 0xa8, 0xc1, + 0xfd, 0x93, 0xaa, 0xf5, 0xa7, 0x62, 0x63, 0xc0, 0xc6, 0x1f, 0xa8, 0xcb, 0xed, 0xe1, 0x34, 0x2b, 0x49, 0x3a, 0x87, + 0x80, 0x1b, 0xd6, 0xf4, 0x18, 0xd5, 0x75, 0x1c, 0x60, 0xfa, 0xa3, 0xf4, 0x3d, 0xa2, 0xc3, 0x4d, 0x34, 0xdf, 0x0e, + 0xe4, 0x26, 0xdf, 0x0c, 0xbe, 0xd1, 0xc6, 0x7f, 0x1c, 0x7c, 0x35, 0xe8, 0xab, 0xe1, 0x8b, 0xc1, 0xe3, 0x6b, 0x6f, + 0xf8, 0x6c, 0xb0, 0xdd, 0x0d, 0x9f, 0x0c, 0xfe, 0x43, 0x7d, 0xaf, 0xfe, 0x68, 0x10, 0x5e, 0x55, 0xfc, 0x7a, 0xa7, + 0x2b, 0x0e, 0x7a, 0x1f, 0x4e, 0x75, 0xaf, 0xca, 0x7b, 0x6f, 0xf7, 0xee, 0x9d, 0xea, 0x67, 0xef, 0x3e, 0xdc, 0x3f, + 0xb5, 0x35, 0xef, 0x01, 0x50, 0xff, 0x54, 0x30, 0xef, 0xdd, 0xa9, 0x66, 0xf5, 0xde, 0x5e, 0x1d, 0xdf, 0xfd, 0xff, + 0xef, 0xbb, 0x86, 0xf7, 0x1e, 0x9e, 0x7a, 0x5a, 0x56, 0xde, 0x54, 0x7a, 0x9b, 0xf7, 0xfa, 0x5f, 0xcb, 0xea, 0x65, + 0x78, 0x65, 0xd0, 0x56, 0xc3, 0x4b, 0x83, 0xba, 0x1a, 0x5e, 0x18, 0x1c, 0x6a, 0xdb, 0x76, 0x86, 0x47, 0x06, 0x6e, + 0x35, 0x3c, 0x37, 0x58, 0x3f, 0x0d, 0x8f, 0x0d, 0xaa, 0xfd, 0xf7, 0x60, 0x78, 0x62, 0xe0, 0x66, 0xc3, 0xd3, 0xc2, + 0xbc, 0xad, 0xf7, 0xec, 0x9f, 0x89, 0x97, 0xa7, 0x31, 0x76, 0xe8, 0xb0, 0xcf, 0xb5, 0xfb, 0x2d, 0xc4, 0xbc, 0x5d, + 0x8e, 0x5d, 0x75, 0x6a, 0x03, 0x36, 0xea, 0x7f, 0x33, 0x2d, 0x37, 0x9c, 0xf0, 0x1b, 0x81, 0x04, 0x96, 0x67, 0xe7, + 0x0a, 0x30, 0xb5, 0x1f, 0x7a, 0x3c, 0x67, 0x60, 0x6a, 0x25, 0x2b, 0x46, 0xae, 0x62, 0xde, 0x9e, 0xfa, 0x3f, 0xf7, + 0x6d, 0x16, 0x6b, 0x94, 0x20, 0x3d, 0xe4, 0x0f, 0xf1, 0xe3, 0x23, 0x37, 0x84, 0x0e, 0xa3, 0x9f, 0x36, 0x29, 0xef, + 0x02, 0xfc, 0xad, 0x25, 0x39, 0xd0, 0xd7, 0x6e, 0x9f, 0x08, 0xdf, 0x82, 0xb3, 0x88, 0x33, 0x2e, 0x24, 0x22, 0x43, + 0x5c, 0xbd, 0xfe, 0x97, 0xab, 0xee, 0x28, 0x36, 0x9e, 0x69, 0x51, 0xfa, 0xc4, 0xfb, 0x62, 0x9b, 0xe4, 0x98, 0x69, + 0x6e, 0xc4, 0x3c, 0x8d, 0xeb, 0xe2, 0x1c, 0x36, 0x43, 0xa2, 0x72, 0x7b, 0x12, 0x5e, 0x22, 0x85, 0x8f, 0xe4, 0xea, + 0x05, 0xf6, 0x9e, 0x60, 0x8a, 0xfd, 0x1c, 0x98, 0xe5, 0x0c, 0xaa, 0x9c, 0xec, 0x40, 0x38, 0x62, 0x52, 0x8d, 0xbf, + 0x52, 0x1e, 0xdf, 0x8e, 0xaa, 0x3c, 0x0e, 0x80, 0xa8, 0xfd, 0x06, 0xde, 0x81, 0x50, 0x99, 0x72, 0xc8, 0x62, 0x82, + 0x17, 0xf4, 0x78, 0xd1, 0x00, 0xaf, 0x64, 0xc2, 0x6f, 0x6b, 0xe5, 0x96, 0xe0, 0x6c, 0x33, 0x32, 0x61, 0x02, 0x66, + 0x57, 0xb0, 0x0a, 0xe2, 0x7f, 0xd9, 0x93, 0x5e, 0x01, 0xa4, 0x40, 0x8b, 0x4d, 0xc3, 0xd0, 0xbd, 0xc4, 0x77, 0x6c, + 0x4c, 0xba, 0xc2, 0xb5, 0xf4, 0x1b, 0xd6, 0x26, 0xeb, 0x67, 0x40, 0xb1, 0xfb, 0xdb, 0x42, 0x1d, 0x80, 0xfe, 0x0b, + 0xa9, 0xfb, 0x97, 0x33, 0x5c, 0x74, 0xed, 0x22, 0x8a, 0x52, 0x4b, 0x0c, 0x0c, 0xb7, 0x11, 0x68, 0x3b, 0x0c, 0x1a, + 0xaf, 0xd3, 0x57, 0x22, 0xe1, 0x8b, 0x68, 0xa5, 0x5c, 0x78, 0x47, 0xb0, 0x83, 0x1a, 0x9d, 0xaa, 0x89, 0xe6, 0x8f, + 0xf2, 0x46, 0x5b, 0x58, 0x04, 0x61, 0xcb, 0x54, 0x8f, 0x14, 0x30, 0x9d, 0x07, 0xfd, 0x6f, 0x34, 0x7b, 0x49, 0xb5, + 0x84, 0x89, 0x7b, 0x7a, 0xcb, 0x7e, 0x42, 0x56, 0xfc, 0x53, 0x24, 0x8f, 0x9d, 0xa6, 0x3c, 0xf1, 0xc9, 0x79, 0x80, + 0x97, 0x5f, 0x8e, 0x80, 0xec, 0x9a, 0xa0, 0xc8, 0x87, 0xbc, 0xd0, 0x84, 0x89, 0x33, 0xe3, 0x11, 0xc1, 0x00, 0x93, + 0x05, 0xb8, 0xcd, 0xbe, 0xd5, 0x62, 0x3a, 0xe1, 0x80, 0xc9, 0x70, 0x59, 0xc9, 0x8b, 0x52, 0x9c, 0x8b, 0xda, 0xdc, + 0x6c, 0x8d, 0x67, 0x84, 0x21, 0x79, 0x73, 0x97, 0x76, 0x38, 0x18, 0x46, 0xfd, 0xad, 0x21, 0x57, 0x89, 0xd2, 0xbd, + 0x98, 0xb4, 0x2b, 0xd9, 0x95, 0x3e, 0xe9, 0x6c, 0x66, 0x3c, 0xbe, 0xf9, 0x6d, 0x48, 0x29, 0x50, 0xec, 0x0d, 0xe5, + 0xfa, 0x10, 0xbf, 0xb7, 0xda, 0x20, 0x7a, 0xe1, 0xf9, 0xf3, 0x53, 0xd0, 0x70, 0x16, 0x8c, 0x52, 0xe9, 0x00, 0x6d, + 0x10, 0x47, 0x67, 0x4d, 0x78, 0xd6, 0xc9, 0xed, 0xb3, 0x0b, 0xf1, 0x60, 0x55, 0x21, 0xe1, 0x0c, 0x9d, 0x7b, 0xda, + 0x58, 0xea, 0xcc, 0x30, 0x25, 0x31, 0x00, 0x1c, 0x01, 0x8f, 0xb6, 0xc3, 0x73, 0xea, 0x69, 0x2f, 0x05, 0xd0, 0x9b, + 0x3c, 0xef, 0xa7, 0x8f, 0x4a, 0xa5, 0x07, 0x3a, 0x8f, 0x5a, 0x7d, 0x76, 0x96, 0xd6, 0x97, 0x25, 0x64, 0x10, 0x18, + 0x8d, 0x1a, 0x9a, 0x2f, 0xa2, 0x72, 0xbf, 0x45, 0x0d, 0xd6, 0x52, 0x65, 0x8d, 0xbc, 0x79, 0xef, 0xfd, 0xc1, 0x35, + 0x6c, 0x76, 0x0d, 0x95, 0xbe, 0x1e, 0xd7, 0x1c, 0x94, 0x02, 0x73, 0x12, 0xe2, 0xd8, 0x16, 0xde, 0x9f, 0x8c, 0x70, + 0xbd, 0x7d, 0xa1, 0x66, 0x8b, 0x2d, 0x0e, 0x60, 0xee, 0x4f, 0x38, 0xe7, 0x92, 0xec, 0x78, 0xe7, 0x2c, 0x96, 0x5f, + 0xd7, 0x5a, 0x79, 0x49, 0xfe, 0xd5, 0x38, 0x3b, 0xb6, 0xac, 0x72, 0x56, 0x81, 0x10, 0x88, 0xfc, 0x74, 0x3a, 0x91, + 0x88, 0xd5, 0x16, 0x04, 0x8d, 0x14, 0x26, 0x84, 0x75, 0xf2, 0xee, 0xe8, 0x46, 0xbc, 0x75, 0x6a, 0x41, 0x66, 0xe2, + 0x40, 0x01, 0xa6, 0xe2, 0xd4, 0xda, 0x93, 0x3d, 0x03, 0x12, 0xec, 0xcb, 0x02, 0x96, 0x6a, 0x80, 0x5c, 0x48, 0x67, + 0xd2, 0x17, 0x44, 0xd1, 0x49, 0x63, 0x2e, 0xb9, 0x78, 0x0a, 0xd8, 0xd0, 0x29, 0x40, 0xa8, 0x34, 0x61, 0xd4, 0x73, + 0x7c, 0xb7, 0x26, 0xb5, 0xa3, 0x4e, 0x49, 0x98, 0xb9, 0x47, 0x0c, 0x9e, 0xcc, 0x9b, 0x0a, 0x71, 0xeb, 0xb3, 0x84, + 0x15, 0xeb, 0x7c, 0x08, 0xf8, 0x04, 0xc6, 0xdb, 0x88, 0xbd, 0x51, 0x1b, 0xf2, 0x06, 0xd6, 0x8f, 0x85, 0x11, 0x84, + 0x8d, 0x19, 0x26, 0xc7, 0x76, 0x83, 0x27, 0x81, 0x06, 0x58, 0xd8, 0x99, 0x9e, 0x13, 0x18, 0x78, 0x77, 0xd6, 0xda, + 0xd8, 0xf4, 0x7e, 0xd5, 0x89, 0x4a, 0xb5, 0x91, 0x59, 0xfe, 0x75, 0x01, 0x55, 0x5a, 0x5f, 0x01, 0xa8, 0x0a, 0xb8, + 0x88, 0xfc, 0xf1, 0x97, 0x9f, 0x27, 0xff, 0xda, 0x04, 0x19, 0x8c, 0xd8, 0x7c, 0x09, 0xb9, 0x41, 0x2d, 0xd8, 0xc8, + 0x77, 0x8c, 0xb9, 0x12, 0xab, 0xc2, 0x97, 0x30, 0x3c, 0x3f, 0xb5, 0xc3, 0x55, 0x1e, 0xd4, 0xa4, 0xc5, 0x47, 0x44, + 0x16, 0x26, 0x69, 0x79, 0x62, 0xa0, 0xa1, 0xaf, 0x84, 0xca, 0x2f, 0x2e, 0xae, 0xd1, 0xf8, 0x56, 0xf1, 0x18, 0x2c, + 0x3c, 0xbe, 0xe5, 0xda, 0x36, 0xd3, 0x46, 0xd9, 0x83, 0xa9, 0x91, 0xb9, 0xd2, 0x5b, 0xb5, 0xd1, 0x21, 0xae, 0xef, + 0xa1, 0x4d, 0x6e, 0xc2, 0x5e, 0xfc, 0x31, 0xa3, 0xac, 0xf6, 0x38, 0x5a, 0xbc, 0xc6, 0xc2, 0x15, 0x7e, 0x5d, 0x40, + 0xc1, 0xdb, 0xe9, 0x63, 0x87, 0x7e, 0x5c, 0xfa, 0x3a, 0x1c, 0x41, 0xa6, 0x4a, 0x54, 0x5c, 0x45, 0x50, 0x09, 0x51, + 0x0f, 0xd7, 0x00, 0x21, 0x4f, 0xe3, 0x4e, 0x34, 0x5a, 0xd5, 0xa6, 0xf4, 0x6a, 0xa4, 0x51, 0xe0, 0xec, 0x2e, 0xfa, + 0xb0, 0x12, 0x79, 0x4b, 0x95, 0x44, 0x0c, 0x94, 0x30, 0x45, 0xd6, 0xbf, 0x99, 0x38, 0x2b, 0x5b, 0xa2, 0x2a, 0x01, + 0x4c, 0x9d, 0x68, 0xc3, 0x4f, 0xbc, 0x11, 0x06, 0xaa, 0x48, 0xa6, 0x12, 0x09, 0x3a, 0x53, 0x65, 0x00, 0x25, 0x4d, + 0x40, 0x1d, 0xd3, 0xee, 0xc1, 0xc3, 0x0a, 0xcb, 0x4d, 0x96, 0x6b, 0x4c, 0x61, 0xb9, 0xbf, 0x7f, 0xca, 0xb3, 0x52, + 0x97, 0x71, 0x10, 0xb5, 0xf2, 0x34, 0xcd, 0x76, 0xaa, 0xaa, 0x84, 0x6e, 0xe3, 0x8a, 0xf3, 0x92, 0xb5, 0xc8, 0xfb, + 0x71, 0x36, 0x6d, 0x7c, 0x10, 0x34, 0x2c, 0x7a, 0xb7, 0xbc, 0x4c, 0xae, 0x24, 0xd6, 0x27, 0x98, 0x1d, 0x41, 0x66, + 0xd0, 0x49, 0x55, 0x2f, 0x48, 0x4a, 0x48, 0x50, 0xaa, 0x44, 0xfe, 0x47, 0xa5, 0xa4, 0x4e, 0xe2, 0xbe, 0x87, 0xf5, + 0x57, 0x95, 0xc5, 0x2b, 0x56, 0x68, 0xdc, 0xf7, 0xf5, 0xed, 0x24, 0xbf, 0x86, 0x11, 0x8a, 0x01, 0x10, 0x5f, 0x07, + 0x70, 0x84, 0x57, 0x2e, 0x9f, 0x8c, 0x60, 0x18, 0x85, 0x8a, 0x23, 0xd6, 0xb4, 0xc5, 0x95, 0xb8, 0x3c, 0x73, 0x05, + 0x23, 0x3c, 0xfc, 0xad, 0x8a, 0x1b, 0x88, 0x87, 0xaf, 0xdb, 0x80, 0x3e, 0x3e, 0xce, 0x97, 0xde, 0x0b, 0xfa, 0xd6, + 0x42, 0x93, 0x4c, 0x10, 0x67, 0xf3, 0x37, 0x8f, 0x97, 0xcd, 0x9e, 0x2f, 0xbf, 0x68, 0x1a, 0x25, 0x81, 0xbe, 0xe7, + 0x6a, 0xf2, 0xf8, 0x67, 0x91, 0x25, 0xc1, 0x21, 0x68, 0xf1, 0x66, 0x42, 0xe0, 0x8b, 0x5e, 0xb0, 0x6a, 0x56, 0x03, + 0xd3, 0x49, 0x71, 0x30, 0xba, 0xb6, 0x89, 0x3a, 0xc5, 0xea, 0x58, 0x9d, 0xd9, 0x11, 0x06, 0x95, 0x7a, 0x08, 0xd5, + 0x53, 0x3a, 0xd2, 0x9b, 0xaf, 0xe8, 0x47, 0xe1, 0xa6, 0xc4, 0xd7, 0xec, 0x52, 0x55, 0x0a, 0xab, 0xc0, 0x19, 0x88, + 0xae, 0x16, 0x1c, 0x27, 0x36, 0x74, 0xf5, 0x10, 0x2c, 0x1b, 0xc6, 0x06, 0x27, 0x6a, 0xa9, 0x42, 0xd1, 0x36, 0x1f, + 0xef, 0xf9, 0x5e, 0xe0, 0x43, 0xc2, 0xac, 0xf3, 0xe1, 0x81, 0x90, 0xed, 0x60, 0xdc, 0x65, 0xf4, 0x03, 0xaa, 0x3b, + 0x23, 0xe8, 0x35, 0xa6, 0xc7, 0xd4, 0x95, 0x44, 0x86, 0xf9, 0xf9, 0xa5, 0xc3, 0x5a, 0x67, 0xe0, 0xea, 0xa1, 0xfb, + 0x21, 0x35, 0x06, 0x35, 0xfc, 0xc1, 0xe8, 0x2a, 0x5c, 0xed, 0xee, 0x9b, 0xe9, 0x20, 0xb0, 0x55, 0x13, 0xa6, 0x66, + 0xc0, 0x34, 0x49, 0x91, 0x98, 0xac, 0x67, 0xd9, 0xd6, 0x8d, 0x7a, 0x5c, 0x50, 0x3e, 0xfb, 0x38, 0x69, 0xfb, 0xba, + 0xb2, 0x82, 0x34, 0x73, 0x21, 0x28, 0x63, 0xe8, 0xa8, 0x4f, 0xac, 0xb3, 0x1a, 0x41, 0x8e, 0x14, 0x96, 0xb6, 0x90, + 0x89, 0x62, 0xcd, 0x69, 0x57, 0x69, 0x5a, 0x59, 0xe2, 0x8f, 0xe9, 0x58, 0xe4, 0xc2, 0x26, 0x83, 0x96, 0x43, 0x29, + 0x4d, 0x9a, 0xf6, 0x4f, 0xf9, 0x44, 0xf8, 0xad, 0x44, 0xd6, 0xaf, 0x6f, 0xf0, 0xec, 0xd9, 0xed, 0x68, 0x03, 0x8c, + 0x97, 0xae, 0x91, 0x4e, 0xb1, 0x1e, 0x63, 0xb7, 0x7c, 0x8f, 0x91, 0xf0, 0x3d, 0x34, 0xd5, 0x57, 0xf9, 0x14, 0xe7, + 0x8e, 0xe8, 0x69, 0x63, 0xf9, 0x77, 0xcf, 0x6e, 0x41, 0xf9, 0x9a, 0xef, 0xb1, 0x20, 0x6d, 0xef, 0x73, 0x26, 0x95, + 0x2b, 0x4a, 0x0c, 0x39, 0x2a, 0xa9, 0xe0, 0x41, 0x03, 0x80, 0x59, 0x9d, 0x55, 0x8d, 0x06, 0x60, 0x17, 0xf9, 0x9d, + 0x52, 0x41, 0x86, 0x4b, 0x64, 0x81, 0x1b, 0x60, 0x7d, 0x00, 0x87, 0x32, 0x53, 0x32, 0x3c, 0x98, 0x5f, 0x61, 0x32, + 0x31, 0xd2, 0xef, 0x50, 0x1c, 0x8f, 0x3b, 0xde, 0xba, 0xe7, 0xa7, 0xa4, 0xd9, 0x69, 0x0f, 0x30, 0x37, 0x91, 0x3c, + 0x0b, 0x0b, 0xfb, 0x20, 0x67, 0xbf, 0x33, 0x0f, 0x84, 0xd1, 0x3a, 0x7f, 0xba, 0xd9, 0x4f, 0x4a, 0x24, 0x78, 0x48, + 0xa9, 0xed, 0xcd, 0x88, 0x72, 0x22, 0x73, 0x29, 0xf5, 0x8d, 0x6d, 0xab, 0x06, 0x53, 0xa4, 0x84, 0x41, 0xa7, 0x11, + 0xbd, 0xb6, 0xb1, 0xbb, 0xa3, 0xd1, 0xf9, 0x27, 0xaa, 0x05, 0x03, 0x99, 0xe1, 0x88, 0x03, 0x58, 0x13, 0xe1, 0x64, + 0x66, 0x67, 0x46, 0x16, 0x64, 0xde, 0x66, 0xee, 0xcf, 0xa4, 0xb9, 0x44, 0x74, 0x5b, 0x6d, 0xae, 0xc8, 0x0c, 0xd3, + 0x53, 0xdc, 0xbd, 0xad, 0xe4, 0xe8, 0xae, 0x77, 0x00, 0x5a, 0xe9, 0xc3, 0xf9, 0x5f, 0x8f, 0xe7, 0xc8, 0x68, 0xc0, + 0xeb, 0x39, 0x57, 0x41, 0xf3, 0x17, 0x38, 0x4f, 0x73, 0x6b, 0x6b, 0x62, 0xa4, 0x26, 0x73, 0x5a, 0xe5, 0xf9, 0x5e, + 0x46, 0x3f, 0x57, 0x8d, 0x3e, 0x6a, 0xe9, 0xd4, 0x6b, 0x90, 0x08, 0x95, 0x19, 0xf1, 0xe7, 0x92, 0xb7, 0x17, 0x10, + 0xdd, 0xa5, 0x12, 0xc6, 0xda, 0x09, 0x98, 0xb9, 0x17, 0xeb, 0x7c, 0x9e, 0x5e, 0x7f, 0x32, 0x69, 0x32, 0x5f, 0xee, + 0xde, 0x05, 0xf2, 0x8e, 0x13, 0x0c, 0x9f, 0x7d, 0x86, 0x21, 0xb2, 0xb8, 0xf8, 0xc5, 0xeb, 0xe9, 0xbd, 0x48, 0x40, + 0xef, 0x13, 0x66, 0x79, 0x4b, 0xc5, 0x2d, 0x98, 0x87, 0x5a, 0x1a, 0xcb, 0xcf, 0xe4, 0xf6, 0x8b, 0xde, 0x11, 0xec, + 0xbd, 0x17, 0x37, 0xbe, 0xfa, 0xbf, 0xb1, 0x67, 0x48, 0xec, 0x7f, 0x2e, 0x91, 0x8a, 0xab, 0xca, 0xdc, 0x8f, 0x25, + 0xa9, 0x82, 0xd5, 0x74, 0x9e, 0x22, 0x19, 0xec, 0xdd, 0x54, 0x83, 0x80, 0x4d, 0x91, 0x31, 0xed, 0x79, 0x80, 0xde, + 0xa0, 0xef, 0x2c, 0xc2, 0x46, 0x45, 0x11, 0xd3, 0x4f, 0x6a, 0x56, 0xe6, 0xe8, 0x74, 0x2c, 0x59, 0x39, 0xb0, 0xd3, + 0xef, 0x5e, 0x7c, 0xfb, 0x35, 0x52, 0xe5, 0xbd, 0xed, 0xdb, 0x59, 0x2b, 0x42, 0xd0, 0xf0, 0x21, 0xd3, 0xdb, 0xf3, + 0x3c, 0xcf, 0x55, 0x16, 0xf7, 0xf1, 0x77, 0x89, 0xc3, 0xc4, 0x28, 0x5b, 0xa3, 0x84, 0x27, 0x5a, 0xd0, 0xcb, 0x5f, + 0x34, 0x45, 0x83, 0xaf, 0x52, 0x14, 0x16, 0xe8, 0x55, 0x43, 0x8e, 0x96, 0xe5, 0xbb, 0x92, 0x06, 0xaa, 0x82, 0xeb, + 0x96, 0xc1, 0xc2, 0xdd, 0xa9, 0x90, 0xd6, 0xa9, 0xb9, 0x50, 0xb6, 0x4f, 0x25, 0xf8, 0x0f, 0xa9, 0xdd, 0x98, 0xa5, + 0x0a, 0xa9, 0x80, 0xea, 0x78, 0xc0, 0xdb, 0x1e, 0x03, 0x2d, 0x4f, 0x30, 0x7b, 0xaf, 0x95, 0x14, 0x83, 0x0a, 0x72, + 0x1b, 0x00, 0x5b, 0x6e, 0x08, 0xd7, 0xe0, 0xe9, 0x18, 0x44, 0xc2, 0x9d, 0x2f, 0x8b, 0xfe, 0xb7, 0x37, 0xf5, 0xac, + 0xfa, 0x4b, 0x86, 0x45, 0xf1, 0xde, 0xf4, 0x1f, 0xb5, 0x69, 0x08, 0x82, 0x6f, 0xa3, 0x44, 0xc4, 0x9f, 0xf9, 0x40, + 0xd5, 0x1a, 0x18, 0xeb, 0x3a, 0x0c, 0x1e, 0x48, 0x61, 0xb2, 0x65, 0x5a, 0x36, 0xa5, 0x4e, 0xdd, 0xc2, 0xee, 0x13, + 0x94, 0xb7, 0x41, 0xf5, 0x5e, 0x2a, 0x2b, 0x1f, 0x50, 0x04, 0x64, 0x45, 0x19, 0x94, 0x8a, 0x7b, 0xba, 0x9e, 0x55, + 0x6c, 0xc2, 0x4f, 0x2f, 0x2b, 0x67, 0xac, 0x83, 0x78, 0x29, 0xff, 0xeb, 0x51, 0xf9, 0x3d, 0xda, 0x1a, 0xea, 0x6b, + 0x51, 0x48, 0x98, 0xe3, 0x16, 0xe3, 0x07, 0x3b, 0x43, 0x27, 0x50, 0x4b, 0x29, 0x9f, 0x10, 0x5f, 0x1c, 0xa2, 0xb0, + 0x73, 0xa8, 0x50, 0x9b, 0x49, 0x08, 0x0b, 0xaf, 0x7e, 0x21, 0xbd, 0xec, 0x87, 0xe0, 0x5e, 0x71, 0x44, 0xaa, 0x4c, + 0xee, 0x58, 0xa7, 0xca, 0x6f, 0x10, 0x0b, 0xb3, 0xb7, 0xef, 0xfb, 0x7d, 0x1d, 0xfc, 0x9d, 0xfe, 0xc7, 0x4f, 0xf8, + 0x68, 0x4f, 0xfb, 0xd1, 0xce, 0xe7, 0x65, 0x40, 0xfd, 0xf1, 0xd4, 0xb4, 0x6d, 0x58, 0xd3, 0x6e, 0xb0, 0x48, 0x5f, + 0x93, 0x85, 0x99, 0x78, 0x68, 0xc6, 0xbf, 0x2d, 0xca, 0xfb, 0x94, 0xce, 0x56, 0x35, 0x83, 0xaa, 0x25, 0xff, 0xfa, + 0x57, 0x85, 0x0d, 0xc2, 0x34, 0x60, 0x27, 0x80, 0xd0, 0x17, 0x79, 0x3f, 0x73, 0x3d, 0x44, 0x08, 0xbe, 0x60, 0x00, + 0x77, 0x0e, 0x7d, 0x81, 0x3a, 0x87, 0xa1, 0x6a, 0xbd, 0x9c, 0xeb, 0xc8, 0x46, 0xcd, 0xf1, 0x6a, 0xd7, 0x47, 0x7f, + 0xa0, 0xef, 0xfd, 0x34, 0xf2, 0x67, 0x4b, 0x2d, 0xb8, 0x19, 0x37, 0xeb, 0x16, 0x70, 0x06, 0x67, 0xf1, 0x1c, 0x28, + 0xd3, 0x57, 0x83, 0x17, 0xe7, 0x32, 0x5a, 0x1b, 0x98, 0x82, 0x69, 0xe5, 0x86, 0x8b, 0xa2, 0x74, 0xec, 0xa8, 0x17, + 0xbb, 0xb6, 0x8a, 0x2e, 0xdd, 0x46, 0x8e, 0x72, 0xbe, 0x65, 0xef, 0x50, 0x95, 0xb0, 0xbe, 0x64, 0x13, 0x79, 0x17, + 0xd3, 0xcb, 0xab, 0xf3, 0x8a, 0x66, 0xbc, 0x6a, 0xcb, 0xda, 0x03, 0x11, 0x67, 0x42, 0xbe, 0xe8, 0x9e, 0xa2, 0x51, + 0xe0, 0xd0, 0x54, 0xed, 0xe2, 0xdf, 0x8f, 0xb8, 0xaa, 0x77, 0xbd, 0xf8, 0x37, 0xbb, 0x66, 0x5d, 0xcf, 0xc4, 0x80, + 0x51, 0x4e, 0xbe, 0x60, 0xe5, 0x30, 0xbc, 0xe2, 0x9e, 0xfa, 0xbe, 0x48, 0xcf, 0x33, 0xea, 0x55, 0x34, 0xb7, 0xef, + 0xd4, 0x9f, 0xe3, 0x59, 0xcd, 0xf5, 0x67, 0xdb, 0xb0, 0x87, 0x25, 0xef, 0xcb, 0xed, 0x93, 0x73, 0xd2, 0xaa, 0x53, + 0x4e, 0xa9, 0x5d, 0x78, 0x09, 0x8f, 0x6c, 0x6f, 0x68, 0x50, 0xe6, 0xce, 0xfa, 0xb4, 0x3b, 0xdc, 0x4f, 0x8e, 0x8a, + 0x32, 0x76, 0xc5, 0x61, 0x9f, 0x51, 0xd2, 0xfb, 0x8a, 0x9b, 0xc3, 0x10, 0x83, 0x53, 0x27, 0x50, 0x94, 0xf5, 0x08, + 0x2b, 0xcf, 0x03, 0xfb, 0xed, 0x8a, 0x9f, 0x81, 0x73, 0x98, 0xda, 0x6d, 0x4c, 0xee, 0xfa, 0x94, 0x4a, 0xee, 0xab, + 0x8a, 0xee, 0x23, 0xe3, 0x82, 0xbd, 0xc3, 0xfa, 0x83, 0x83, 0x3e, 0xe2, 0xb2, 0xc5, 0xc7, 0x8f, 0x59, 0x80, 0xbf, + 0xaa, 0xce, 0xfb, 0x86, 0x21, 0x14, 0x60, 0xb2, 0x4a, 0x4d, 0x1b, 0xc5, 0x4b, 0x86, 0xcd, 0xbd, 0x93, 0x8f, 0x4b, + 0xd4, 0x09, 0xee, 0xaf, 0xd1, 0xb2, 0xda, 0x0d, 0xf0, 0x79, 0x12, 0x4b, 0xcc, 0x89, 0xf6, 0xd8, 0x3f, 0xde, 0xac, + 0x66, 0xf2, 0x27, 0x66, 0xe8, 0x33, 0x54, 0x0b, 0xeb, 0x58, 0xfe, 0x20, 0xce, 0x4f, 0x7d, 0x7e, 0xbb, 0x24, 0xf9, + 0x9b, 0xa1, 0xc2, 0xc2, 0xa6, 0xb0, 0x82, 0xb0, 0x95, 0xaf, 0x2f, 0xec, 0x00, 0xea, 0xbd, 0xc9, 0xec, 0xfe, 0x0d, + 0xe3, 0xcb, 0x2e, 0xe1, 0xcb, 0xed, 0x12, 0xc5, 0xb2, 0x8b, 0xc3, 0x45, 0x2e, 0x23, 0x0a, 0x27, 0x1e, 0x8c, 0x80, + 0x17, 0x95, 0x75, 0xe0, 0x87, 0x75, 0xc4, 0xc7, 0xe7, 0x71, 0xb9, 0x20, 0x5a, 0x94, 0xe6, 0xcf, 0x83, 0x96, 0x25, + 0x1d, 0xd7, 0xf4, 0x4d, 0x74, 0x98, 0xd2, 0x04, 0x84, 0xec, 0xb1, 0x29, 0xf4, 0x63, 0x95, 0xa2, 0xba, 0x59, 0x3a, + 0x70, 0xe7, 0xc6, 0x76, 0xd5, 0x48, 0xf9, 0x5d, 0xbf, 0x4e, 0x77, 0xb2, 0x6b, 0xd9, 0x3f, 0x65, 0xc8, 0x7c, 0xd4, + 0x05, 0xf3, 0xc7, 0x99, 0x2a, 0x1d, 0x72, 0xed, 0xf5, 0x69, 0x57, 0x45, 0xd0, 0x14, 0xfb, 0x9f, 0x76, 0xf5, 0x92, + 0xee, 0x8b, 0x1f, 0x15, 0xd0, 0xea, 0xa2, 0x43, 0x8a, 0x1c, 0x18, 0xc3, 0x21, 0x61, 0xb8, 0x11, 0xb1, 0x6d, 0x48, + 0x82, 0xc7, 0xca, 0x29, 0xbc, 0x10, 0xf7, 0xc7, 0x91, 0x8a, 0x51, 0x15, 0xdd, 0xd8, 0xda, 0xd8, 0xc6, 0x66, 0x62, + 0x1e, 0xd7, 0x43, 0xf9, 0xab, 0x28, 0x93, 0x26, 0xb8, 0x1b, 0x0c, 0xea, 0xec, 0x79, 0xa2, 0x14, 0xb4, 0x99, 0xe9, + 0xb1, 0x15, 0x4e, 0x93, 0x5b, 0xee, 0x76, 0x91, 0x44, 0x97, 0x85, 0xa1, 0xd9, 0x1a, 0x4c, 0x1c, 0x23, 0xf5, 0x16, + 0x24, 0xb2, 0x2d, 0x85, 0xcb, 0x2e, 0x7e, 0xa3, 0x28, 0x61, 0xd0, 0xf9, 0x4c, 0x30, 0xde, 0x44, 0xc0, 0x94, 0x23, + 0x4f, 0x13, 0xda, 0x4a, 0x1e, 0x8d, 0x91, 0x57, 0x32, 0x4d, 0x65, 0x7b, 0x2c, 0x7f, 0x24, 0xc9, 0x94, 0x9b, 0xe9, + 0x62, 0xa1, 0x17, 0x13, 0x04, 0xaa, 0xb0, 0xea, 0xad, 0x58, 0x49, 0x04, 0x60, 0xb9, 0x82, 0xb2, 0xec, 0xd2, 0xfd, + 0xbc, 0x02, 0x47, 0x1e, 0xa6, 0x53, 0xc4, 0x86, 0x27, 0x8d, 0x8c, 0xc4, 0x89, 0xaf, 0x2f, 0xc9, 0x96, 0x53, 0x33, + 0x38, 0x8b, 0x78, 0x60, 0xaa, 0xdb, 0xdc, 0x78, 0x79, 0xa4, 0xd8, 0xba, 0x97, 0xde, 0x89, 0xb8, 0x74, 0x9d, 0x95, + 0xa2, 0x1c, 0x55, 0x52, 0xa8, 0xe7, 0x4c, 0xa3, 0xa9, 0xbc, 0xb5, 0x85, 0x12, 0x59, 0x05, 0xad, 0x92, 0xd3, 0xff, + 0xef, 0x88, 0x24, 0x24, 0x5c, 0x08, 0x2c, 0xfe, 0x32, 0x15, 0xd2, 0xec, 0xad, 0xb6, 0x63, 0x18, 0x44, 0xba, 0xce, + 0x0b, 0x6e, 0x19, 0xbf, 0xfa, 0x05, 0x00, 0x7a, 0x2b, 0xda, 0x06, 0xa6, 0x8b, 0x05, 0x9c, 0xd9, 0xd9, 0x8c, 0xde, + 0xe6, 0xc2, 0xac, 0x8e, 0x2b, 0xfa, 0x89, 0xd5, 0xbf, 0x86, 0x85, 0xdd, 0xb3, 0xfd, 0x78, 0xb0, 0x63, 0x46, 0x53, + 0x57, 0x09, 0x61, 0x98, 0x20, 0x8b, 0x5e, 0x06, 0x77, 0xc8, 0x22, 0x8c, 0xc0, 0xae, 0x1c, 0xda, 0xc8, 0x84, 0xf3, + 0x15, 0x84, 0x7f, 0x8e, 0xf9, 0x7a, 0x0a, 0x2c, 0xcb, 0xfd, 0xc9, 0x50, 0x0f, 0x03, 0xc2, 0x44, 0x46, 0x38, 0x82, + 0x24, 0x64, 0x53, 0x21, 0x98, 0x78, 0x0a, 0xea, 0x26, 0x38, 0xb0, 0xc5, 0xd1, 0x8d, 0x8d, 0x52, 0x98, 0x11, 0x7f, + 0xc5, 0x82, 0x91, 0xdb, 0xc7, 0xf8, 0xf6, 0x80, 0xc2, 0x2b, 0xd8, 0x29, 0x84, 0xea, 0xe5, 0xa5, 0x36, 0xbd, 0xd8, + 0x8f, 0x7c, 0x07, 0x7d, 0x3c, 0x9b, 0xe9, 0xc8, 0x0b, 0x32, 0x4c, 0xa7, 0x21, 0x0d, 0x40, 0x42, 0x78, 0xe1, 0xa6, + 0x6e, 0x7f, 0x72, 0x68, 0x9d, 0x4c, 0x15, 0x58, 0xde, 0xe5, 0x4d, 0x27, 0x23, 0x20, 0x2f, 0xec, 0xb2, 0x52, 0xcc, + 0xa7, 0xff, 0x54, 0x8d, 0xed, 0x30, 0x9d, 0x76, 0x38, 0xbb, 0x98, 0xbb, 0x42, 0x63, 0x26, 0x22, 0x2f, 0xca, 0x15, + 0xb6, 0x5e, 0x9c, 0xe6, 0x70, 0x80, 0xf7, 0xb8, 0x7c, 0x43, 0x42, 0xc8, 0x07, 0x2f, 0x48, 0x87, 0xe8, 0x59, 0x9a, + 0x8f, 0x19, 0xf5, 0xc2, 0x5b, 0x5f, 0x64, 0x0a, 0x02, 0xfe, 0x74, 0xeb, 0x23, 0x51, 0x8d, 0xf4, 0x14, 0x2d, 0x4e, + 0xa8, 0x2c, 0xd9, 0x16, 0xc8, 0xe9, 0xbf, 0x20, 0x3a, 0x18, 0x63, 0xf9, 0x36, 0xe1, 0xcd, 0xcb, 0x2d, 0x6b, 0xbc, + 0xfd, 0xc8, 0x76, 0x86, 0x52, 0xfe, 0xc6, 0x71, 0x88, 0xe9, 0x4c, 0x26, 0x76, 0x66, 0x02, 0x46, 0x0f, 0x0b, 0x68, + 0x1d, 0xb8, 0x19, 0x79, 0xfc, 0xe4, 0xd5, 0x9b, 0x90, 0x9b, 0xcf, 0xd5, 0xff, 0xfc, 0xb2, 0x75, 0x16, 0xf7, 0x6e, + 0x2f, 0x25, 0x0e, 0x9d, 0x99, 0xcd, 0x32, 0x18, 0xaf, 0x68, 0x80, 0xe0, 0xe4, 0x1a, 0x30, 0x0c, 0xca, 0xd2, 0x0f, + 0x04, 0x8c, 0x5d, 0x1e, 0xa9, 0xba, 0x19, 0x3f, 0x42, 0xcc, 0x76, 0x59, 0x3e, 0x44, 0x5a, 0x18, 0xed, 0x5b, 0xa0, + 0xb0, 0x03, 0x66, 0x2e, 0x8e, 0x40, 0xde, 0x73, 0x99, 0x79, 0x0d, 0x44, 0xeb, 0xf3, 0xcd, 0x79, 0x7c, 0x9d, 0x94, + 0xff, 0x28, 0x9a, 0x43, 0x5a, 0xd1, 0x8c, 0xfc, 0x3e, 0x1a, 0x3d, 0xd6, 0xdb, 0xbc, 0xd9, 0x8e, 0xab, 0x4c, 0xd9, + 0x12, 0x8c, 0x28, 0xb9, 0xb1, 0xc3, 0x7c, 0x50, 0x71, 0x15, 0xd8, 0x92, 0xaf, 0xd1, 0xad, 0x1d, 0xe2, 0x70, 0xee, + 0x37, 0x2d, 0xf2, 0xb6, 0xe5, 0xe8, 0xa2, 0xb0, 0x5b, 0x81, 0xf3, 0xab, 0x86, 0xb6, 0x12, 0xdf, 0xc8, 0x9f, 0x8c, + 0x89, 0x2a, 0x24, 0x88, 0x09, 0x7a, 0x34, 0x9c, 0x7f, 0x10, 0xa2, 0xa1, 0xcb, 0x64, 0xb7, 0x6c, 0xd2, 0x97, 0xda, + 0xc2, 0x55, 0x60, 0x16, 0xd8, 0x6d, 0xec, 0x77, 0x7d, 0x3c, 0x6f, 0xc7, 0x65, 0x66, 0xcd, 0x87, 0x5a, 0xf1, 0x15, + 0xce, 0x05, 0x41, 0xa5, 0x35, 0xdc, 0x92, 0xfc, 0xdf, 0xcf, 0xfb, 0x67, 0xdc, 0x7a, 0x5a, 0xf6, 0xea, 0x7b, 0xe8, + 0xf7, 0xf5, 0x5e, 0x2d, 0x17, 0xbd, 0x48, 0x2d, 0xfa, 0x6a, 0x34, 0x6d, 0x3c, 0xbf, 0x7f, 0x7d, 0x7d, 0x21, 0x9d, + 0xde, 0xf1, 0x2b, 0xbf, 0x85, 0xee, 0x1d, 0xb8, 0xa2, 0xdc, 0xe0, 0xe7, 0x2a, 0x1e, 0xce, 0xfe, 0x2b, 0x77, 0x58, + 0x1d, 0xd7, 0xaf, 0xaa, 0xcb, 0x36, 0xc7, 0x33, 0xd8, 0x1b, 0xfd, 0xb6, 0x3d, 0x03, 0xfe, 0xbf, 0x05, 0x48, 0x7c, + 0x91, 0x92, 0x49, 0x05, 0x0a, 0x40, 0xa0, 0xbb, 0x1e, 0xfc, 0x11, 0x84, 0x51, 0x4a, 0x3b, 0x7c, 0xfc, 0x98, 0x4c, + 0x54, 0x70, 0x78, 0x75, 0x6e, 0xa1, 0x59, 0x8f, 0xf4, 0xfb, 0x3c, 0xdd, 0xf5, 0xf8, 0x53, 0x1b, 0x55, 0x27, 0x02, + 0x99, 0xd9, 0x38, 0xd3, 0x4e, 0xb9, 0xfe, 0x6d, 0xa3, 0x3f, 0xab, 0xf0, 0xad, 0x42, 0x45, 0x77, 0x5f, 0xfc, 0xe3, + 0xaa, 0xd1, 0xbb, 0xee, 0x2a, 0xfc, 0x70, 0xd5, 0xab, 0xb7, 0xdd, 0xed, 0xbb, 0x15, 0x15, 0x6b, 0x58, 0x9e, 0x31, + 0xc3, 0xa0, 0x39, 0x22, 0x9a, 0x9d, 0xf2, 0xff, 0x7d, 0x64, 0xeb, 0x45, 0xc4, 0x92, 0xad, 0xb8, 0x00, 0x79, 0xb1, + 0x8d, 0xd3, 0x67, 0xf1, 0x46, 0x35, 0x17, 0xae, 0x3c, 0xea, 0xdd, 0x49, 0xba, 0x37, 0x18, 0xaa, 0xf9, 0xfd, 0x80, + 0xd7, 0x05, 0x5d, 0x39, 0xf1, 0xd1, 0xf1, 0x4e, 0xd9, 0xfa, 0x68, 0x6c, 0xff, 0x2b, 0x5f, 0x43, 0xc7, 0xe6, 0xc5, + 0xb6, 0x03, 0xbb, 0xe1, 0xc7, 0x6c, 0xe2, 0xcd, 0xa7, 0xf5, 0xf8, 0x8c, 0xcf, 0xd3, 0xb8, 0xc7, 0x18, 0xde, 0x19, + 0xb7, 0xe6, 0x01, 0x9f, 0x19, 0x65, 0x06, 0x72, 0x19, 0xb2, 0xf7, 0x1e, 0xd6, 0xe8, 0xa9, 0x03, 0xfa, 0x35, 0x15, + 0x0a, 0x80, 0x45, 0xb9, 0x98, 0x21, 0xad, 0x99, 0xd1, 0xbf, 0x81, 0x46, 0x94, 0x8c, 0xf2, 0xf9, 0xdc, 0x59, 0x74, + 0x43, 0xa7, 0x4f, 0x40, 0x06, 0xd6, 0xd6, 0x01, 0x6b, 0x89, 0x45, 0x85, 0x68, 0x13, 0x9a, 0x4c, 0x00, 0xee, 0x93, + 0x60, 0x43, 0xe1, 0xd7, 0x5a, 0x4e, 0x82, 0x9f, 0xbb, 0x57, 0x82, 0xa4, 0x97, 0xe2, 0x28, 0x9d, 0x4c, 0x18, 0xb4, + 0x7b, 0xcd, 0xcb, 0x97, 0xbd, 0xcf, 0xed, 0xfa, 0x90, 0xf9, 0xc8, 0x9e, 0xb5, 0xe6, 0x64, 0xe4, 0x6b, 0xcd, 0x51, + 0x77, 0xf2, 0x06, 0x52, 0x36, 0xfb, 0x85, 0x61, 0x81, 0xc5, 0x6f, 0x35, 0x4c, 0x6e, 0xbd, 0x39, 0xa5, 0xf6, 0x11, + 0x4f, 0x12, 0x38, 0x1b, 0x5e, 0x37, 0xd4, 0x5a, 0x68, 0xaf, 0x57, 0x38, 0xaa, 0xf4, 0xe9, 0x4e, 0x29, 0x37, 0xd7, + 0x63, 0xef, 0xbe, 0xf5, 0xad, 0xf4, 0x84, 0xbc, 0xf3, 0x02, 0x9c, 0x95, 0x3f, 0x5f, 0xfb, 0x8f, 0x05, 0xa4, 0xae, + 0x1a, 0x67, 0x73, 0x5b, 0xf6, 0xc6, 0x77, 0x4b, 0xde, 0xbe, 0x17, 0xd6, 0xb0, 0x6e, 0x5b, 0x27, 0x89, 0xd7, 0x6e, + 0x31, 0x2b, 0x2d, 0xe4, 0x33, 0x72, 0xc9, 0x4c, 0x22, 0xe4, 0x1a, 0xa1, 0xe1, 0x5a, 0xaf, 0xd0, 0x6d, 0xd7, 0x10, + 0xe6, 0x2a, 0x4c, 0x8f, 0x2d, 0x11, 0x1c, 0x54, 0xcd, 0xb7, 0xf5, 0xbf, 0x81, 0x1e, 0xfe, 0xd8, 0xec, 0x95, 0x05, + 0x53, 0x3c, 0xe9, 0xdc, 0xd7, 0xfa, 0xbb, 0x46, 0x3c, 0x4a, 0x4f, 0x1a, 0xa2, 0xe8, 0x11, 0x09, 0xf8, 0x5a, 0xc5, + 0xa0, 0x97, 0x15, 0xf7, 0x50, 0xa1, 0x4f, 0x5b, 0x98, 0xa3, 0xc2, 0x55, 0xaf, 0xc8, 0x93, 0x11, 0xfa, 0x4c, 0xad, + 0x0f, 0x84, 0x5c, 0x14, 0xef, 0x7d, 0xd2, 0x7a, 0xbb, 0x3e, 0x5f, 0xe4, 0x0e, 0xe9, 0xdd, 0xdb, 0x84, 0xe9, 0xa5, + 0x43, 0x37, 0xb6, 0xf1, 0x4f, 0xc4, 0xb3, 0x8d, 0xe1, 0x42, 0x95, 0xa5, 0x78, 0x5a, 0x8e, 0x52, 0xdd, 0xd1, 0x98, + 0x24, 0x15, 0xc8, 0xde, 0xd9, 0x76, 0x58, 0x73, 0xe1, 0xab, 0xec, 0xea, 0xd8, 0x03, 0x95, 0xb8, 0x87, 0xe4, 0x0e, + 0xfb, 0xb6, 0xbf, 0xcc, 0x54, 0xa6, 0x21, 0xfe, 0xc7, 0xf7, 0xdc, 0x81, 0x46, 0x7f, 0x3b, 0x8e, 0xe8, 0x58, 0x72, + 0x8b, 0x65, 0xca, 0x70, 0xe4, 0x04, 0x8b, 0xed, 0xde, 0x70, 0xca, 0xb9, 0xec, 0xb4, 0x45, 0x31, 0x4c, 0x72, 0x0f, + 0x8c, 0x6c, 0x45, 0xfb, 0x27, 0xf6, 0x44, 0xc3, 0x9c, 0x9e, 0x9a, 0x77, 0x96, 0xf8, 0x36, 0xed, 0x9f, 0xa8, 0x5d, + 0x42, 0x15, 0xa5, 0xc8, 0x4a, 0xdc, 0xe5, 0x97, 0x76, 0x9b, 0x08, 0xdb, 0x45, 0x98, 0xd6, 0x5e, 0x4f, 0x52, 0x39, + 0xd2, 0x28, 0x75, 0xec, 0xf0, 0xb6, 0x93, 0xa6, 0x02, 0x22, 0x54, 0x54, 0x4f, 0x4a, 0x5a, 0x4a, 0x5f, 0x88, 0x5a, + 0x77, 0x3e, 0xda, 0x8a, 0xf6, 0x04, 0x1c, 0xc0, 0xa6, 0xd5, 0x16, 0x95, 0xca, 0xc3, 0x0d, 0x3b, 0x04, 0xed, 0x2b, + 0x78, 0xf9, 0x00, 0x47, 0x55, 0x9e, 0xde, 0x17, 0xa4, 0xe2, 0xc7, 0x29, 0x36, 0x1e, 0x66, 0x93, 0xa1, 0x12, 0xb8, + 0x31, 0x4a, 0x87, 0xcf, 0xd7, 0xef, 0x74, 0x98, 0xbc, 0xfa, 0xb8, 0xa7, 0x17, 0xd3, 0x2b, 0x20, 0x5e, 0xb8, 0x79, + 0x7f, 0x1c, 0x26, 0xd7, 0x70, 0x82, 0xf4, 0x49, 0xaa, 0xb7, 0x6d, 0x19, 0x03, 0x0a, 0xcb, 0xbe, 0x9c, 0xc6, 0x5e, + 0x4c, 0x7c, 0x9e, 0xbf, 0x4b, 0x1b, 0xd3, 0xb2, 0xc2, 0x58, 0x7b, 0x75, 0xdb, 0x21, 0x5c, 0xe6, 0x0e, 0x7e, 0xf9, + 0x3f, 0x7c, 0xd4, 0x76, 0x73, 0xbf, 0x6e, 0xce, 0x8c, 0x00, 0xcf, 0x48, 0x88, 0xbe, 0x3c, 0x90, 0x2b, 0xd7, 0xaf, + 0xfe, 0x37, 0x50, 0xfc, 0xa4, 0x2b, 0xcd, 0xbf, 0xe6, 0xfa, 0xb0, 0x18, 0x9b, 0x82, 0x6c, 0x1f, 0x49, 0x61, 0x74, + 0x8d, 0x68, 0xbc, 0xdf, 0xb7, 0x61, 0x5d, 0x0d, 0x32, 0x72, 0x8b, 0x90, 0xd7, 0x87, 0x58, 0x60, 0xf4, 0xfd, 0x65, + 0xdb, 0xe2, 0x9b, 0x56, 0x24, 0xde, 0x30, 0xab, 0xb4, 0xfe, 0x17, 0x59, 0xb8, 0xbe, 0xfb, 0xd2, 0x80, 0x80, 0xd6, + 0xda, 0x57, 0xc2, 0x72, 0xe7, 0x08, 0x02, 0x18, 0x94, 0x30, 0x16, 0x4f, 0x22, 0xfa, 0x97, 0xcc, 0x88, 0xd4, 0x53, + 0xc5, 0x74, 0xe2, 0x84, 0xe1, 0xac, 0x04, 0x35, 0x56, 0x7a, 0x80, 0xcd, 0x5c, 0x94, 0xab, 0x61, 0x2b, 0xc6, 0x43, + 0x8a, 0xb8, 0x63, 0xd6, 0xc8, 0x7b, 0x42, 0x25, 0x0d, 0xaa, 0x88, 0x0a, 0x29, 0x8f, 0x42, 0x1c, 0x86, 0x67, 0x10, + 0x02, 0xa4, 0x52, 0xc4, 0x3a, 0x73, 0x49, 0x86, 0x71, 0xe0, 0xb5, 0x53, 0xc9, 0xab, 0xd1, 0xdd, 0x2a, 0x74, 0x0a, + 0x22, 0x3a, 0x30, 0xbb, 0x05, 0x5d, 0x6f, 0x16, 0xb0, 0x5b, 0x31, 0xb5, 0x52, 0xdc, 0xcd, 0x98, 0xad, 0x58, 0x6c, + 0x61, 0x40, 0x24, 0xb4, 0x65, 0xfe, 0x1a, 0x1d, 0xf0, 0xa2, 0x8b, 0xa2, 0x27, 0xa5, 0xf1, 0xdf, 0x94, 0x7a, 0x5f, + 0x50, 0xc3, 0xc8, 0x82, 0x82, 0xeb, 0x6c, 0xdc, 0x4a, 0xfc, 0xf0, 0x96, 0x3a, 0xdc, 0x42, 0xf0, 0x55, 0x48, 0x37, + 0xd5, 0xc2, 0x5c, 0x61, 0x0f, 0xb2, 0xe5, 0xda, 0x72, 0xa3, 0xe3, 0xbb, 0x5e, 0xbb, 0xf0, 0xfc, 0xa5, 0x36, 0xcf, + 0x95, 0x53, 0x3c, 0x96, 0x82, 0x5c, 0xe2, 0xa9, 0x95, 0x75, 0x27, 0xf5, 0x61, 0x58, 0xd3, 0x51, 0x8d, 0x3b, 0xe3, + 0xc9, 0x13, 0x32, 0xc9, 0x97, 0x56, 0xea, 0x9c, 0x50, 0x47, 0xa0, 0xb6, 0x1e, 0x94, 0xa9, 0x5f, 0x8a, 0x2d, 0x60, + 0x1e, 0x1e, 0xf8, 0x8f, 0x61, 0x91, 0x3c, 0x99, 0x44, 0x4e, 0x13, 0x4f, 0xe5, 0xf8, 0x15, 0x9f, 0x33, 0x9e, 0x0c, + 0x27, 0x7b, 0x2c, 0x49, 0x7a, 0xb6, 0x8c, 0xf9, 0x61, 0x00, 0x88, 0x13, 0x61, 0xcc, 0x45, 0x1e, 0x51, 0x28, 0x5a, + 0x9c, 0x5c, 0x57, 0x40, 0x6a, 0xaa, 0x6d, 0xbf, 0xa6, 0xe8, 0x08, 0xcc, 0xd2, 0x65, 0x1a, 0xd5, 0x2c, 0x55, 0x26, + 0x08, 0xe1, 0x73, 0x6e, 0xad, 0x1d, 0x17, 0x30, 0xd3, 0x8e, 0x9e, 0xdb, 0xe4, 0x75, 0xf6, 0x47, 0x46, 0x66, 0xea, + 0xce, 0xaa, 0xc6, 0x04, 0x63, 0x57, 0xed, 0xd2, 0x50, 0x79, 0xe3, 0x64, 0xf7, 0xd5, 0xa9, 0xdd, 0x86, 0x32, 0xb8, + 0x88, 0x89, 0x87, 0x6c, 0x04, 0x20, 0xba, 0x96, 0xab, 0x95, 0x27, 0xc7, 0xc6, 0x10, 0xe6, 0xa6, 0x38, 0xcf, 0x81, + 0xb6, 0x7f, 0xdc, 0xb5, 0x50, 0x2b, 0x44, 0x56, 0x36, 0xfb, 0x67, 0x13, 0x78, 0xbd, 0x58, 0xbc, 0x08, 0x2f, 0xe6, + 0x41, 0x2a, 0x2f, 0x16, 0xbf, 0xb2, 0x94, 0x86, 0x14, 0x61, 0x2d, 0xb0, 0xb9, 0xb4, 0x92, 0x67, 0xcb, 0xe9, 0x85, + 0xeb, 0x99, 0xcc, 0xbc, 0x10, 0x30, 0x66, 0xe9, 0x57, 0x5e, 0xa2, 0xb3, 0x03, 0xfb, 0x9f, 0xfd, 0x86, 0x3a, 0x22, + 0x53, 0xb0, 0xe9, 0x36, 0x46, 0x6a, 0x91, 0xac, 0x24, 0xea, 0x47, 0x56, 0x3e, 0x7b, 0xd7, 0xea, 0xb7, 0xda, 0xb9, + 0x21, 0x50, 0xf8, 0xde, 0x88, 0x09, 0x0d, 0x2a, 0xb1, 0xa4, 0x6e, 0xdc, 0x07, 0xe7, 0x41, 0x59, 0xd3, 0xaf, 0x04, + 0x82, 0xff, 0xc4, 0x6e, 0xda, 0x25, 0x57, 0x90, 0x2e, 0x06, 0x77, 0x2a, 0x54, 0x37, 0x44, 0x78, 0x7d, 0x76, 0x2f, + 0xd1, 0xc4, 0x61, 0xb6, 0x22, 0x0b, 0x3d, 0xbc, 0xf6, 0xe0, 0xf6, 0x79, 0x66, 0x2d, 0xee, 0x54, 0x82, 0xf6, 0xb5, + 0xd9, 0xab, 0x7e, 0xf2, 0x78, 0xf0, 0xab, 0xc1, 0x73, 0x41, 0x06, 0x37, 0xbb, 0x41, 0xd4, 0x0f, 0xa1, 0xf3, 0x2c, + 0xf8, 0x1e, 0xc1, 0x94, 0xfe, 0x95, 0x17, 0xe2, 0x57, 0x83, 0x8f, 0x32, 0x33, 0xa8, 0x1e, 0xab, 0x08, 0x52, 0x7e, + 0x92, 0x61, 0x84, 0x91, 0x61, 0xe8, 0xba, 0x0a, 0x51, 0xc2, 0x1b, 0x2c, 0x36, 0xb3, 0x7b, 0x53, 0xf3, 0x7f, 0x81, + 0xd4, 0x21, 0xfc, 0x90, 0xd8, 0x13, 0xf3, 0x10, 0xf6, 0x6a, 0xe6, 0x71, 0xb6, 0xaf, 0xa2, 0x8e, 0xf5, 0x66, 0x8b, + 0x27, 0x16, 0x54, 0x1f, 0xc2, 0xda, 0x54, 0x81, 0x4b, 0xc4, 0xdc, 0xae, 0xfd, 0x7f, 0xfc, 0x75, 0xda, 0xb1, 0x8d, + 0x98, 0x99, 0x1e, 0x8e, 0xfb, 0xc6, 0x15, 0x51, 0x17, 0xa0, 0x60, 0x0e, 0x5a, 0x57, 0xb0, 0x12, 0x8f, 0xda, 0xd3, + 0xdb, 0xae, 0xbf, 0x1f, 0x20, 0xc4, 0x0f, 0xcd, 0xf2, 0xbe, 0x42, 0x6c, 0x34, 0x69, 0xbb, 0xb1, 0x73, 0x6c, 0xab, + 0x0e, 0x2b, 0x0a, 0x25, 0x74, 0x43, 0x03, 0xe7, 0x6e, 0x20, 0xc0, 0xfa, 0x29, 0xce, 0xa2, 0x5d, 0xd8, 0x43, 0xd7, + 0x6e, 0x6b, 0x3c, 0x35, 0x7a, 0x62, 0xa4, 0x95, 0x80, 0x2d, 0x53, 0xdf, 0x79, 0x45, 0x77, 0x9b, 0x1b, 0x76, 0xae, + 0xcf, 0x6d, 0xa9, 0xf6, 0xe3, 0x78, 0x6c, 0x1b, 0x66, 0x99, 0xda, 0xbd, 0xbb, 0x66, 0xae, 0x7e, 0xb9, 0xce, 0x54, + 0x84, 0x6c, 0x38, 0x85, 0xe4, 0x84, 0xe4, 0xb6, 0xd7, 0x92, 0x18, 0xc5, 0x7a, 0xc7, 0x06, 0x8e, 0x90, 0x73, 0xb6, + 0x62, 0x06, 0x6b, 0xb3, 0xdd, 0xc7, 0xc2, 0x64, 0xc3, 0x69, 0xed, 0x1e, 0x5a, 0x68, 0x04, 0x97, 0x8c, 0xe7, 0x2a, + 0x93, 0xc5, 0xe3, 0x0e, 0xf3, 0xcb, 0xf6, 0x19, 0x8d, 0x17, 0x0d, 0xa7, 0x1a, 0x7b, 0x53, 0x52, 0x46, 0xb3, 0xef, + 0xdc, 0xd2, 0x5a, 0x24, 0xde, 0xbc, 0xa7, 0x77, 0x82, 0xa1, 0xb5, 0xf7, 0xaa, 0x2d, 0x80, 0xfa, 0x9f, 0xed, 0xac, + 0x58, 0xd0, 0x38, 0xec, 0x0c, 0x70, 0xe3, 0xe2, 0x79, 0x87, 0xe2, 0x31, 0x99, 0xe9, 0xbd, 0x15, 0x59, 0xef, 0xf2, + 0xbf, 0xda, 0xae, 0x13, 0x9f, 0x3d, 0xba, 0xdb, 0xea, 0xa0, 0xb5, 0xae, 0x8b, 0x94, 0xf8, 0x38, 0xad, 0x5d, 0x4c, + 0xdc, 0x92, 0x85, 0x97, 0x39, 0x9a, 0xff, 0x15, 0x8b, 0x1c, 0x36, 0x68, 0x9c, 0x9b, 0xf8, 0xd6, 0x52, 0x1a, 0x7d, + 0x6a, 0x50, 0x17, 0x26, 0x51, 0x89, 0x20, 0xb4, 0xd2, 0xbf, 0x62, 0xef, 0x6b, 0x6f, 0x33, 0x15, 0xd7, 0x29, 0xce, + 0x60, 0xf2, 0xa8, 0xe7, 0x1c, 0x49, 0xc7, 0x2c, 0x6b, 0x7c, 0x03, 0x4d, 0xdb, 0x4a, 0xd3, 0x64, 0x54, 0xc3, 0x46, + 0xac, 0x33, 0x1b, 0xf1, 0xc2, 0x48, 0xd3, 0xb6, 0x2b, 0xa1, 0xd3, 0xa9, 0xfa, 0xc5, 0x13, 0xe7, 0xd6, 0xc2, 0x7f, + 0xcb, 0x8b, 0x03, 0xc4, 0xb9, 0xae, 0x46, 0x1a, 0x19, 0x74, 0xe1, 0x2e, 0x3e, 0xe5, 0x8e, 0x5b, 0x39, 0x86, 0x60, + 0xd5, 0x6a, 0xe3, 0xe2, 0x50, 0xd6, 0xd7, 0x20, 0xf5, 0x3e, 0x18, 0x69, 0x32, 0x66, 0x57, 0xce, 0x9f, 0xe6, 0xe9, + 0xa1, 0x44, 0x99, 0x1a, 0x99, 0x36, 0x7c, 0xcf, 0xaf, 0xe6, 0x24, 0x76, 0x6d, 0x3c, 0x1f, 0x9c, 0x98, 0x7a, 0x2b, + 0x67, 0x25, 0x45, 0x01, 0xd0, 0x86, 0xb9, 0xb6, 0x64, 0x23, 0x65, 0xda, 0xb3, 0xce, 0xfb, 0x76, 0xe7, 0x8a, 0x93, + 0xd9, 0x69, 0x02, 0x5d, 0xa1, 0xa9, 0xea, 0xd4, 0x0c, 0x8d, 0x10, 0x98, 0xf1, 0x61, 0x0a, 0xfd, 0xa2, 0x48, 0x30, + 0x74, 0xd3, 0x0b, 0x8a, 0x15, 0x27, 0x9a, 0xe7, 0x4b, 0x5d, 0x25, 0xe1, 0xa6, 0xf6, 0x7e, 0xed, 0xfe, 0x97, 0x9e, + 0xdc, 0x45, 0x9d, 0x09, 0x41, 0x29, 0x60, 0xd2, 0x71, 0xf0, 0x61, 0x28, 0xc3, 0x1f, 0x57, 0x30, 0x7a, 0x91, 0x59, + 0x7f, 0x20, 0x92, 0x43, 0xc5, 0x77, 0x96, 0x5f, 0x5a, 0xa1, 0xf8, 0x89, 0xc8, 0x0e, 0x8a, 0xaf, 0x41, 0xc0, 0x23, + 0xa8, 0xd9, 0x4e, 0x57, 0x82, 0x27, 0x78, 0xc7, 0x8b, 0x7c, 0xc5, 0xc8, 0xeb, 0x69, 0xb5, 0xa4, 0x61, 0x68, 0x8e, + 0x25, 0x41, 0x63, 0x53, 0xc7, 0x12, 0x82, 0x79, 0x5f, 0x1f, 0xeb, 0xb9, 0xd5, 0x8e, 0x02, 0x27, 0x58, 0xfb, 0x81, + 0xb4, 0x8e, 0x74, 0x3c, 0xb5, 0x68, 0xd6, 0x36, 0x32, 0xd1, 0xd9, 0xc4, 0x40, 0x3a, 0x0b, 0x0e, 0x36, 0xe6, 0xd3, + 0x68, 0xae, 0xbc, 0x61, 0x04, 0xff, 0xbd, 0x0a, 0xcb, 0x59, 0x7a, 0xb5, 0xe5, 0x62, 0x1c, 0x55, 0xf8, 0x3f, 0x0d, + 0x13, 0xbe, 0xc9, 0xf9, 0xb8, 0x5c, 0x24, 0x44, 0xa8, 0x80, 0x07, 0x3a, 0x26, 0x7c, 0x1d, 0xad, 0x86, 0x11, 0x5a, + 0x75, 0x2b, 0xc8, 0x11, 0xd2, 0x7e, 0xdf, 0x54, 0x5b, 0xdf, 0x34, 0x67, 0x6f, 0xcf, 0x0d, 0x9b, 0x06, 0xf3, 0xe3, + 0x73, 0x8f, 0x4d, 0x37, 0x12, 0x55, 0x2c, 0xbf, 0x83, 0x8f, 0xda, 0x98, 0xe1, 0x83, 0xfe, 0xf0, 0xa6, 0x71, 0xcc, + 0x78, 0x95, 0x4d, 0x9a, 0xf4, 0xc3, 0x99, 0x6b, 0x81, 0xda, 0xa7, 0xa6, 0xee, 0x48, 0xd1, 0x81, 0xa3, 0xab, 0xf9, + 0x16, 0x5f, 0x89, 0xf0, 0xf0, 0x6b, 0x12, 0x95, 0x35, 0xcd, 0xa0, 0x4e, 0xa5, 0x34, 0x51, 0x75, 0xdb, 0x54, 0x00, + 0x7b, 0xcf, 0xb0, 0x32, 0x50, 0xa3, 0x27, 0xba, 0x13, 0x34, 0x42, 0x1a, 0xc7, 0x9f, 0x42, 0xfb, 0x91, 0xc6, 0x6f, + 0xc5, 0x94, 0x63, 0x3b, 0x86, 0x79, 0xd5, 0x00, 0x55, 0x0b, 0x7d, 0xfc, 0xeb, 0x9b, 0xad, 0xdb, 0xb5, 0xed, 0x76, + 0x87, 0xb0, 0x54, 0x2f, 0x8f, 0x5a, 0x34, 0x93, 0x98, 0xa6, 0x14, 0x16, 0x5d, 0xb4, 0x8e, 0x97, 0xd3, 0xc6, 0x41, + 0xad, 0x30, 0xd8, 0x16, 0xaa, 0x74, 0x19, 0x31, 0xdc, 0x4e, 0x61, 0x84, 0x4c, 0xa1, 0x42, 0x1f, 0xb1, 0x66, 0xba, + 0x75, 0xf7, 0x50, 0x5a, 0xcb, 0xf2, 0xad, 0x17, 0x6b, 0xd4, 0xb7, 0xde, 0x66, 0x35, 0x8a, 0x5a, 0x4c, 0xbc, 0x12, + 0x8c, 0xae, 0x2f, 0x13, 0x5a, 0xb9, 0x45, 0x5b, 0xa5, 0x20, 0x48, 0xec, 0xd6, 0xe2, 0x2b, 0xd1, 0x8e, 0xcd, 0x1c, + 0x89, 0xc9, 0xfc, 0xf4, 0xda, 0x54, 0x86, 0xca, 0x87, 0x0f, 0x3e, 0x67, 0x68, 0x8a, 0x27, 0xef, 0xc0, 0x4f, 0xba, + 0xfc, 0x49, 0xea, 0x03, 0xef, 0xb6, 0x0c, 0x4e, 0x51, 0x3b, 0xb7, 0x74, 0x18, 0xc0, 0x75, 0x52, 0xf0, 0x82, 0x2b, + 0x4c, 0x92, 0x46, 0x3e, 0x3a, 0x41, 0x4c, 0x8a, 0xce, 0x94, 0x35, 0x18, 0x94, 0xb5, 0x0c, 0x80, 0x35, 0xda, 0x84, + 0xe1, 0x23, 0x90, 0x19, 0x63, 0x06, 0x69, 0x1b, 0xe6, 0x94, 0xcf, 0xba, 0x3f, 0xbe, 0x10, 0xba, 0x3d, 0xd8, 0x13, + 0x51, 0x96, 0x0f, 0xc8, 0x07, 0x1d, 0xd2, 0xbf, 0x22, 0x31, 0xca, 0xe1, 0xb9, 0xdc, 0x7f, 0x12, 0x58, 0x38, 0x80, + 0x9b, 0xb5, 0x77, 0xec, 0x80, 0x64, 0xde, 0x2a, 0x2c, 0xbf, 0x1f, 0x02, 0x0c, 0x5b, 0x3b, 0xb1, 0x9c, 0x15, 0xa3, + 0x65, 0x39, 0x59, 0x41, 0xc3, 0xf2, 0x37, 0x80, 0xaf, 0x03, 0x56, 0xbd, 0x5f, 0xe2, 0x32, 0x53, 0x14, 0xf8, 0x67, + 0xe3, 0xb4, 0x4a, 0x5b, 0x10, 0x1f, 0x04, 0x22, 0x0f, 0xb0, 0x07, 0x57, 0x8f, 0x85, 0xb7, 0x53, 0xbe, 0x8b, 0xca, + 0xd2, 0x35, 0x1a, 0x39, 0xa5, 0x7a, 0xbf, 0xc5, 0x76, 0x83, 0x3d, 0x08, 0xa9, 0x2d, 0x94, 0x7f, 0x85, 0xaa, 0x4a, + 0x51, 0xeb, 0xcd, 0x08, 0x83, 0x16, 0x9c, 0x9b, 0x23, 0x50, 0x43, 0x60, 0xd4, 0xda, 0x5c, 0x4b, 0xa0, 0x35, 0x3f, + 0x80, 0x5d, 0xe7, 0xe3, 0x97, 0x51, 0x4c, 0x78, 0xbc, 0x6f, 0x1a, 0x93, 0x93, 0x1f, 0x3d, 0xee, 0xfa, 0x66, 0xdd, + 0x64, 0x88, 0x59, 0x24, 0xf5, 0x3c, 0xc2, 0x6c, 0xe7, 0xb5, 0x70, 0xb1, 0x3a, 0x41, 0xcf, 0xe5, 0x8a, 0x14, 0xf7, + 0xa8, 0xbb, 0x65, 0xf7, 0x7c, 0xaa, 0x9e, 0xc4, 0x58, 0x4b, 0x11, 0x3f, 0xc5, 0xb5, 0x99, 0x50, 0xa5, 0xc8, 0xcd, + 0x26, 0xb0, 0x95, 0x23, 0xed, 0xf1, 0x48, 0x96, 0x13, 0x75, 0xac, 0x41, 0xd4, 0x3c, 0xbe, 0xb3, 0x72, 0xe8, 0x46, + 0x77, 0xd8, 0x37, 0xff, 0x1f, 0xbb, 0xe9, 0xe9, 0x38, 0x93, 0x65, 0xf0, 0x32, 0x06, 0x67, 0xbc, 0xf3, 0xc2, 0xb4, + 0x4a, 0x45, 0x8c, 0x46, 0x3f, 0x16, 0x7d, 0x7f, 0xaa, 0x77, 0x5d, 0x82, 0x20, 0xd5, 0xe5, 0xbf, 0x81, 0xa3, 0xba, + 0x3a, 0x5c, 0x7a, 0x7a, 0xe6, 0x96, 0x46, 0x97, 0xef, 0x98, 0xc1, 0x5d, 0x05, 0x13, 0x60, 0x0d, 0xbc, 0x45, 0xef, + 0xdc, 0x12, 0xc2, 0x65, 0xd4, 0xbb, 0xee, 0x95, 0x53, 0x28, 0x3a, 0x47, 0x77, 0x83, 0x84, 0x1a, 0xae, 0xf3, 0xdc, + 0x3e, 0x5a, 0x29, 0x2a, 0x1f, 0xe7, 0xc3, 0x85, 0xb3, 0x44, 0x12, 0x05, 0xc7, 0x4b, 0x08, 0xd7, 0x7d, 0x3b, 0x66, + 0x84, 0x91, 0x6d, 0x4b, 0xa5, 0xba, 0xe1, 0x5d, 0xe8, 0x51, 0xcc, 0x5a, 0x36, 0xe0, 0xfc, 0x7f, 0xe9, 0xf5, 0x48, + 0xba, 0xb7, 0x29, 0xf1, 0xb8, 0xf0, 0xef, 0xe2, 0xc8, 0x29, 0x28, 0x89, 0x4a, 0xb4, 0x7d, 0x57, 0x76, 0xe0, 0x78, + 0x68, 0x0f, 0xe9, 0xb4, 0x29, 0xcb, 0x2a, 0x00, 0xad, 0x7d, 0xe6, 0x65, 0xe4, 0x64, 0xf4, 0xa4, 0xbd, 0x43, 0xd1, + 0x1b, 0x54, 0x26, 0x21, 0x87, 0x41, 0x22, 0xe6, 0x3a, 0xe0, 0xee, 0xaa, 0xdb, 0x5d, 0x73, 0x15, 0xba, 0x6b, 0x76, + 0xe5, 0x80, 0x8e, 0xe4, 0x90, 0x64, 0xe6, 0xac, 0xf6, 0x41, 0x11, 0x45, 0xde, 0x23, 0xf6, 0xc5, 0x9d, 0x4a, 0xba, + 0x99, 0x77, 0x51, 0x48, 0x14, 0x10, 0xc6, 0x29, 0x88, 0xf7, 0x04, 0x08, 0xa5, 0x75, 0x77, 0xd4, 0x26, 0x5c, 0xf5, + 0x4c, 0x5b, 0x19, 0xc3, 0x9d, 0xce, 0x9d, 0x91, 0x5d, 0xe0, 0x52, 0xf7, 0x62, 0x08, 0xa2, 0x40, 0x4e, 0x41, 0x0c, + 0x27, 0x41, 0xf1, 0xa1, 0x38, 0x90, 0x80, 0x43, 0xe4, 0x41, 0xa9, 0x71, 0xc9, 0xdc, 0x78, 0xa3, 0x10, 0x62, 0x31, + 0x12, 0x31, 0x21, 0xd9, 0x30, 0x70, 0x4c, 0x05, 0xda, 0xfd, 0x72, 0xdf, 0x7b, 0xe1, 0xf7, 0x43, 0x4d, 0x2d, 0xe6, + 0x42, 0x16, 0x46, 0xab, 0x93, 0x7b, 0x81, 0x63, 0xbe, 0x57, 0x2f, 0xb7, 0x91, 0xbd, 0xf0, 0x8d, 0x4b, 0x72, 0x95, + 0x12, 0x10, 0xf6, 0x1f, 0x8c, 0x03, 0x01, 0x30, 0x97, 0x56, 0xb5, 0x96, 0xc8, 0xc3, 0x1b, 0x69, 0xd6, 0xb4, 0x14, + 0xeb, 0x66, 0x1e, 0x2a, 0xc0, 0x92, 0x5a, 0xdc, 0x30, 0x97, 0x15, 0xce, 0x68, 0x0e, 0x4a, 0x78, 0xd3, 0x42, 0xd7, + 0xe6, 0x73, 0x78, 0x92, 0xe6, 0xe8, 0xf7, 0xf0, 0x56, 0x75, 0xcb, 0x92, 0xea, 0x4c, 0x32, 0x98, 0xc8, 0x54, 0xea, + 0x69, 0x38, 0xee, 0xa4, 0xef, 0x04, 0x63, 0xb2, 0xd0, 0x78, 0x27, 0xeb, 0x6c, 0xec, 0x0c, 0x7d, 0x60, 0x7f, 0xc0, + 0x05, 0xc5, 0x77, 0x49, 0xc7, 0xb7, 0x49, 0x84, 0x45, 0x56, 0x76, 0xed, 0xf2, 0xd2, 0xf7, 0x5d, 0x6f, 0xe6, 0xa5, + 0xfb, 0xec, 0xbb, 0xdf, 0xbd, 0x25, 0x6b, 0x45, 0xc9, 0x49, 0xf2, 0x84, 0xe5, 0x6d, 0xda, 0x1e, 0xf2, 0x74, 0x60, + 0xc8, 0xdc, 0x38, 0xae, 0x7f, 0x51, 0x8c, 0x34, 0x75, 0xd8, 0x51, 0x7a, 0x53, 0x81, 0xa7, 0xf6, 0x39, 0x8b, 0x0e, + 0x14, 0xcf, 0x30, 0x5d, 0x13, 0xe1, 0xcd, 0xfe, 0xc5, 0xfc, 0xdf, 0x03, 0xa2, 0xe3, 0xc3, 0x98, 0x36, 0xe4, 0xc3, + 0x2a, 0xbc, 0x14, 0xc7, 0xe2, 0x07, 0x8b, 0x49, 0xe4, 0x49, 0x1c, 0xe0, 0x7d, 0x60, 0x91, 0x0a, 0x23, 0x83, 0x3a, + 0x56, 0x76, 0xc7, 0xf1, 0x02, 0x30, 0xe2, 0x21, 0xe3, 0xfc, 0xe2, 0x33, 0x10, 0x38, 0x5e, 0xa8, 0x66, 0x3b, 0x87, + 0x15, 0x08, 0x80, 0x8c, 0x59, 0xa9, 0xb8, 0x18, 0xcd, 0xa2, 0x14, 0x83, 0x67, 0x7c, 0x68, 0x57, 0x0d, 0xb1, 0xcc, + 0xfc, 0x60, 0x50, 0xce, 0xad, 0x85, 0x14, 0xdc, 0xae, 0x2f, 0x8c, 0x09, 0x6e, 0xdb, 0x48, 0xb0, 0x45, 0xfd, 0x18, + 0x10, 0x8b, 0x0b, 0xea, 0x1a, 0xbf, 0xd7, 0x99, 0x3b, 0x69, 0x9f, 0xb8, 0x8e, 0xd2, 0xb2, 0x94, 0xc4, 0x75, 0x1e, + 0x46, 0x02, 0xc1, 0xf4, 0x9a, 0x10, 0x95, 0x18, 0x62, 0x1f, 0xcb, 0xbd, 0x01, 0xf0, 0x18, 0xa2, 0x23, 0xc7, 0xec, + 0xbc, 0x43, 0x78, 0xba, 0x81, 0x5f, 0x16, 0xbf, 0x95, 0xf1, 0xeb, 0xe3, 0x51, 0x76, 0x44, 0x3e, 0xbc, 0x91, 0xb8, + 0x53, 0x31, 0x07, 0xd2, 0xc8, 0x15, 0xb0, 0xb4, 0x05, 0x72, 0x91, 0x71, 0x0c, 0x5b, 0x3f, 0xb5, 0x3e, 0x06, 0x3f, + 0xf6, 0xb1, 0xe8, 0xf8, 0x75, 0xa0, 0xaf, 0x52, 0x22, 0x7f, 0x2b, 0xa5, 0x38, 0x7b, 0x6f, 0x46, 0xbb, 0x3b, 0x71, + 0x53, 0xaf, 0xec, 0x6d, 0x43, 0x7d, 0x93, 0xb8, 0x7d, 0x6b, 0x1e, 0x03, 0xee, 0xeb, 0xc4, 0x8d, 0xa1, 0xd0, 0x27, + 0xcb, 0xe3, 0x46, 0x53, 0x13, 0x43, 0x77, 0x1e, 0xe1, 0x57, 0xa7, 0x3d, 0x9c, 0xdd, 0x97, 0x26, 0xdd, 0x08, 0xb3, + 0xb8, 0xd8, 0x25, 0x19, 0x1c, 0x06, 0x2c, 0x0e, 0x45, 0x8a, 0x16, 0xb9, 0x6c, 0x0c, 0x91, 0xc3, 0x0e, 0xee, 0x26, + 0x8d, 0x53, 0xde, 0x31, 0x78, 0x69, 0x52, 0x9f, 0xb7, 0xd5, 0x62, 0x42, 0x4d, 0x98, 0x6a, 0xf0, 0xd6, 0xb6, 0x7c, + 0x2c, 0x94, 0x72, 0x12, 0x48, 0xa7, 0x2c, 0x54, 0x0a, 0x7e, 0xe2, 0x0f, 0xf7, 0x7f, 0x50, 0x94, 0x3b, 0x02, 0x6e, + 0x05, 0x1d, 0xfe, 0x7c, 0x10, 0x2f, 0x63, 0x88, 0x47, 0x46, 0xc6, 0xf4, 0x2f, 0x29, 0xab, 0x7e, 0x05, 0x99, 0x98, + 0xaf, 0xb3, 0x07, 0xb9, 0x1a, 0xdf, 0xa9, 0xb5, 0x30, 0xae, 0x23, 0x0d, 0x4d, 0xcc, 0x4f, 0xa1, 0xb0, 0xe9, 0x2a, + 0x03, 0x0b, 0xa5, 0x0c, 0xf9, 0xbe, 0xd4, 0xed, 0xa3, 0xe1, 0x27, 0xa1, 0xe7, 0xd3, 0x0c, 0x93, 0x90, 0x16, 0x40, + 0xf5, 0xe1, 0x68, 0xd2, 0x0d, 0x76, 0xf3, 0x51, 0x07, 0x2a, 0x9d, 0x1d, 0x73, 0x4a, 0x90, 0xf3, 0xfc, 0x64, 0x1b, + 0x7b, 0xc7, 0x5f, 0x1b, 0x7f, 0x83, 0xc0, 0x67, 0xfe, 0xa3, 0x37, 0x55, 0x1b, 0x88, 0xf5, 0x72, 0x46, 0xd0, 0xb6, + 0x0c, 0xb8, 0xa5, 0xca, 0xa1, 0xd9, 0x52, 0x31, 0x2c, 0xcc, 0xd4, 0xc2, 0x14, 0x2f, 0x3a, 0x41, 0xee, 0x0f, 0x21, + 0x16, 0x28, 0x37, 0x20, 0x65, 0xc9, 0x31, 0x1d, 0x44, 0x8a, 0xde, 0x06, 0x0a, 0x22, 0x94, 0x5f, 0xbf, 0xd4, 0xff, + 0x45, 0x04, 0x58, 0x8e, 0xb4, 0xca, 0x40, 0x32, 0xb5, 0xb1, 0x9c, 0xd4, 0xe2, 0x54, 0x9c, 0x55, 0xca, 0x30, 0xf9, + 0xdd, 0x78, 0xd9, 0x9a, 0xa0, 0x66, 0x08, 0x4f, 0xc9, 0xc1, 0x1a, 0x4d, 0x4c, 0x4f, 0x99, 0xfd, 0x05, 0x17, 0xa2, + 0x41, 0x7e, 0x23, 0xb8, 0x75, 0x2c, 0x6d, 0x14, 0x78, 0xd4, 0xbe, 0x89, 0x15, 0x95, 0x56, 0xe1, 0x9c, 0xa8, 0x99, + 0x6c, 0xcb, 0x5e, 0xee, 0xca, 0x3d, 0x06, 0x2e, 0x33, 0x23, 0xd0, 0x4b, 0xeb, 0x7b, 0xef, 0x00, 0xff, 0xd1, 0xa2, + 0xc8, 0x0d, 0xdb, 0x22, 0x85, 0x8c, 0x6d, 0xbd, 0xf1, 0x5b, 0x7d, 0x8a, 0x83, 0x3c, 0xf6, 0x42, 0x2b, 0x3b, 0xe1, + 0x9d, 0xef, 0x4e, 0x19, 0xe6, 0x45, 0x1c, 0xe7, 0x59, 0x54, 0xe8, 0xc3, 0xa2, 0xaa, 0x44, 0xff, 0x09, 0x00, 0x46, + 0xee, 0x72, 0x2a, 0xfc, 0x5b, 0xc2, 0x6d, 0x7c, 0xd0, 0x4e, 0x0d, 0xe7, 0x73, 0xaa, 0xcf, 0xbb, 0xee, 0x3b, 0xfc, + 0x30, 0x7c, 0xad, 0x71, 0x44, 0x05, 0xa6, 0x69, 0x9e, 0x98, 0xad, 0xe1, 0x77, 0x0a, 0xf8, 0xfe, 0xa1, 0x14, 0xdb, + 0xb0, 0x99, 0x56, 0xed, 0xcd, 0xbc, 0xde, 0xc1, 0x67, 0xce, 0x6a, 0x96, 0xaf, 0x3f, 0xf8, 0x3e, 0xa1, 0x2c, 0xc2, + 0x6f, 0xcb, 0x44, 0x3d, 0xe2, 0x2c, 0x1d, 0x5c, 0xc0, 0xe3, 0x1e, 0xc9, 0xd0, 0xf3, 0x75, 0x36, 0x22, 0x7f, 0xb4, + 0x71, 0x01, 0x69, 0xab, 0x09, 0x25, 0xea, 0x44, 0x8f, 0x48, 0xca, 0x58, 0x58, 0x68, 0x5b, 0x1d, 0x90, 0x45, 0xc1, + 0x72, 0x1b, 0x38, 0x4f, 0x4c, 0x11, 0x0e, 0xdf, 0xb5, 0xa7, 0x8b, 0xa8, 0xff, 0x31, 0x03, 0xf8, 0x0f, 0x98, 0x18, + 0x15, 0xca, 0xff, 0x0e, 0xc3, 0x1f, 0x84, 0x11, 0x71, 0x3a, 0x31, 0x3b, 0x30, 0x60, 0xe4, 0x45, 0x65, 0x46, 0x52, + 0x62, 0xad, 0x95, 0x3c, 0xf8, 0x3e, 0x14, 0x8d, 0xeb, 0x1a, 0x84, 0x60, 0x83, 0x69, 0x05, 0xf1, 0x70, 0x1a, 0x51, + 0xd6, 0x78, 0x34, 0x7e, 0x4f, 0xa5, 0x26, 0xf4, 0xf8, 0x36, 0x4a, 0x16, 0x8f, 0xaa, 0x27, 0xca, 0x47, 0x12, 0x43, + 0xda, 0xc8, 0x49, 0xf1, 0x26, 0xe3, 0xfd, 0xb4, 0x31, 0x22, 0x39, 0x39, 0x9d, 0x1d, 0x91, 0xf2, 0x0b, 0x19, 0x66, + 0xd7, 0x7f, 0xf1, 0xf2, 0x8b, 0x2f, 0xbe, 0x96, 0x4a, 0x54, 0xd7, 0x22, 0x86, 0x6e, 0xd7, 0xd1, 0xfb, 0xae, 0x84, + 0x21, 0x1d, 0x52, 0x1e, 0x14, 0x12, 0x53, 0x59, 0x20, 0x0d, 0xf9, 0x49, 0x54, 0xfe, 0x1e, 0xe6, 0xb3, 0x77, 0xaf, + 0x52, 0x97, 0xa4, 0xac, 0x24, 0x2e, 0x0f, 0x58, 0x9a, 0x4c, 0xbc, 0x39, 0x0f, 0xbb, 0x3f, 0x27, 0x6f, 0xfe, 0xaf, + 0x28, 0x63, 0xaa, 0x29, 0x47, 0x16, 0xea, 0xa0, 0x94, 0xd5, 0x70, 0xda, 0xe2, 0x8b, 0x20, 0xda, 0x2a, 0x74, 0xa9, + 0x79, 0xe0, 0xb2, 0xb0, 0x26, 0x82, 0x2d, 0xe8, 0xe9, 0x30, 0xb2, 0x25, 0xb5, 0x89, 0x4d, 0xaf, 0x23, 0xcf, 0xf2, + 0xa9, 0xda, 0x5d, 0xea, 0x63, 0xef, 0xa0, 0x1e, 0x8b, 0xab, 0xfd, 0xd4, 0x64, 0x1a, 0x70, 0x81, 0xa0, 0x7e, 0x05, + 0xb9, 0x55, 0x8c, 0xb8, 0xd1, 0xcd, 0xfd, 0x63, 0xb5, 0x75, 0x2b, 0xff, 0xb4, 0x0b, 0x22, 0x23, 0x81, 0x81, 0x66, + 0xd1, 0x6a, 0x42, 0x3f, 0x36, 0x2c, 0x85, 0x21, 0x67, 0x4b, 0x66, 0x39, 0xaf, 0x0a, 0xda, 0x95, 0xb6, 0x82, 0x03, + 0x12, 0x46, 0xeb, 0x18, 0x33, 0x83, 0xcf, 0xa1, 0x20, 0x5f, 0xb5, 0xc9, 0x05, 0xfb, 0xe2, 0x9e, 0x26, 0x98, 0x0a, + 0xc2, 0xbc, 0x52, 0x30, 0x9d, 0xf5, 0xcd, 0xc2, 0x1c, 0x0b, 0x85, 0xfc, 0xf8, 0x0b, 0x8a, 0x83, 0xa9, 0x40, 0x17, + 0xf9, 0x2b, 0x0d, 0xdb, 0xce, 0x2c, 0xfa, 0xee, 0x83, 0x02, 0xbc, 0x51, 0x47, 0xe6, 0x25, 0x8b, 0xbf, 0x7a, 0xe7, + 0xe3, 0xe4, 0x1b, 0x2d, 0xb2, 0x8b, 0x89, 0xfe, 0x52, 0x49, 0x33, 0xbf, 0x2e, 0xf5, 0x50, 0xb6, 0xa7, 0x3c, 0xae, + 0x98, 0xe6, 0x3d, 0x4a, 0x7f, 0x1a, 0xf3, 0x84, 0x4c, 0x68, 0x2f, 0xa7, 0xbf, 0x25, 0x6a, 0x76, 0x9f, 0x59, 0xaa, + 0xfe, 0x0d, 0x2f, 0x95, 0x26, 0xe5, 0x58, 0xc6, 0xb4, 0x9e, 0x12, 0xeb, 0x96, 0x05, 0x0c, 0xb2, 0x28, 0x4e, 0x6c, + 0xb4, 0xd9, 0x3b, 0xa2, 0xf9, 0x4e, 0xed, 0x65, 0x72, 0xc2, 0xc2, 0x5c, 0x5d, 0xc9, 0x76, 0x1a, 0x61, 0xb7, 0xde, + 0x13, 0xa9, 0x21, 0x68, 0x46, 0xc9, 0xae, 0x76, 0x7b, 0x41, 0xc3, 0xc4, 0x9a, 0x49, 0x91, 0x2d, 0x9a, 0xe5, 0x4e, + 0xd0, 0x43, 0x3e, 0x95, 0xfc, 0xea, 0x3f, 0x5b, 0x88, 0x9b, 0xcd, 0xf9, 0x3d, 0x23, 0x32, 0x08, 0x83, 0xdc, 0xad, + 0x22, 0x5e, 0xce, 0x04, 0x0a, 0x63, 0x67, 0x82, 0xcd, 0xbb, 0x58, 0x47, 0x58, 0x24, 0xaa, 0x23, 0x69, 0x48, 0x57, + 0x79, 0x08, 0x54, 0xb1, 0xef, 0xc9, 0xd3, 0xca, 0x28, 0x5a, 0xbf, 0x3a, 0xf6, 0x19, 0x10, 0x52, 0x25, 0xcb, 0x8a, + 0xb4, 0x72, 0x85, 0x99, 0x81, 0x91, 0x84, 0x83, 0x23, 0xd0, 0x4d, 0x13, 0xc2, 0xcb, 0x43, 0x7a, 0x69, 0x2d, 0x35, + 0xaa, 0xc5, 0x35, 0x78, 0x25, 0x80, 0xd8, 0x64, 0x8c, 0x5f, 0xef, 0xf6, 0xf4, 0xb0, 0xbe, 0x68, 0xb1, 0xfe, 0x88, + 0x80, 0x63, 0xa4, 0xfb, 0xa2, 0x1c, 0x7a, 0x03, 0x96, 0xb5, 0xc4, 0xb7, 0x8f, 0x61, 0xa8, 0x74, 0xa0, 0x5e, 0x8e, + 0xdc, 0x22, 0xaa, 0x37, 0xc0, 0xb5, 0xdb, 0x15, 0x11, 0xbe, 0x9d, 0x1f, 0xd3, 0xa4, 0x96, 0x10, 0xc4, 0xba, 0x8f, + 0x68, 0x96, 0x89, 0xb0, 0xd9, 0xb8, 0xeb, 0x70, 0x71, 0x0c, 0x45, 0x1f, 0x9e, 0xe2, 0x22, 0x96, 0x9c, 0x2d, 0xbd, + 0xb4, 0x31, 0x4f, 0x87, 0xf4, 0x53, 0xdb, 0x51, 0xe1, 0xd1, 0x0b, 0xcb, 0x85, 0xc6, 0x9d, 0xa4, 0xe0, 0xea, 0x3d, + 0x10, 0x26, 0xe9, 0x73, 0xf7, 0x98, 0xc7, 0xd5, 0xe8, 0x2d, 0x38, 0x7d, 0x0b, 0x68, 0x6f, 0x8a, 0xe0, 0x72, 0xd5, + 0x5e, 0x9a, 0x30, 0xa3, 0x3d, 0xcf, 0x74, 0xb6, 0x24, 0x55, 0x23, 0xde, 0x8b, 0x16, 0xbc, 0x86, 0x72, 0x4f, 0x2c, + 0x61, 0xcc, 0xe0, 0xb6, 0x4b, 0x48, 0xb2, 0xaf, 0xa5, 0x82, 0x95, 0xa0, 0x07, 0xf2, 0xa8, 0x48, 0x46, 0x49, 0xa6, + 0xdb, 0xfe, 0x6c, 0xe6, 0xb6, 0x37, 0x95, 0xdf, 0xb6, 0xce, 0x44, 0x95, 0xa4, 0xaf, 0x57, 0x7d, 0xda, 0x3d, 0xa3, + 0x2b, 0x0f, 0x02, 0xfa, 0x96, 0xd1, 0x5b, 0x2e, 0xb0, 0x6e, 0xc9, 0x0d, 0xa9, 0x20, 0xf6, 0x2e, 0x2b, 0x70, 0xe1, + 0xad, 0x3d, 0x98, 0xb0, 0x06, 0xef, 0x33, 0x3d, 0x69, 0xad, 0xbe, 0x7d, 0xa9, 0xeb, 0xf8, 0xec, 0xbb, 0xed, 0x86, + 0x68, 0xf0, 0x5b, 0x2e, 0xbe, 0x17, 0x9f, 0x99, 0x69, 0x15, 0x0e, 0x66, 0x51, 0xfa, 0x2a, 0xfd, 0x8b, 0x93, 0xd6, + 0x91, 0x0b, 0x70, 0x00, 0xf2, 0x6e, 0xb8, 0x2e, 0xc6, 0x61, 0xbc, 0xe6, 0x84, 0xf3, 0xd4, 0x7b, 0xb0, 0x6b, 0xa7, + 0x14, 0xfc, 0x73, 0x86, 0x8d, 0x1c, 0x32, 0x3b, 0x5e, 0x84, 0x6f, 0x6a, 0x1b, 0x7e, 0x4e, 0xfc, 0x80, 0xbf, 0xce, + 0x0c, 0xef, 0x67, 0x71, 0xf6, 0xb6, 0xc0, 0x1f, 0xa6, 0x78, 0xe1, 0xcf, 0x95, 0x30, 0xe3, 0x2b, 0xfe, 0x95, 0xf8, + 0x6f, 0x04, 0x6f, 0x98, 0x70, 0x99, 0xad, 0x35, 0x5a, 0x64, 0xf3, 0x9b, 0x7c, 0x7a, 0x77, 0xf7, 0x70, 0x76, 0xb3, + 0x5a, 0x56, 0xb4, 0x61, 0x25, 0x7b, 0x8e, 0xea, 0x3a, 0xae, 0xca, 0xfe, 0x99, 0xc2, 0x6a, 0x69, 0xdf, 0x51, 0x24, + 0xf7, 0x26, 0xe9, 0xb3, 0xb7, 0x9b, 0x53, 0x93, 0x87, 0xa2, 0x09, 0x1d, 0xe9, 0xcb, 0xa3, 0xb5, 0x04, 0x9e, 0x96, + 0x5d, 0xa9, 0x8b, 0x60, 0xf2, 0xc3, 0xcc, 0xcb, 0x5e, 0xa4, 0x34, 0x55, 0x87, 0xdd, 0xb5, 0x8a, 0x24, 0x54, 0x69, + 0x47, 0xee, 0x94, 0x62, 0xd2, 0xaa, 0x03, 0x67, 0xa0, 0x20, 0xfb, 0x4a, 0x24, 0x4e, 0xf8, 0x21, 0x7a, 0xf0, 0x81, + 0xf1, 0xa4, 0x88, 0xe6, 0xc1, 0x14, 0xe1, 0xff, 0x9f, 0xae, 0x67, 0xd5, 0x33, 0x1b, 0xa0, 0xd6, 0xff, 0x05, 0x62, + 0x0e, 0x4d, 0x55, 0x42, 0xf2, 0xc0, 0x84, 0x3b, 0x7f, 0x7a, 0xd6, 0x0c, 0x16, 0x96, 0x1f, 0xd5, 0x61, 0x90, 0xa7, + 0xd9, 0x39, 0x99, 0xc8, 0x38, 0x4e, 0xce, 0x94, 0x42, 0x3d, 0xe3, 0xea, 0xcb, 0x35, 0x88, 0xde, 0x6b, 0xaa, 0xd5, + 0xa1, 0x93, 0x74, 0x92, 0x77, 0xc6, 0x52, 0x2c, 0xa2, 0x65, 0xbb, 0x6a, 0xe3, 0x62, 0x3b, 0x82, 0x33, 0x28, 0x38, + 0xcb, 0x1c, 0x7a, 0x0f, 0x16, 0xda, 0xee, 0xdc, 0xd8, 0x61, 0xba, 0x37, 0x37, 0x0e, 0x47, 0x04, 0x8d, 0xdd, 0x36, + 0xdd, 0xb6, 0x22, 0x2a, 0xb1, 0xd9, 0xa2, 0x8b, 0x78, 0x63, 0x40, 0x40, 0x2f, 0x3e, 0x4e, 0xf5, 0xa9, 0xcf, 0xdb, + 0x4e, 0xbe, 0xd2, 0x09, 0xcb, 0x5a, 0xce, 0xbd, 0xc3, 0x78, 0xd5, 0x8d, 0x82, 0xd0, 0x2c, 0x84, 0x54, 0xf6, 0x42, + 0xe7, 0x09, 0xd8, 0xc4, 0x88, 0x3f, 0x62, 0x2b, 0xa1, 0x4c, 0x0e, 0xac, 0x0a, 0x4a, 0xc7, 0x87, 0x9a, 0x1d, 0x08, + 0x42, 0x57, 0xfb, 0xc8, 0x06, 0x72, 0x2c, 0xb4, 0x13, 0x19, 0x88, 0x26, 0x0e, 0x81, 0x6b, 0xac, 0x11, 0xd6, 0x47, + 0x32, 0x5e, 0x0e, 0x6d, 0xbd, 0x59, 0x03, 0x9b, 0xa8, 0xcd, 0x41, 0xef, 0xfe, 0x53, 0xde, 0xa1, 0x8b, 0x22, 0x6b, + 0x3d, 0x67, 0x4c, 0xcf, 0x22, 0xe6, 0x41, 0xb1, 0x2c, 0x81, 0x46, 0x11, 0x8a, 0xd0, 0xbf, 0x27, 0xf6, 0x50, 0x4f, + 0x2a, 0xa3, 0x3c, 0x61, 0x3e, 0xac, 0x50, 0x4d, 0xab, 0xa5, 0xa4, 0x53, 0x16, 0x1e, 0xc3, 0xdd, 0xc1, 0x8f, 0xaf, + 0xfd, 0xd1, 0xb8, 0xfe, 0x6a, 0xf4, 0xb1, 0xed, 0xf9, 0x9a, 0x96, 0xa9, 0x1f, 0x67, 0x4d, 0x7e, 0x1d, 0x9c, 0x37, + 0x16, 0x9b, 0xed, 0x95, 0x1e, 0xb8, 0x64, 0x22, 0x50, 0xaa, 0x5f, 0x66, 0xfb, 0x01, 0x9d, 0x2d, 0x14, 0x9f, 0x1a, + 0x15, 0xe5, 0xde, 0x5a, 0x8d, 0x65, 0x80, 0x70, 0x0c, 0x69, 0xa4, 0x5d, 0x62, 0x0a, 0x22, 0xad, 0xcf, 0xd2, 0x53, + 0xc0, 0x6b, 0x6b, 0x34, 0x8f, 0xe0, 0x90, 0x21, 0xd3, 0x24, 0xb1, 0x22, 0xfd, 0x0c, 0x10, 0xb7, 0x11, 0xd2, 0x8b, + 0xf4, 0x0f, 0x28, 0x01, 0x78, 0x15, 0xd1, 0xde, 0xa8, 0x8c, 0x45, 0xb2, 0x2a, 0xab, 0x95, 0xc2, 0x72, 0x7c, 0xc0, + 0xbf, 0xf4, 0x0f, 0x0b, 0x16, 0xa8, 0x1a, 0xe9, 0xd8, 0xc2, 0x4d, 0x04, 0x6a, 0x85, 0xed, 0x46, 0x88, 0xe2, 0xfb, + 0x75, 0x7d, 0xa4, 0x6c, 0x2e, 0x16, 0xc7, 0x18, 0x5b, 0xed, 0xcd, 0xd7, 0x5c, 0x6e, 0x3b, 0x43, 0xb7, 0x6d, 0xee, + 0xc0, 0xde, 0xa4, 0x7b, 0x1a, 0xbf, 0x7d, 0xab, 0xe1, 0x29, 0x7e, 0xf3, 0x6a, 0xa4, 0xf4, 0xf2, 0x78, 0x25, 0x74, + 0xef, 0xc9, 0x82, 0xe6, 0xc6, 0x65, 0x5c, 0xfa, 0xdd, 0x7b, 0x8a, 0xf0, 0x57, 0xfd, 0x57, 0x6a, 0x3c, 0xa9, 0xca, + 0xca, 0xdf, 0xac, 0x14, 0xf6, 0x07, 0x86, 0xaa, 0xca, 0x90, 0x21, 0x90, 0xa7, 0x4a, 0x0f, 0xb6, 0x27, 0x51, 0x6e, + 0xbf, 0x29, 0x69, 0x6c, 0x01, 0x17, 0x8a, 0x4e, 0x8d, 0xac, 0x72, 0xc2, 0xb3, 0x9e, 0xad, 0xf7, 0x90, 0x44, 0x5c, + 0xb9, 0xce, 0xc4, 0x11, 0x77, 0x3f, 0xe7, 0xf3, 0x8f, 0x2a, 0x02, 0x3a, 0x64, 0xf1, 0x51, 0x77, 0x19, 0x05, 0x41, + 0xc3, 0x0b, 0x69, 0x98, 0x20, 0x33, 0xa1, 0xac, 0xa9, 0x76, 0x5f, 0xa6, 0x69, 0xbd, 0x3e, 0x7f, 0x2e, 0x8e, 0xbd, + 0x1d, 0x90, 0xcc, 0xc6, 0x9c, 0x69, 0x56, 0x12, 0x37, 0xf2, 0xe0, 0x54, 0xd1, 0xe6, 0x2c, 0xc8, 0xd6, 0xea, 0xad, + 0x9e, 0x93, 0x39, 0xe0, 0x24, 0xd2, 0x32, 0xcc, 0x8f, 0x3c, 0xe7, 0xcf, 0x15, 0x27, 0xd3, 0xe8, 0xbe, 0x57, 0x63, + 0xdb, 0x66, 0x98, 0xf4, 0x24, 0x03, 0x40, 0x9d, 0x08, 0xe0, 0x9b, 0x12, 0xd4, 0x01, 0x8a, 0xaf, 0x2d, 0x8b, 0xc9, + 0x4c, 0x0c, 0x08, 0x26, 0x93, 0xfd, 0xda, 0x93, 0x03, 0xd3, 0x99, 0x08, 0x6c, 0x18, 0xf2, 0xcf, 0x40, 0x54, 0xe3, + 0xdb, 0x14, 0x24, 0xa1, 0x68, 0x4d, 0xa6, 0xb8, 0xf9, 0x04, 0x05, 0x9e, 0x7d, 0x11, 0xb2, 0xa5, 0x7e, 0x53, 0xc6, + 0x29, 0x84, 0xcd, 0x69, 0x3d, 0x98, 0xb2, 0x32, 0x9e, 0x49, 0x78, 0x8d, 0xe3, 0x9f, 0x2d, 0x82, 0xc7, 0xa8, 0xbc, + 0x8c, 0xc1, 0x1a, 0x8d, 0x5f, 0xc0, 0xe0, 0xb2, 0xb7, 0xa2, 0xc3, 0x28, 0xae, 0x3f, 0x6c, 0x8d, 0x71, 0x92, 0xd5, + 0x7e, 0x71, 0xf6, 0x37, 0xdb, 0x6f, 0x3d, 0x72, 0xf3, 0xd5, 0xcd, 0xce, 0x0e, 0x4d, 0x6b, 0x05, 0x83, 0x1e, 0xc6, + 0x60, 0x03, 0x70, 0xc5, 0x38, 0xb3, 0x91, 0x62, 0x47, 0xcd, 0x33, 0xe0, 0xa8, 0x57, 0x37, 0x3f, 0xd2, 0xee, 0xf8, + 0x79, 0x86, 0xf8, 0x44, 0x22, 0x4c, 0xde, 0x89, 0x60, 0x83, 0x5a, 0xba, 0xa3, 0x3f, 0xf2, 0x54, 0x86, 0x16, 0x15, + 0x6f, 0x26, 0x79, 0x4e, 0x31, 0xd1, 0xb2, 0xdc, 0x1e, 0x3d, 0xf1, 0x16, 0xd3, 0x52, 0x87, 0xda, 0x65, 0xed, 0x34, + 0x8d, 0x1d, 0x53, 0xa8, 0x27, 0xec, 0xf8, 0x3b, 0x8c, 0xf2, 0xaf, 0x85, 0x97, 0x7f, 0x3e, 0xfe, 0xd7, 0x1c, 0xff, + 0x22, 0xdc, 0xd5, 0xc9, 0xea, 0xf0, 0xe7, 0xe8, 0xff, 0x1e, 0x1f, 0x50, 0x9d, 0xbc, 0xd9, 0xda, 0x7c, 0xa3, 0x5a, + 0x6f, 0x92, 0x47, 0x6b, 0x3d, 0x4e, 0xd0, 0x57, 0x1f, 0x77, 0x4e, 0x27, 0x18, 0xe7, 0x9c, 0x3a, 0x3f, 0x8a, 0xfd, + 0xd1, 0x12, 0x17, 0x7e, 0x31, 0xfe, 0xb0, 0xb5, 0x99, 0xdc, 0xa3, 0xfd, 0x37, 0xa4, 0x72, 0x9f, 0x9f, 0x50, 0x6d, + 0x2b, 0x1a, 0x24, 0x7f, 0x7f, 0xe8, 0xdc, 0x11, 0x81, 0x0d, 0xe7, 0xfe, 0xeb, 0x35, 0x8d, 0x3f, 0x19, 0x5c, 0x44, + 0x8a, 0xd6, 0x0a, 0x8b, 0xcf, 0xf4, 0x99, 0x56, 0x56, 0xdf, 0xb8, 0x55, 0x65, 0x1b, 0x3a, 0xa1, 0x36, 0xdd, 0x00, + 0xc4, 0xa4, 0x82, 0x06, 0x65, 0xed, 0x7e, 0xf9, 0xd3, 0x44, 0x1b, 0x12, 0x8a, 0x7e, 0xf6, 0x83, 0x91, 0x76, 0x37, + 0xa7, 0x0a, 0x20, 0xb1, 0x61, 0x7a, 0xfc, 0x52, 0x30, 0x19, 0xd0, 0xf0, 0x50, 0x47, 0x17, 0xad, 0x55, 0x9f, 0x45, + 0x7a, 0x63, 0xbd, 0x26, 0xc5, 0xf5, 0x15, 0x90, 0x20, 0xa4, 0x69, 0xb8, 0xfc, 0x3f, 0xfe, 0x44, 0x24, 0x4d, 0xaf, + 0xd1, 0x50, 0x39, 0x0d, 0xfd, 0xf5, 0x3b, 0xb4, 0xec, 0x85, 0x96, 0x33, 0x7b, 0x19, 0x15, 0x03, 0xcf, 0x82, 0xa0, + 0xba, 0x22, 0xc7, 0x26, 0x57, 0xe3, 0x39, 0x29, 0xc7, 0xfc, 0x1f, 0x67, 0x79, 0xbd, 0x86, 0x39, 0xc7, 0x88, 0xef, + 0xec, 0x02, 0x61, 0x49, 0x56, 0xc3, 0xc6, 0xec, 0x41, 0x7f, 0xf8, 0xe8, 0x0d, 0x34, 0xe8, 0x87, 0x8f, 0xbf, 0x20, + 0x01, 0x5f, 0xf8, 0x61, 0x74, 0x35, 0x2a, 0x1e, 0x9b, 0xd3, 0xda, 0xf5, 0x71, 0x63, 0x60, 0xe8, 0x23, 0x11, 0x1c, + 0x72, 0x0a, 0x10, 0x5f, 0x24, 0x9d, 0x1d, 0x4d, 0x1c, 0x73, 0x39, 0x24, 0x07, 0xed, 0x38, 0x97, 0x2a, 0x53, 0x0e, + 0x35, 0xa7, 0x8e, 0x72, 0x40, 0x8e, 0xf3, 0xe8, 0x40, 0xb3, 0x6e, 0x2f, 0x27, 0xe3, 0xcb, 0x9c, 0xb4, 0xcb, 0x66, + 0xb3, 0xd7, 0xd4, 0x30, 0x93, 0x88, 0x91, 0x0a, 0xde, 0xe4, 0xac, 0x8a, 0xa0, 0x5f, 0x74, 0x8a, 0xa9, 0x8d, 0x78, + 0xb8, 0xb7, 0x9e, 0x9d, 0x3a, 0x0f, 0x34, 0xb8, 0x32, 0x20, 0xaf, 0x78, 0x0d, 0xb8, 0x51, 0xc8, 0x84, 0x59, 0xc2, + 0x02, 0x2e, 0xcc, 0xdf, 0x7d, 0xe2, 0x3e, 0xd8, 0x2f, 0xa2, 0xe0, 0x3c, 0x7b, 0x6f, 0x06, 0xcb, 0x12, 0x76, 0x41, + 0xf5, 0xc6, 0x7d, 0xee, 0x3d, 0xfe, 0x71, 0xc3, 0x14, 0x14, 0x58, 0x06, 0xf9, 0x74, 0xe7, 0x0b, 0x22, 0xf0, 0x03, + 0xfb, 0xc3, 0x3c, 0xe6, 0xec, 0x1f, 0x9a, 0x53, 0x73, 0x4b, 0x28, 0x1b, 0x48, 0x75, 0x69, 0xcb, 0x82, 0xb3, 0xd3, + 0x61, 0x8b, 0xf3, 0x9e, 0xa3, 0x46, 0xa9, 0xee, 0xa9, 0x83, 0x32, 0x21, 0x5a, 0xe6, 0x14, 0xd8, 0x22, 0x80, 0x96, + 0xad, 0x08, 0xaf, 0x03, 0xe5, 0xa5, 0x66, 0x46, 0x43, 0x7f, 0x88, 0xb3, 0x49, 0xf8, 0x06, 0x74, 0x72, 0xd1, 0xe1, + 0xa2, 0xcb, 0xa5, 0x53, 0x7a, 0x7c, 0x3c, 0x40, 0x34, 0x76, 0xce, 0xc2, 0x60, 0x5e, 0x4f, 0x52, 0xbe, 0xf4, 0xec, + 0xd7, 0xe3, 0xa2, 0xbd, 0x36, 0xfa, 0x70, 0x32, 0x4d, 0x98, 0xd8, 0x80, 0x9a, 0x56, 0xc7, 0x21, 0x1e, 0x3c, 0xa4, + 0x80, 0x1e, 0x94, 0x66, 0x79, 0xdf, 0x04, 0x52, 0x48, 0x45, 0xc8, 0x44, 0x5e, 0x16, 0x7a, 0xb6, 0x0e, 0x06, 0x82, + 0x9a, 0xed, 0x8c, 0x4f, 0x75, 0xd2, 0x68, 0xa9, 0x78, 0x81, 0x98, 0x12, 0x46, 0x48, 0xd3, 0xfa, 0x27, 0xa0, 0x1b, + 0xbe, 0x06, 0x28, 0x7f, 0x52, 0x2e, 0x3b, 0x9e, 0x59, 0xc6, 0x0e, 0xe1, 0x80, 0x9f, 0xaa, 0x02, 0x77, 0x17, 0x15, + 0xfa, 0xc7, 0xf3, 0xd1, 0x90, 0x1c, 0x22, 0x34, 0x0c, 0x95, 0x70, 0x01, 0x91, 0x51, 0xea, 0x63, 0x87, 0xd0, 0xeb, + 0x7e, 0x40, 0xbe, 0xf8, 0x23, 0x9a, 0xf0, 0x88, 0x3b, 0xe5, 0xad, 0xae, 0x5a, 0x68, 0xe2, 0x23, 0xee, 0x82, 0x06, + 0xdf, 0x7c, 0x70, 0x9a, 0xee, 0x1e, 0x55, 0x96, 0x56, 0xe8, 0x13, 0x0d, 0x64, 0x4a, 0xf5, 0xf4, 0x7a, 0xa6, 0x9a, + 0xde, 0x2c, 0xa1, 0x95, 0x40, 0xd9, 0xc6, 0x74, 0x9e, 0xc6, 0x96, 0xed, 0xb5, 0x8b, 0x14, 0xf9, 0xf3, 0x34, 0x62, + 0x0d, 0x5b, 0x02, 0x76, 0xe3, 0x8e, 0xbe, 0xed, 0x64, 0xc7, 0xd0, 0x10, 0x25, 0xbd, 0xa8, 0x38, 0x1d, 0x63, 0xe4, + 0xe6, 0x75, 0x0f, 0xd8, 0x2e, 0xa3, 0xb7, 0xd5, 0xc0, 0x70, 0xee, 0x9b, 0xd4, 0x9c, 0x14, 0x9c, 0xf3, 0xd6, 0xfd, + 0x75, 0x82, 0x34, 0x9e, 0xe7, 0xad, 0x8b, 0xf7, 0x22, 0x9e, 0x69, 0xf3, 0xaf, 0x17, 0xe5, 0xf9, 0xaa, 0xc6, 0x65, + 0xeb, 0xaf, 0x49, 0xb0, 0x85, 0xec, 0x67, 0x15, 0x52, 0xfd, 0x47, 0xc5, 0x8e, 0x78, 0x7b, 0x3e, 0xa7, 0x02, 0x67, + 0xae, 0x3a, 0x3e, 0x2a, 0xbe, 0x41, 0x2f, 0x0e, 0x07, 0x38, 0x07, 0x01, 0xf2, 0xc0, 0x49, 0xa8, 0xc9, 0x3c, 0x60, + 0xcc, 0xa9, 0x56, 0xf3, 0x15, 0xeb, 0x31, 0xeb, 0x0d, 0x33, 0x3c, 0x57, 0xff, 0x03, 0xd4, 0x80, 0x0b, 0xe8, 0x0f, + 0x3b, 0xbc, 0xaf, 0x31, 0x84, 0x46, 0xdc, 0x8d, 0x7c, 0x62, 0xf0, 0xbb, 0xfc, 0x37, 0x83, 0x99, 0x6c, 0x24, 0xc8, + 0xcc, 0x3a, 0xd5, 0x3e, 0x31, 0x59, 0x19, 0x82, 0x7a, 0x2d, 0xed, 0xa6, 0xf4, 0x10, 0x19, 0x8a, 0x70, 0x02, 0x0c, + 0x14, 0xb4, 0x31, 0x81, 0x57, 0x57, 0x68, 0xa6, 0x1b, 0xcc, 0xd5, 0x47, 0x4d, 0x9d, 0x43, 0xdc, 0x2b, 0x2d, 0x95, + 0xc1, 0xa0, 0x36, 0x08, 0xbc, 0x6b, 0xbf, 0xfc, 0xc3, 0x32, 0x9e, 0x27, 0x87, 0xaa, 0x9f, 0x0e, 0x1b, 0xc3, 0x35, + 0x75, 0xac, 0x7a, 0xfd, 0xcf, 0xd4, 0x24, 0xc6, 0xa7, 0x46, 0x82, 0xc1, 0xba, 0x8a, 0x13, 0x2d, 0x88, 0xd3, 0x46, + 0x69, 0x17, 0x8a, 0x3a, 0xd4, 0x02, 0x2e, 0x0d, 0xa9, 0x71, 0xc0, 0x2a, 0x37, 0x2f, 0xcf, 0x0d, 0x74, 0xe2, 0x39, + 0x7f, 0x9d, 0x99, 0xf0, 0xa1, 0x9e, 0xe6, 0x50, 0xd7, 0x26, 0xcf, 0xe5, 0xfd, 0xf8, 0xc5, 0xca, 0x43, 0x22, 0x27, + 0xb1, 0xd0, 0x26, 0x9b, 0xeb, 0x7c, 0xbe, 0xc0, 0x62, 0x23, 0x88, 0xfa, 0x7c, 0x85, 0x0a, 0xa2, 0xc3, 0x61, 0x53, + 0x4c, 0x75, 0xc4, 0x33, 0xc6, 0x44, 0xa5, 0xed, 0x62, 0x33, 0x1c, 0xc0, 0x00, 0x9c, 0x8b, 0xb2, 0x96, 0x8f, 0xdf, + 0xa6, 0xd1, 0x9f, 0xe4, 0xec, 0x4c, 0x4a, 0x19, 0xbf, 0x21, 0xfb, 0x33, 0xbe, 0x3f, 0x62, 0x74, 0xef, 0xdf, 0xc9, + 0x3e, 0xed, 0x5f, 0x33, 0xb6, 0x31, 0xb6, 0x24, 0x6f, 0xcc, 0xec, 0xab, 0xcd, 0xcb, 0xb8, 0x24, 0x0a, 0xc8, 0xfe, + 0x46, 0xe3, 0x61, 0x9a, 0x87, 0x38, 0x3c, 0xac, 0x1a, 0x45, 0x7e, 0x47, 0x41, 0x96, 0x18, 0xe0, 0x6d, 0xa6, 0x45, + 0xba, 0x99, 0xc0, 0xdb, 0xa0, 0x94, 0x74, 0x68, 0x77, 0xa6, 0x2c, 0x31, 0xa8, 0xc2, 0xc0, 0x20, 0x22, 0x77, 0xba, + 0x04, 0xa2, 0xdd, 0x4a, 0x66, 0x4f, 0xf0, 0x3e, 0xa6, 0xa1, 0x13, 0xb7, 0x6c, 0x79, 0x8b, 0x6d, 0x4d, 0xcd, 0xec, + 0xe8, 0x85, 0x9a, 0xa1, 0x30, 0x32, 0x3a, 0x7d, 0xa1, 0xd6, 0x8f, 0x26, 0x64, 0xa9, 0x10, 0xbf, 0x2a, 0xf1, 0x55, + 0xeb, 0x6b, 0xa9, 0x10, 0x57, 0x67, 0x17, 0x39, 0x86, 0x9f, 0x65, 0x88, 0xc7, 0xd8, 0x8e, 0x7b, 0xeb, 0x6b, 0x0f, + 0x27, 0x80, 0x8a, 0xa4, 0x65, 0x48, 0x6e, 0xe5, 0xd8, 0x90, 0x86, 0x96, 0xfe, 0xf0, 0x74, 0x86, 0x99, 0x22, 0x40, + 0x67, 0xcd, 0x13, 0x4f, 0x5d, 0x4c, 0xd5, 0x7f, 0xa7, 0xa0, 0x62, 0xfb, 0x83, 0xca, 0x00, 0x38, 0x49, 0x1d, 0x44, + 0x23, 0xb3, 0xcf, 0x3a, 0x8d, 0x3e, 0xe4, 0xe2, 0x29, 0x38, 0x02, 0x96, 0x53, 0xe4, 0x9a, 0x33, 0x5a, 0xd7, 0x32, + 0xa4, 0x49, 0xb6, 0x6f, 0x97, 0xe3, 0xde, 0x05, 0x77, 0x68, 0xd2, 0x48, 0x68, 0xa9, 0xba, 0x42, 0xae, 0x94, 0xa5, + 0xa3, 0xee, 0xb4, 0x1b, 0x53, 0x6e, 0xac, 0x70, 0x2b, 0x73, 0xd1, 0xb1, 0x8c, 0x55, 0x39, 0xc2, 0x22, 0x5d, 0x1c, + 0x05, 0x96, 0x05, 0xf8, 0x1e, 0x18, 0x44, 0xa5, 0x2a, 0xcb, 0x44, 0x11, 0x92, 0xea, 0x84, 0x05, 0xc6, 0xb2, 0xf9, + 0x7e, 0x13, 0x09, 0x1e, 0x7c, 0xfd, 0x37, 0x8c, 0x24, 0xb1, 0x11, 0x10, 0x40, 0x83, 0x86, 0x16, 0x50, 0xcd, 0xfc, + 0x5e, 0xd9, 0x2d, 0x84, 0xce, 0x93, 0xf8, 0xa0, 0x92, 0x64, 0xd0, 0x9f, 0xff, 0xc7, 0x04, 0x31, 0x68, 0x1d, 0x52, + 0xce, 0x82, 0x03, 0x6e, 0x98, 0x9b, 0x4e, 0xa2, 0xba, 0x6c, 0x51, 0x2c, 0xb6, 0xd8, 0xf3, 0xb9, 0x0d, 0x6a, 0x05, + 0x2b, 0x2f, 0x21, 0xa5, 0x1d, 0xcd, 0x57, 0x5e, 0x87, 0x2a, 0x6f, 0x79, 0x8d, 0x3b, 0x4c, 0xf4, 0x0b, 0x27, 0xba, + 0x26, 0xab, 0xd1, 0xad, 0x23, 0x00, 0x99, 0x8d, 0x03, 0xd5, 0x1b, 0x84, 0x4b, 0x48, 0xd9, 0xe8, 0x2d, 0x73, 0x6e, + 0xf0, 0xdb, 0xf9, 0x9c, 0x90, 0xc4, 0xc8, 0x85, 0x26, 0x80, 0x93, 0x38, 0x25, 0xb4, 0xa9, 0x8b, 0x9c, 0xa9, 0xd3, + 0x13, 0xde, 0x3a, 0x68, 0x6e, 0x6d, 0x36, 0x42, 0xb1, 0x97, 0xf5, 0x49, 0x11, 0x25, 0x55, 0x97, 0x83, 0x72, 0x53, + 0x82, 0x5d, 0xfb, 0x31, 0xde, 0xca, 0x30, 0x64, 0x37, 0x2b, 0x60, 0x24, 0x66, 0x42, 0x72, 0x26, 0x48, 0x92, 0x65, + 0xd2, 0x65, 0x2d, 0xcd, 0xea, 0xda, 0x7f, 0xb4, 0x10, 0x1e, 0x91, 0x8c, 0xf3, 0xb3, 0x3c, 0x94, 0x1d, 0x57, 0xd6, + 0x29, 0xb2, 0x3c, 0x3d, 0x11, 0xae, 0xbb, 0x55, 0x35, 0x35, 0xbc, 0x07, 0x44, 0x64, 0x72, 0xcb, 0x56, 0xf5, 0xb1, + 0x33, 0xc1, 0xcf, 0x5c, 0x1e, 0x88, 0x8b, 0x07, 0x15, 0x49, 0xe8, 0xe7, 0xdb, 0x3c, 0x4f, 0x14, 0x1a, 0xbd, 0x43, + 0xce, 0xad, 0xe4, 0xe2, 0x5c, 0x0b, 0x94, 0x58, 0xf0, 0xe5, 0xf6, 0xa4, 0x3a, 0x47, 0x1e, 0xf8, 0x4e, 0x9c, 0x09, + 0x5d, 0x64, 0x5e, 0xe9, 0x1a, 0x79, 0x2b, 0xbd, 0x57, 0xd5, 0xc8, 0x1f, 0xfc, 0xea, 0x7f, 0x59, 0xe9, 0x35, 0x7a, + 0x11, 0x89, 0x33, 0x5f, 0xe2, 0x12, 0xed, 0x0c, 0xec, 0x30, 0x4e, 0xea, 0x9a, 0xbb, 0x2f, 0x80, 0x56, 0x17, 0xde, + 0x74, 0xb4, 0x16, 0x09, 0x3c, 0xd7, 0xdd, 0x25, 0xae, 0x84, 0x1d, 0x6e, 0xa0, 0xd8, 0xc3, 0x0c, 0x06, 0x42, 0xa3, + 0xc8, 0x86, 0x03, 0xc0, 0xcf, 0x21, 0xfe, 0x1a, 0xf3, 0xa3, 0x6e, 0xd9, 0x46, 0x0b, 0x9c, 0x53, 0x64, 0x06, 0xd9, + 0x8b, 0xc8, 0x80, 0x1c, 0xea, 0x84, 0x2c, 0xc8, 0x35, 0x6a, 0xec, 0x80, 0xb5, 0xc2, 0x0a, 0x65, 0x35, 0xc0, 0xb1, + 0xc1, 0x66, 0xed, 0xa5, 0xb9, 0xa9, 0xc0, 0xa7, 0x4b, 0x44, 0xae, 0xe9, 0x91, 0x50, 0xbe, 0x82, 0x14, 0x54, 0xa4, + 0x9f, 0x57, 0xff, 0x0a, 0x4c, 0x7a, 0x3b, 0x27, 0x68, 0x17, 0x91, 0x71, 0xbf, 0xd0, 0x11, 0x28, 0x2d, 0x62, 0xfb, + 0x87, 0xc9, 0xf1, 0x75, 0x30, 0xa6, 0x6b, 0xe4, 0x73, 0x6b, 0xcd, 0x3f, 0x41, 0xf5, 0x3c, 0x19, 0x0f, 0x14, 0xa9, + 0x30, 0x00, 0xfc, 0xde, 0x08, 0x1a, 0xef, 0xfd, 0xdf, 0x33, 0x1c, 0x67, 0x74, 0x4b, 0x28, 0x3c, 0x02, 0xf2, 0x4d, + 0xfe, 0x17, 0xc3, 0x78, 0x54, 0x00, 0x3b, 0x2b, 0xf2, 0xde, 0xd0, 0xde, 0xad, 0x43, 0xc0, 0xd0, 0x37, 0x60, 0xcc, + 0xfc, 0x0d, 0x47, 0xd9, 0x40, 0x6e, 0xdb, 0x19, 0xae, 0xab, 0x92, 0x66, 0x26, 0x19, 0x1e, 0x49, 0x0c, 0x52, 0x69, + 0xe4, 0x47, 0x5d, 0x59, 0x9c, 0x66, 0xee, 0x2a, 0x38, 0xf2, 0xb3, 0xc7, 0x33, 0x6c, 0xde, 0xd8, 0x88, 0x3b, 0x5e, + 0x80, 0x34, 0x37, 0x34, 0x00, 0xe0, 0x85, 0x4b, 0x45, 0x87, 0x3b, 0xe6, 0x2a, 0x5b, 0x81, 0xfa, 0x69, 0xa2, 0x39, + 0x38, 0xce, 0x46, 0x15, 0xf2, 0x09, 0xb7, 0x1b, 0xf1, 0x79, 0x0e, 0x10, 0x8f, 0x63, 0xa5, 0x32, 0x18, 0x12, 0x05, + 0x3f, 0x11, 0x61, 0x47, 0xd3, 0x89, 0xb3, 0xe4, 0xae, 0x52, 0x7b, 0x0c, 0x50, 0x0d, 0x09, 0x58, 0x65, 0x6c, 0xc3, + 0xfa, 0x45, 0x90, 0xb8, 0xac, 0xef, 0x18, 0x2d, 0xeb, 0xb0, 0x50, 0x0b, 0x1f, 0x39, 0xa7, 0x1f, 0xe2, 0xa0, 0x10, + 0x67, 0x23, 0x9c, 0x67, 0x20, 0x79, 0xda, 0x40, 0x66, 0xe4, 0xc5, 0xf8, 0xbd, 0x74, 0x67, 0xbb, 0x61, 0x65, 0x48, + 0xba, 0xc5, 0x5b, 0x6d, 0x3d, 0x93, 0xfc, 0x88, 0x1c, 0x38, 0x29, 0x02, 0xc9, 0x24, 0x52, 0x41, 0x95, 0xd2, 0x60, + 0xe5, 0xaf, 0x00, 0x28, 0x98, 0x6b, 0x5e, 0xd3, 0x54, 0x4f, 0xcb, 0x84, 0xdd, 0xe6, 0x68, 0xb0, 0x4e, 0x1c, 0xaa, + 0x1f, 0x0c, 0x3a, 0x85, 0x38, 0x43, 0xbb, 0xc0, 0x03, 0x8d, 0x4c, 0xec, 0xf1, 0xe7, 0xf9, 0x49, 0xc1, 0x3b, 0xab, + 0x34, 0x4b, 0xc1, 0x33, 0x95, 0x32, 0x78, 0x0c, 0x56, 0xe7, 0xdf, 0xee, 0x6b, 0xa2, 0xd2, 0x80, 0x00, 0xd0, 0x51, + 0xcc, 0xe1, 0xbc, 0x9b, 0xa2, 0x49, 0x77, 0x6a, 0xb2, 0xff, 0xd6, 0xab, 0xdb, 0x9b, 0x71, 0x94, 0x17, 0xdd, 0x61, + 0x35, 0xf1, 0x71, 0xd2, 0x84, 0xed, 0x8c, 0xad, 0xd4, 0xf5, 0x0b, 0xb0, 0x00, 0x76, 0x99, 0xf1, 0x6c, 0x0c, 0xaf, + 0xeb, 0xc8, 0x4e, 0x17, 0xe4, 0xea, 0xe1, 0xa3, 0x9a, 0xc3, 0x47, 0xdc, 0x72, 0x72, 0xca, 0x11, 0x9c, 0x59, 0x04, + 0xcd, 0x0c, 0xa0, 0x02, 0xf2, 0x12, 0x9a, 0x92, 0x2e, 0x08, 0x7e, 0x6d, 0x90, 0x34, 0x1f, 0x30, 0x06, 0xe0, 0xa3, + 0xbe, 0xd3, 0x9c, 0xbf, 0x19, 0x9c, 0xee, 0x44, 0xbc, 0xb7, 0xa8, 0xe2, 0x97, 0x56, 0xca, 0x90, 0x29, 0x4f, 0x2e, + 0xd9, 0x2a, 0xac, 0x42, 0xd5, 0xda, 0xae, 0x43, 0x09, 0xf1, 0x19, 0xed, 0x0f, 0x2e, 0x28, 0xde, 0xc1, 0x40, 0x7d, + 0xe1, 0x47, 0xde, 0x69, 0xbd, 0x8a, 0x66, 0x2d, 0x6c, 0xbd, 0xf8, 0xbe, 0x6a, 0x5a, 0x03, 0x47, 0x76, 0xb6, 0x57, + 0xfa, 0x67, 0x75, 0x18, 0xad, 0x43, 0x54, 0xfe, 0xac, 0xfe, 0x4a, 0x37, 0x75, 0xcb, 0x9a, 0xc6, 0xaf, 0x23, 0xf1, + 0x9b, 0x24, 0x4c, 0xea, 0xb5, 0x5b, 0xd3, 0xe3, 0xf4, 0x38, 0xd1, 0x38, 0x75, 0x72, 0xf7, 0xfc, 0xd7, 0x68, 0x75, + 0xd4, 0xa0, 0xed, 0x64, 0xda, 0xa6, 0xdf, 0x35, 0x96, 0x28, 0x4d, 0xaa, 0xa7, 0xb1, 0x73, 0x6d, 0x17, 0x2f, 0x16, + 0x1d, 0x12, 0xdd, 0x9f, 0x75, 0x5f, 0x91, 0xb9, 0x16, 0x26, 0x7e, 0x66, 0x52, 0x43, 0x5c, 0x6b, 0x35, 0xf1, 0xce, + 0x5e, 0x6c, 0x4b, 0x8e, 0xdd, 0x74, 0x95, 0x64, 0x30, 0xa8, 0x8e, 0x4c, 0x0d, 0x89, 0x64, 0x88, 0xa8, 0x5f, 0x3e, + 0x08, 0x98, 0x75, 0x8d, 0x77, 0xcf, 0xd7, 0xa4, 0x71, 0xfa, 0xd6, 0x63, 0xae, 0x3f, 0x2f, 0xc3, 0xed, 0x7b, 0x04, + 0xce, 0xb6, 0x29, 0xfd, 0xe8, 0x8d, 0x52, 0xa7, 0x8d, 0x92, 0x58, 0x4e, 0xd3, 0x13, 0x28, 0xff, 0x80, 0x48, 0x22, + 0xfc, 0xa4, 0x29, 0x3b, 0x49, 0x25, 0xd3, 0x6f, 0xd4, 0xdd, 0x7e, 0xaf, 0x84, 0x40, 0x7a, 0xfb, 0x47, 0x1d, 0x55, + 0xd3, 0xcb, 0x44, 0x12, 0xab, 0x0e, 0xc4, 0x6b, 0x0a, 0x43, 0xee, 0xf3, 0x2f, 0xb6, 0x77, 0xca, 0x28, 0x14, 0x51, + 0xd6, 0x92, 0xde, 0x01, 0x4c, 0x43, 0x0d, 0x23, 0xa3, 0x68, 0xd8, 0x26, 0xe5, 0xef, 0xf1, 0xc7, 0xd9, 0x30, 0xa0, + 0x4d, 0x47, 0xa5, 0x0d, 0x5d, 0xb0, 0xaa, 0xde, 0xc2, 0xef, 0xd3, 0x53, 0x5f, 0xb0, 0xe6, 0x15, 0xf6, 0x4e, 0xdf, + 0xde, 0xe6, 0xcf, 0xe7, 0xfc, 0xfc, 0xf9, 0xac, 0x37, 0xbc, 0x61, 0x66, 0x65, 0xdc, 0xab, 0xe0, 0xe5, 0x82, 0xae, + 0x71, 0x28, 0xc1, 0x53, 0x5b, 0xfe, 0xa3, 0x13, 0x30, 0xe5, 0x01, 0xce, 0x68, 0x03, 0x7d, 0x2a, 0x03, 0xa7, 0x9b, + 0x1b, 0x66, 0x34, 0x5d, 0x99, 0x19, 0x69, 0x66, 0x3c, 0x29, 0xa2, 0xcf, 0x49, 0xcc, 0xc1, 0x1e, 0xc9, 0x59, 0xfa, + 0x58, 0xcc, 0xf8, 0x51, 0x69, 0x0b, 0xda, 0x0e, 0x85, 0x9f, 0x82, 0x4c, 0x05, 0xe8, 0x45, 0xe7, 0xdb, 0x38, 0x8d, + 0xb3, 0xf4, 0x77, 0x0e, 0xe9, 0x48, 0x4f, 0x4f, 0x44, 0xf6, 0xa0, 0xbb, 0xee, 0xbd, 0x17, 0xf0, 0x4b, 0x42, 0x53, + 0x32, 0x7e, 0x27, 0x06, 0xed, 0x8b, 0xf4, 0x51, 0x8d, 0xc0, 0xa9, 0x00, 0x79, 0x35, 0xc2, 0x38, 0x90, 0x37, 0xb4, + 0xd7, 0xc8, 0x0f, 0x4a, 0x95, 0xee, 0xb9, 0xa7, 0x25, 0xad, 0xc8, 0x42, 0xa6, 0x9f, 0x8c, 0x31, 0x66, 0x55, 0xe4, + 0xd8, 0xd2, 0xbc, 0x6f, 0x90, 0x49, 0xbe, 0x70, 0x91, 0xd1, 0x62, 0x4e, 0x8d, 0x05, 0xba, 0x55, 0xa8, 0xb5, 0x0b, + 0xaf, 0x7f, 0xa1, 0x72, 0xa0, 0xa9, 0x28, 0xfb, 0x7e, 0x88, 0x2d, 0xe2, 0x03, 0xfd, 0x8a, 0x8f, 0x90, 0x71, 0xdb, + 0x73, 0x9c, 0x10, 0x52, 0xf5, 0xae, 0x28, 0xee, 0x6d, 0x93, 0x0a, 0xc9, 0x0d, 0x55, 0x0c, 0x65, 0xd4, 0xc2, 0xf9, + 0x19, 0x9c, 0x2f, 0x9c, 0x9f, 0xe6, 0xdc, 0xa0, 0x2d, 0x99, 0xaa, 0x67, 0x24, 0x96, 0xae, 0xb0, 0xa3, 0x96, 0xdf, + 0xe4, 0x27, 0xec, 0x42, 0x06, 0x68, 0x6a, 0xa5, 0x57, 0x45, 0x82, 0x2e, 0x83, 0x0d, 0xa8, 0x51, 0x1d, 0x88, 0xbc, + 0xc4, 0x37, 0x13, 0x10, 0x80, 0xd1, 0x83, 0x4f, 0xaa, 0x29, 0x9d, 0x36, 0x7c, 0xb7, 0xcb, 0x31, 0x81, 0xa2, 0x6b, + 0x36, 0x98, 0x84, 0xbc, 0x29, 0xb8, 0xa6, 0x9a, 0x3d, 0x15, 0xc2, 0x18, 0xbc, 0x3c, 0x35, 0xb6, 0x58, 0xbd, 0x7f, + 0x2b, 0xd6, 0x57, 0x86, 0x90, 0xd8, 0x72, 0xc8, 0xbe, 0xd0, 0xbc, 0xd2, 0x83, 0x68, 0x9a, 0xe6, 0xe4, 0xd2, 0x43, + 0x5f, 0xc8, 0xeb, 0xd1, 0xd9, 0x27, 0xc8, 0xeb, 0xdb, 0x6c, 0x5b, 0x73, 0x13, 0x36, 0xf1, 0x25, 0x7d, 0xa6, 0xfb, + 0xe7, 0x6a, 0x21, 0x7b, 0x56, 0xea, 0xbc, 0x73, 0x25, 0x76, 0x4d, 0xa7, 0x88, 0x1a, 0x83, 0x4e, 0xc1, 0xdb, 0x0e, + 0x11, 0xb4, 0x05, 0x27, 0x49, 0x86, 0x48, 0x54, 0x06, 0xea, 0xb3, 0xa9, 0x48, 0x82, 0xd9, 0x00, 0x4b, 0x25, 0xaf, + 0xb9, 0xd8, 0x35, 0xbf, 0x64, 0x4d, 0x32, 0xab, 0x80, 0x8b, 0xe4, 0x99, 0x4e, 0x4e, 0xd7, 0x91, 0xd5, 0x1e, 0xa6, + 0xc6, 0x5d, 0x2c, 0x5e, 0x25, 0x5c, 0xce, 0xca, 0x4d, 0xac, 0xc4, 0x9b, 0x40, 0xcd, 0x78, 0x4f, 0x2a, 0x7f, 0x6c, + 0xb2, 0xa3, 0x36, 0x52, 0x02, 0x6d, 0x0f, 0xa9, 0xb6, 0x36, 0x8d, 0x70, 0x1b, 0xd2, 0x6f, 0x57, 0xb7, 0x2d, 0x50, + 0xe9, 0xb7, 0xb4, 0x30, 0xa4, 0xff, 0x1b, 0x15, 0xaa, 0x46, 0x85, 0x11, 0xc2, 0xfd, 0x24, 0x40, 0xb8, 0x2f, 0x9c, + 0xbc, 0x20, 0x16, 0xd5, 0x79, 0x14, 0xf6, 0x5e, 0x67, 0xcd, 0xd5, 0xb8, 0xf8, 0xfb, 0xa0, 0xfe, 0x3e, 0x0a, 0x8d, + 0x63, 0xbd, 0xc6, 0xef, 0x8c, 0x1f, 0x7f, 0x64, 0xdf, 0xd0, 0xc0, 0x08, 0x37, 0x11, 0xb4, 0x12, 0x34, 0xdb, 0x12, + 0xd6, 0xb6, 0x2a, 0xa0, 0x08, 0x61, 0x36, 0x52, 0xd5, 0x82, 0x09, 0x6d, 0xa5, 0x27, 0x58, 0xbc, 0xeb, 0x38, 0xfd, + 0x6f, 0x68, 0xbd, 0x4e, 0x08, 0x29, 0x58, 0x93, 0x23, 0x4f, 0x9e, 0x44, 0xab, 0x7d, 0xe6, 0xdf, 0x18, 0xb7, 0xbe, + 0xfa, 0x8c, 0x57, 0x23, 0x75, 0xa4, 0x98, 0x41, 0xe1, 0xb5, 0x9b, 0xd3, 0x9b, 0xf1, 0x39, 0xc9, 0x7b, 0xd1, 0x3c, + 0xda, 0xa9, 0xa0, 0x54, 0x53, 0xd7, 0xac, 0xce, 0xb5, 0x79, 0x9d, 0xd1, 0xb9, 0xc6, 0xde, 0x58, 0xd6, 0xd3, 0x35, + 0xce, 0xf8, 0x8d, 0xb6, 0x62, 0xa0, 0xd4, 0xf1, 0xb0, 0xd1, 0x73, 0xac, 0x40, 0x06, 0xe8, 0x85, 0xe3, 0x26, 0x82, + 0xf4, 0x97, 0xc0, 0xa1, 0x53, 0x1b, 0x2e, 0xb0, 0xd6, 0x72, 0xc4, 0x90, 0x67, 0x58, 0x62, 0x4a, 0xbf, 0x71, 0x1d, + 0x48, 0xbb, 0xf5, 0x9b, 0x05, 0x8f, 0x82, 0xaf, 0xec, 0xe9, 0x30, 0x8f, 0x68, 0x9c, 0x5b, 0x04, 0x2f, 0x12, 0xe5, + 0x61, 0xbb, 0xf0, 0x9c, 0x5f, 0x89, 0x74, 0x50, 0x90, 0x65, 0x3c, 0x9f, 0x79, 0x01, 0x42, 0x48, 0x77, 0x2d, 0xa1, + 0xed, 0x73, 0xc1, 0x9e, 0x18, 0xd7, 0x8e, 0x49, 0x52, 0x53, 0x82, 0xfd, 0xdf, 0x36, 0x5d, 0x96, 0x56, 0xe7, 0x2f, + 0xef, 0x2b, 0x66, 0x62, 0x3b, 0xae, 0xce, 0x52, 0x21, 0x7b, 0xef, 0x57, 0x91, 0x78, 0x8c, 0xcc, 0x1f, 0xdb, 0x20, + 0x7e, 0xef, 0x9c, 0x72, 0xfc, 0x5f, 0xd8, 0x6f, 0x7a, 0xf4, 0xca, 0xc9, 0x6c, 0x23, 0x01, 0x93, 0x23, 0xf7, 0xaa, + 0xbe, 0x1f, 0x01, 0x7b, 0xc3, 0x03, 0x81, 0xb2, 0x8a, 0xfe, 0x83, 0x7a, 0xd3, 0x00, 0x60, 0x0a, 0xc3, 0x6d, 0xb8, + 0xe7, 0x8f, 0xc6, 0x6f, 0x75, 0xc0, 0xe5, 0x8a, 0xe5, 0xbf, 0x81, 0xc1, 0xf5, 0x3a, 0x22, 0xd8, 0x6f, 0x9d, 0xf5, + 0x40, 0xd0, 0x9d, 0xc7, 0x9c, 0x62, 0x10, 0xd7, 0x92, 0x2f, 0x58, 0xaf, 0x22, 0xf3, 0x18, 0xc5, 0xe6, 0x17, 0x6b, + 0x2b, 0xf8, 0x2a, 0x93, 0xfa, 0x45, 0x1e, 0xfc, 0x17, 0xa4, 0x76, 0x08, 0x87, 0xe7, 0x89, 0x45, 0xfe, 0x4d, 0xe2, + 0x70, 0x84, 0x05, 0xb6, 0x62, 0xa5, 0xa1, 0x39, 0x33, 0x7e, 0x4c, 0xc9, 0xa1, 0x4d, 0x30, 0x0e, 0x45, 0xce, 0xd6, + 0x1c, 0x2c, 0x47, 0xa9, 0x66, 0x9e, 0x7f, 0x6f, 0xf0, 0x41, 0x98, 0xb4, 0xb4, 0xf2, 0x7c, 0x80, 0xf6, 0x31, 0xfa, + 0xf3, 0x7f, 0x16, 0x87, 0x0d, 0xc3, 0xb2, 0xf7, 0x6e, 0xe2, 0x27, 0x1b, 0x38, 0xaa, 0x79, 0x52, 0xc2, 0xd5, 0x5b, + 0xab, 0xaf, 0xda, 0x96, 0x1e, 0x3f, 0x09, 0x85, 0xc6, 0x30, 0x46, 0x0b, 0x83, 0x81, 0x3b, 0x17, 0xfb, 0x39, 0x98, + 0xb9, 0x61, 0x1b, 0x7d, 0x23, 0xe1, 0x4b, 0x3e, 0x7f, 0x07, 0xea, 0x10, 0xa3, 0xa6, 0x4b, 0x23, 0x2a, 0xfd, 0x0e, + 0x45, 0xb7, 0x06, 0x14, 0x68, 0x9e, 0xf9, 0x1c, 0x0a, 0xa7, 0xa3, 0x48, 0x24, 0x39, 0xc0, 0xda, 0x99, 0x7e, 0xd6, + 0xb2, 0xc7, 0xef, 0xb3, 0xa5, 0xc3, 0xf0, 0xba, 0xb6, 0x3d, 0x1e, 0x73, 0xe5, 0x56, 0x56, 0x1d, 0x17, 0x50, 0x5f, + 0x96, 0x6d, 0x36, 0xf6, 0x8e, 0x50, 0x67, 0xab, 0x87, 0x22, 0x72, 0x86, 0x78, 0x90, 0x58, 0xdd, 0xa0, 0x8f, 0x54, + 0xb0, 0xce, 0x67, 0x1b, 0x34, 0xf9, 0x56, 0xd1, 0x8b, 0xab, 0x85, 0xcd, 0x69, 0x48, 0x88, 0x69, 0xc4, 0x70, 0xf0, + 0x49, 0x84, 0xce, 0xa4, 0x7d, 0xdc, 0x50, 0x9d, 0x38, 0x43, 0xd2, 0x70, 0x1d, 0x71, 0x5a, 0x55, 0xc2, 0xac, 0xb2, + 0x85, 0xc5, 0x53, 0xda, 0xe1, 0xea, 0xae, 0x70, 0x3b, 0x67, 0xc2, 0x51, 0xcb, 0x35, 0xb4, 0x4d, 0x44, 0x0a, 0xd9, + 0x61, 0xcb, 0x35, 0xfa, 0xea, 0xb0, 0x62, 0x85, 0x8c, 0xb7, 0xf3, 0xe2, 0x55, 0xcc, 0x38, 0x6c, 0x09, 0x4b, 0x71, + 0x80, 0x81, 0x0f, 0x6d, 0xe5, 0x7d, 0xd5, 0xc9, 0xa9, 0x70, 0x4e, 0x79, 0x97, 0x52, 0x82, 0x2d, 0x63, 0xff, 0xdc, + 0xd5, 0xab, 0xf3, 0xcb, 0xb9, 0xab, 0xce, 0x78, 0x73, 0x61, 0xea, 0xb4, 0xbe, 0x84, 0xae, 0xed, 0x10, 0x51, 0xe5, + 0x3e, 0x57, 0xd3, 0x71, 0x6f, 0xb1, 0x86, 0x9e, 0x74, 0x8e, 0x89, 0xfe, 0xbf, 0x42, 0x94, 0x8f, 0x08, 0x9d, 0xdc, + 0xdd, 0x29, 0x5f, 0x95, 0x3c, 0x55, 0x49, 0xec, 0x63, 0xb5, 0x0d, 0x23, 0x83, 0x56, 0xda, 0x89, 0x6a, 0xdf, 0x5e, + 0xee, 0x09, 0x62, 0xc8, 0x5b, 0x62, 0x59, 0xb8, 0x5d, 0x5e, 0x96, 0xdc, 0x21, 0xce, 0xed, 0x64, 0x68, 0xa7, 0x63, + 0x34, 0x42, 0x3f, 0xb4, 0xa5, 0x98, 0x04, 0x44, 0x52, 0xfb, 0x09, 0xe9, 0x1c, 0xfe, 0x2e, 0x7b, 0x7f, 0x16, 0xef, + 0x09, 0x61, 0x3e, 0x7a, 0xd1, 0x31, 0xa8, 0x4b, 0xa8, 0x73, 0xbc, 0xce, 0xab, 0x06, 0x4c, 0x12, 0x4d, 0xaf, 0xad, + 0x38, 0xd5, 0x39, 0xf5, 0xb6, 0x08, 0xc5, 0x2e, 0xfd, 0xa2, 0x25, 0xb9, 0xd9, 0x2c, 0x33, 0x66, 0x0c, 0x02, 0x75, + 0xa8, 0xe8, 0x66, 0x80, 0x62, 0x4c, 0x89, 0xb0, 0xd3, 0xf9, 0x87, 0x4c, 0xaa, 0x29, 0x2d, 0xaa, 0x76, 0xf4, 0xfb, + 0xc6, 0x60, 0x87, 0x47, 0xd3, 0x97, 0x3f, 0xbf, 0x3d, 0xd2, 0x83, 0x2a, 0xe8, 0x10, 0x3e, 0xee, 0xee, 0x8e, 0xa1, + 0x50, 0x80, 0xac, 0x6c, 0x5f, 0xcc, 0x00, 0x6a, 0x4c, 0x45, 0x48, 0x77, 0x6d, 0xdd, 0x5f, 0x4a, 0x72, 0x5b, 0x53, + 0xe5, 0xfb, 0x40, 0x83, 0xef, 0x0d, 0xb5, 0xd3, 0x1d, 0x3e, 0x87, 0xd9, 0x88, 0xa7, 0x40, 0xc7, 0xc2, 0xe0, 0x6f, + 0x48, 0x71, 0x13, 0x06, 0x19, 0xaa, 0x64, 0x9a, 0x3d, 0xa5, 0x2d, 0xab, 0xe6, 0x5a, 0x4a, 0x3a, 0xc7, 0x84, 0xbd, + 0x2a, 0xfc, 0x91, 0xf7, 0x24, 0xb5, 0xa5, 0x1a, 0x0c, 0x70, 0x82, 0xd2, 0x86, 0xe5, 0x58, 0xc5, 0x8d, 0x7c, 0xa7, + 0xf0, 0x22, 0x02, 0x3d, 0x1d, 0xdc, 0xdb, 0xf9, 0xfd, 0xde, 0x18, 0x21, 0x48, 0x05, 0xdf, 0x4a, 0xa9, 0xc9, 0x1a, + 0x9e, 0xfb, 0x47, 0xaf, 0x6c, 0x87, 0x47, 0xba, 0x9b, 0x24, 0x6a, 0x8b, 0x4e, 0x54, 0x80, 0x15, 0x88, 0xa6, 0x80, + 0x0b, 0xd5, 0x31, 0xa6, 0x71, 0xe7, 0x77, 0x3f, 0xb1, 0xd6, 0xdd, 0xea, 0xf5, 0xac, 0x97, 0x4e, 0x1e, 0x93, 0x05, + 0x6a, 0x3c, 0x8a, 0x7d, 0x79, 0x15, 0xbe, 0x5b, 0xf6, 0x9b, 0x95, 0x2d, 0xc8, 0x0c, 0x02, 0xf4, 0x9b, 0xb5, 0x39, + 0x13, 0xbd, 0x46, 0xb8, 0x93, 0x4a, 0xf3, 0xbc, 0x92, 0x33, 0x95, 0x5f, 0x5f, 0x39, 0x8b, 0x21, 0x59, 0xed, 0xac, + 0xdd, 0xa8, 0x48, 0x8f, 0xad, 0x41, 0xd6, 0xaf, 0x99, 0x64, 0xa9, 0xff, 0x35, 0x7c, 0xd4, 0x37, 0xaf, 0xd7, 0x60, + 0xda, 0x76, 0xb5, 0xd3, 0xcb, 0x53, 0x8e, 0x8a, 0x39, 0x2f, 0x7e, 0x61, 0x8d, 0x2d, 0x3c, 0x1e, 0x6c, 0xf4, 0x84, + 0xc9, 0x54, 0xb2, 0x7a, 0x56, 0xc9, 0xca, 0x59, 0xe2, 0x72, 0xb3, 0x17, 0x5d, 0x40, 0xc7, 0x1f, 0x0e, 0x5a, 0x95, + 0x3f, 0x6c, 0xcc, 0xaa, 0x7c, 0xd8, 0x49, 0xd5, 0xfa, 0x24, 0x91, 0xd9, 0x33, 0x6b, 0xe4, 0x61, 0x61, 0xad, 0x98, + 0x4c, 0xf2, 0x7d, 0x42, 0xae, 0xd0, 0x0c, 0xab, 0x6a, 0xd5, 0xe1, 0xc9, 0x0d, 0x37, 0xb8, 0x58, 0xf8, 0xb9, 0x19, + 0xd7, 0x7f, 0x46, 0xdc, 0x59, 0x0e, 0x3a, 0x0b, 0xad, 0xbf, 0xbd, 0x0e, 0x75, 0x3f, 0x82, 0x2f, 0x4d, 0x70, 0x65, + 0xfa, 0x16, 0x5c, 0xfd, 0x4a, 0x92, 0xd9, 0x16, 0x78, 0xad, 0x00, 0xb9, 0xd8, 0x1b, 0x1b, 0xb1, 0xd6, 0x92, 0x44, + 0x63, 0x43, 0x90, 0x3a, 0x8b, 0xb4, 0x1b, 0x52, 0x3b, 0x9a, 0xed, 0xb4, 0x8e, 0xe6, 0x27, 0xfc, 0x8d, 0x3f, 0x55, + 0x43, 0x15, 0xe6, 0x5b, 0x85, 0xea, 0x15, 0x0f, 0x4e, 0x5b, 0x6f, 0x35, 0x8b, 0xf3, 0x4d, 0xb0, 0xd2, 0x8a, 0xa8, + 0x08, 0x8d, 0xc1, 0x17, 0x19, 0x1c, 0xc4, 0xfd, 0x8a, 0xb5, 0x82, 0x74, 0x53, 0xd6, 0xed, 0x7f, 0x0d, 0xb5, 0xd2, + 0xee, 0x40, 0xec, 0x1b, 0x74, 0x81, 0x95, 0xb5, 0x02, 0xb9, 0x87, 0xf5, 0xfe, 0x82, 0xd2, 0x0a, 0x71, 0xe1, 0xcc, + 0x11, 0x35, 0x61, 0xad, 0xf7, 0x88, 0xb7, 0xc8, 0xfa, 0xcb, 0x3f, 0xd3, 0x8b, 0x26, 0xce, 0xe2, 0x61, 0x19, 0xe7, + 0x0e, 0xd9, 0x91, 0xcb, 0x2c, 0x9f, 0xae, 0xbc, 0xd5, 0x22, 0x82, 0x86, 0x3c, 0x99, 0xf6, 0xf8, 0x14, 0x4e, 0x9b, + 0x35, 0x9c, 0x9e, 0xc8, 0xa7, 0xd6, 0x5a, 0xd3, 0xc9, 0xaa, 0xe1, 0x1f, 0x70, 0xc1, 0x05, 0x86, 0x1d, 0x0c, 0x4e, + 0xaf, 0x9c, 0xaf, 0xba, 0xa0, 0x49, 0x4f, 0x58, 0x70, 0x06, 0xcd, 0x6d, 0xc0, 0x93, 0x0f, 0xe9, 0x29, 0x75, 0x77, + 0x76, 0x9b, 0xd7, 0x40, 0x6e, 0x13, 0x7d, 0x6a, 0x31, 0xcf, 0x0a, 0x5b, 0x70, 0xa6, 0xce, 0x6e, 0x63, 0x7a, 0xae, + 0xae, 0xdb, 0x56, 0x82, 0xa4, 0x4d, 0x9e, 0xcf, 0x06, 0xd7, 0x8c, 0x14, 0x86, 0xc1, 0xff, 0x97, 0x90, 0x92, 0xb7, + 0xa2, 0x20, 0x98, 0x3a, 0x27, 0x7d, 0xad, 0x17, 0x57, 0xb8, 0x11, 0xb1, 0xcc, 0xaa, 0x23, 0xa8, 0x52, 0xf6, 0x04, + 0x5d, 0xfa, 0xdc, 0xc1, 0x25, 0x27, 0x62, 0xbb, 0x67, 0xa5, 0x33, 0x29, 0xa1, 0xfd, 0x79, 0xc1, 0xbb, 0x6b, 0xbc, + 0x72, 0x47, 0xf6, 0xc7, 0xca, 0x3d, 0xe3, 0x1d, 0xb8, 0x7a, 0xf6, 0xe7, 0x38, 0x6b, 0xe1, 0xa0, 0xcb, 0x30, 0x8f, + 0x27, 0x3d, 0x3c, 0xcb, 0x3f, 0xe1, 0x59, 0x39, 0xcf, 0x18, 0x82, 0xd6, 0x61, 0x85, 0x6f, 0xbe, 0x06, 0x28, 0xef, + 0x64, 0xf8, 0xf8, 0x58, 0xfc, 0xd6, 0xd8, 0x8b, 0x4e, 0xca, 0x21, 0x9a, 0xa9, 0x1d, 0x34, 0xcf, 0x5b, 0x30, 0xe4, + 0xa9, 0xdd, 0x20, 0x90, 0x46, 0xeb, 0x3c, 0x57, 0x3f, 0xc5, 0x41, 0x35, 0x7f, 0x9b, 0x79, 0x09, 0x73, 0x5b, 0xa1, + 0x88, 0xfc, 0x33, 0x21, 0x9a, 0xfd, 0x48, 0xa5, 0x81, 0x3a, 0xf9, 0x55, 0x4b, 0xf2, 0x95, 0xb7, 0x23, 0x06, 0x9d, + 0xb9, 0x09, 0xbb, 0xd8, 0x08, 0xf3, 0xd3, 0x98, 0x7c, 0xa6, 0x3a, 0x9b, 0xc9, 0x32, 0xcb, 0x6a, 0x1f, 0x13, 0x0f, + 0x8f, 0xd6, 0x4b, 0xaa, 0x5b, 0x14, 0x6a, 0xb3, 0x3c, 0x5f, 0x94, 0x59, 0xa9, 0x7d, 0x4e, 0xbd, 0x10, 0x47, 0x93, + 0xf5, 0xc2, 0xe3, 0x5e, 0x62, 0x46, 0x26, 0xd5, 0xbc, 0xcc, 0x1c, 0x22, 0x0f, 0xcf, 0x1f, 0x7c, 0xcb, 0x2e, 0x79, + 0xa2, 0xa0, 0xb4, 0x1d, 0x32, 0x0f, 0xdc, 0x37, 0x98, 0xae, 0x9c, 0x7a, 0xcc, 0xd3, 0x15, 0x70, 0x6b, 0xc0, 0x6c, + 0x69, 0x14, 0x47, 0x56, 0x59, 0x85, 0xac, 0xeb, 0xf5, 0xba, 0xf2, 0xb9, 0x65, 0x9a, 0x09, 0x37, 0xf6, 0x14, 0x64, + 0x9a, 0xae, 0x4a, 0xd7, 0xd2, 0x67, 0xfe, 0xcd, 0x9c, 0x67, 0x1f, 0xf0, 0xd3, 0x4f, 0xc1, 0x2d, 0xfa, 0xcb, 0xa9, + 0x6b, 0x5c, 0xf9, 0x36, 0xa3, 0x51, 0xe3, 0x14, 0x8d, 0x37, 0x48, 0x4c, 0x54, 0x54, 0x85, 0xd5, 0x98, 0xf2, 0x73, + 0xec, 0xdd, 0x48, 0x4e, 0xa6, 0x43, 0x3e, 0xd7, 0x76, 0x3f, 0xb3, 0x66, 0xf5, 0x19, 0x75, 0x68, 0x95, 0xd5, 0x71, + 0xc4, 0x97, 0xce, 0x6e, 0x57, 0x06, 0xa1, 0x00, 0x04, 0xd8, 0xc3, 0xe4, 0x73, 0xca, 0x5a, 0x4d, 0xfe, 0xfc, 0xfb, + 0xfb, 0x47, 0x15, 0x9c, 0x62, 0x95, 0xf7, 0xdd, 0xd8, 0x04, 0x8b, 0x64, 0x46, 0x18, 0x59, 0x23, 0xbb, 0x39, 0x46, + 0x92, 0x22, 0x44, 0xe3, 0x1e, 0x4b, 0x11, 0x7a, 0xab, 0xfb, 0x01, 0xe0, 0x1c, 0x79, 0x52, 0x9c, 0x26, 0x47, 0xa7, + 0xc8, 0xa6, 0xd9, 0x56, 0x6c, 0x91, 0x85, 0x03, 0x7c, 0x2d, 0x6a, 0x25, 0xdb, 0xc6, 0x58, 0x41, 0x83, 0x62, 0x0e, + 0x64, 0x3a, 0xf3, 0x01, 0x5f, 0x31, 0xe2, 0x9c, 0x3f, 0x4c, 0x1b, 0x93, 0x27, 0xd3, 0x5e, 0x5f, 0x25, 0xcc, 0x6c, + 0xb7, 0x5e, 0x30, 0x9c, 0xd3, 0x0c, 0x0c, 0xc8, 0xc7, 0x15, 0xaa, 0xf9, 0x13, 0x2c, 0x51, 0xf0, 0xb7, 0x36, 0xb2, + 0xf3, 0xe7, 0xa4, 0x36, 0x62, 0xc8, 0x98, 0x68, 0x6c, 0x2f, 0x8c, 0x94, 0x82, 0x17, 0x35, 0x74, 0x46, 0x58, 0x04, + 0x1f, 0xec, 0x9e, 0xc2, 0xf5, 0x59, 0xd9, 0xeb, 0x74, 0x12, 0x3d, 0x30, 0x4f, 0x94, 0xe0, 0xd2, 0x7c, 0x5f, 0xdb, + 0x20, 0xa0, 0x3e, 0x6f, 0x79, 0x26, 0x07, 0x24, 0x25, 0x26, 0xb0, 0xf0, 0xb8, 0x29, 0x5f, 0xe3, 0xd4, 0x5b, 0xef, + 0xb2, 0x1a, 0x75, 0xc5, 0x25, 0x8d, 0x36, 0xce, 0x18, 0x34, 0x18, 0x1d, 0x11, 0x89, 0xe7, 0x42, 0x30, 0x46, 0xc3, + 0xdf, 0x7a, 0x24, 0x69, 0x08, 0xce, 0x63, 0x4f, 0x10, 0x37, 0x39, 0x99, 0xde, 0x40, 0x88, 0xb2, 0x6d, 0xb9, 0xf9, + 0x79, 0x5f, 0xa0, 0xd1, 0x9c, 0x8f, 0x4d, 0xcc, 0x9c, 0xf7, 0x00, 0x65, 0x26, 0x5a, 0x04, 0xe4, 0xd0, 0xe3, 0x1e, + 0xe2, 0x2a, 0x3d, 0x58, 0xec, 0x25, 0x2e, 0xd3, 0x31, 0x10, 0x5f, 0xaf, 0x95, 0x82, 0x34, 0x3b, 0x8b, 0x14, 0x78, + 0x31, 0xdf, 0xfc, 0xc9, 0x95, 0x62, 0x95, 0x7c, 0xd3, 0x60, 0x72, 0xfe, 0xe4, 0xc7, 0xe6, 0x97, 0xe0, 0xe5, 0x5b, + 0x2d, 0xb5, 0xc8, 0x7d, 0xe0, 0x9d, 0xaf, 0x49, 0x41, 0xbb, 0xff, 0xd9, 0x92, 0x91, 0xf7, 0x31, 0xad, 0x96, 0xc5, + 0x5b, 0xed, 0xa2, 0x5b, 0x14, 0xf2, 0x26, 0x0f, 0xf7, 0xb0, 0x08, 0xa9, 0xb5, 0x96, 0x61, 0x56, 0xdb, 0xa3, 0xdc, + 0xd8, 0x7b, 0xbd, 0x16, 0xa4, 0x45, 0xcc, 0x2e, 0x51, 0xe5, 0xc6, 0x0b, 0x4c, 0xd6, 0x9f, 0x5c, 0x08, 0x96, 0xf9, + 0x05, 0x55, 0x69, 0xef, 0xb2, 0x8e, 0xa7, 0x6c, 0x66, 0xad, 0x8b, 0x9a, 0x4d, 0x01, 0xa7, 0x28, 0x2b, 0x55, 0xdc, + 0xc8, 0xe0, 0xbb, 0x46, 0xa0, 0x35, 0xf0, 0x13, 0x18, 0xa5, 0xc8, 0x6a, 0xaa, 0x8d, 0xa4, 0xff, 0xce, 0xe4, 0xdf, + 0x39, 0xe6, 0xbf, 0x41, 0xe6, 0xdf, 0x87, 0x56, 0x7e, 0xdf, 0x18, 0x6b, 0x02, 0x5c, 0xe1, 0xa4, 0x10, 0x5f, 0xa9, + 0x9c, 0x25, 0x80, 0x1a, 0x4d, 0x99, 0xec, 0xc6, 0x0b, 0x81, 0x15, 0x91, 0xe7, 0x36, 0x4e, 0xb3, 0xb4, 0x47, 0xb6, + 0xe8, 0xfe, 0xce, 0x0b, 0x70, 0x42, 0x2e, 0x0a, 0xee, 0x88, 0xed, 0xab, 0x31, 0xe7, 0x50, 0xc4, 0xd9, 0xe4, 0xa2, + 0x00, 0x31, 0x82, 0x01, 0x21, 0x1b, 0x49, 0xa0, 0xa3, 0xa4, 0x99, 0x68, 0xc4, 0x14, 0x80, 0x06, 0xd8, 0xdd, 0x03, + 0x04, 0x16, 0xc1, 0x0c, 0x13, 0x04, 0x23, 0x79, 0x25, 0xc0, 0x72, 0x4c, 0xf6, 0x8e, 0x55, 0xb0, 0xb0, 0x52, 0x07, + 0x3b, 0xd0, 0x20, 0x4e, 0x60, 0x8a, 0x66, 0x79, 0x24, 0x28, 0xaa, 0x60, 0x11, 0x25, 0xcb, 0x36, 0x17, 0x2f, 0x32, + 0xb7, 0xf5, 0x2a, 0x49, 0xa1, 0x8b, 0xa7, 0x4f, 0x33, 0x4b, 0x28, 0xfd, 0x03, 0xf0, 0xaf, 0x41, 0x1d, 0xd8, 0xb3, + 0x0e, 0xa0, 0x63, 0x2b, 0x4e, 0x4e, 0xa5, 0xca, 0x9f, 0x5d, 0x03, 0x40, 0x49, 0x4f, 0x1b, 0xc4, 0x5c, 0xa0, 0x75, + 0x0d, 0x71, 0x0d, 0x2a, 0x80, 0x61, 0x93, 0xf1, 0x52, 0x53, 0xdb, 0x7a, 0x66, 0xf1, 0x52, 0xef, 0x91, 0x99, 0xa3, + 0x43, 0x12, 0x2f, 0xa2, 0xc4, 0x5d, 0x14, 0x96, 0x23, 0xa5, 0xd6, 0xdc, 0x28, 0xd6, 0x98, 0xf2, 0xd2, 0x6e, 0x0e, + 0xf1, 0x1d, 0xa2, 0xd3, 0x45, 0x50, 0xf5, 0x79, 0x8b, 0xa7, 0xb5, 0x11, 0xf8, 0x91, 0xd3, 0xa2, 0x40, 0x79, 0xbb, + 0xe2, 0xa4, 0xa6, 0x27, 0x3b, 0x56, 0xd8, 0x34, 0x2d, 0xbd, 0x83, 0x5b, 0x4f, 0xdf, 0x96, 0x64, 0x90, 0x71, 0x20, + 0xb0, 0x23, 0x20, 0x6c, 0x8a, 0x3b, 0x33, 0xd1, 0x16, 0x47, 0x70, 0x82, 0x50, 0x46, 0x66, 0x87, 0x6f, 0x05, 0xcf, + 0x2a, 0x02, 0x9f, 0xf7, 0xa3, 0xf7, 0x9c, 0xeb, 0x6a, 0x28, 0xad, 0x8e, 0x3d, 0x6a, 0x24, 0x38, 0xca, 0xb3, 0xa6, + 0x6f, 0x38, 0xa7, 0x16, 0x21, 0x55, 0x71, 0xbf, 0x00, 0x2b, 0xb7, 0xf7, 0x49, 0x83, 0x15, 0x9f, 0xb1, 0x6c, 0x0f, + 0xb2, 0x95, 0x32, 0xa2, 0x91, 0xf2, 0xba, 0xc7, 0xcc, 0x68, 0x7b, 0xc1, 0xc8, 0x8d, 0xb9, 0xe1, 0xfd, 0xec, 0x31, + 0x8a, 0xea, 0x15, 0x46, 0xac, 0x16, 0xdb, 0x09, 0x30, 0xf7, 0xc6, 0xbd, 0x55, 0x33, 0x67, 0x3e, 0xe5, 0x42, 0x4a, + 0xa9, 0x60, 0xbe, 0x53, 0x79, 0x06, 0x27, 0x9f, 0x42, 0x30, 0xe4, 0x87, 0xef, 0x33, 0xbf, 0x5e, 0x73, 0x6b, 0x96, + 0xf1, 0xa2, 0xbe, 0xa7, 0x7d, 0x36, 0x43, 0x6d, 0x78, 0xb5, 0x94, 0x10, 0x57, 0x67, 0xd9, 0xb9, 0x78, 0x0d, 0xac, + 0xa9, 0x0c, 0xf0, 0x15, 0xab, 0xa2, 0x2e, 0xc1, 0x57, 0xc4, 0xbc, 0x91, 0x30, 0x7f, 0xc3, 0x2a, 0x06, 0xf3, 0xa6, + 0x4a, 0xca, 0x27, 0xee, 0x8f, 0xd8, 0x94, 0x71, 0x89, 0xb2, 0xa5, 0x0f, 0xe9, 0x77, 0xb0, 0x37, 0xaa, 0x78, 0xb3, + 0x12, 0xbe, 0x96, 0xec, 0xb7, 0x7d, 0x6c, 0x4d, 0xc2, 0x14, 0x00, 0x2d, 0x32, 0x16, 0x01, 0xdd, 0x7a, 0xf5, 0xb6, + 0x90, 0xad, 0x09, 0x8d, 0x34, 0x34, 0x84, 0xa2, 0xee, 0xbd, 0x60, 0x62, 0x52, 0xdc, 0x1d, 0x28, 0x31, 0x31, 0x9e, + 0x35, 0x96, 0x5f, 0x90, 0x9f, 0x57, 0x75, 0xda, 0x1a, 0x73, 0xa1, 0x63, 0x46, 0x30, 0xa9, 0x41, 0x33, 0x01, 0x92, + 0x00, 0x5e, 0x2e, 0xa3, 0xc1, 0x38, 0x4f, 0x38, 0x36, 0xf7, 0x3a, 0x4b, 0xc8, 0x00, 0x81, 0x4e, 0x31, 0xa5, 0x52, + 0xbc, 0x5a, 0x1f, 0xa4, 0x94, 0x17, 0x80, 0xb2, 0x63, 0x36, 0x58, 0x52, 0x50, 0x1f, 0x6d, 0xda, 0x4c, 0xae, 0x6d, + 0x0d, 0x7b, 0xca, 0x64, 0xd6, 0x42, 0x99, 0xe6, 0x0f, 0x97, 0xf9, 0x45, 0xc4, 0xb8, 0xa8, 0xf9, 0x84, 0x7d, 0xd5, + 0x61, 0x04, 0x5a, 0x8f, 0x41, 0x5e, 0x0f, 0x27, 0xbc, 0x9f, 0xd7, 0xfb, 0xe6, 0xd6, 0xc4, 0x93, 0x17, 0x05, 0x4e, + 0x7d, 0xa9, 0xfc, 0x4b, 0xfb, 0x13, 0xd8, 0xc4, 0x03, 0x99, 0xf8, 0x54, 0xb2, 0x95, 0x89, 0xa2, 0x04, 0xa2, 0x5a, + 0x84, 0x67, 0x92, 0x0b, 0x82, 0x94, 0x8c, 0x97, 0x81, 0x50, 0xdb, 0x8c, 0x06, 0x24, 0xef, 0x6b, 0x4b, 0x78, 0x2d, + 0xf9, 0x74, 0x11, 0xf2, 0x66, 0x33, 0xac, 0xed, 0xf9, 0xb4, 0xdb, 0xde, 0x4a, 0xa1, 0x6a, 0x80, 0x92, 0xc9, 0x70, + 0x19, 0xf4, 0x0d, 0xcd, 0x0e, 0xe5, 0x09, 0xed, 0xf6, 0x6d, 0x56, 0xca, 0x24, 0xcc, 0x4e, 0xd7, 0xe4, 0xa8, 0xf8, + 0x85, 0xd2, 0xee, 0x6c, 0x74, 0x05, 0xaf, 0x75, 0x07, 0xe3, 0xa2, 0x50, 0x0e, 0x30, 0xa6, 0x46, 0xe6, 0x0f, 0xdc, + 0xc8, 0x91, 0xa5, 0x0f, 0xcb, 0xe4, 0xa2, 0x56, 0x54, 0x26, 0x43, 0xda, 0xb4, 0xb6, 0xea, 0x36, 0x1b, 0x25, 0xe9, + 0xb2, 0x44, 0xce, 0xb7, 0x56, 0xf1, 0xb2, 0xea, 0xe1, 0x5d, 0x28, 0xa5, 0xef, 0x4b, 0x5c, 0xbc, 0x74, 0xa0, 0xee, + 0x6d, 0x25, 0x96, 0xf0, 0xa9, 0x69, 0xe2, 0x14, 0xdc, 0x01, 0x63, 0x95, 0xad, 0x88, 0x5a, 0x20, 0xa9, 0xff, 0xc2, + 0x8b, 0xfb, 0x42, 0x84, 0x78, 0xe7, 0xaa, 0x57, 0x33, 0x24, 0x66, 0x92, 0xc7, 0x68, 0xf5, 0x3b, 0x88, 0x82, 0x6e, + 0x39, 0x8d, 0x03, 0x02, 0x4f, 0x4d, 0x7a, 0xf9, 0xed, 0x48, 0xe2, 0xec, 0x36, 0x2b, 0x34, 0xd0, 0xe3, 0x59, 0x76, + 0xb0, 0xc6, 0xb6, 0x6a, 0x8f, 0x67, 0xa6, 0x2f, 0x2e, 0xb4, 0x4c, 0xc2, 0x98, 0xdf, 0x36, 0xf4, 0x03, 0xd8, 0xa5, + 0xe9, 0xc6, 0x41, 0x63, 0x76, 0x57, 0xab, 0x2f, 0xf1, 0xbc, 0xa8, 0x82, 0x24, 0x2e, 0xb1, 0x31, 0x0a, 0xeb, 0xb7, + 0x2a, 0x1f, 0x15, 0x05, 0xcb, 0xb9, 0xe5, 0xaa, 0xca, 0x6b, 0xd7, 0x91, 0x17, 0xaf, 0x45, 0x4e, 0x82, 0xca, 0x3d, + 0x32, 0xe3, 0x18, 0x5c, 0x44, 0x0b, 0xfd, 0x9c, 0x5e, 0x54, 0x15, 0x1d, 0xaf, 0x2c, 0x6b, 0x88, 0x20, 0x70, 0xab, + 0xea, 0x15, 0x52, 0x62, 0x91, 0x98, 0x67, 0x11, 0xb2, 0xbd, 0x0e, 0x72, 0x9b, 0xb3, 0x81, 0x70, 0x93, 0x4e, 0x09, + 0x9c, 0x92, 0xf0, 0x0f, 0xe5, 0xd9, 0x86, 0x11, 0xf5, 0x4c, 0x6b, 0xa4, 0x8b, 0xaa, 0x35, 0xe7, 0xb5, 0x28, 0xd4, + 0x0e, 0x94, 0xb8, 0x5a, 0xaf, 0x6e, 0x84, 0x42, 0x80, 0x70, 0x61, 0xfe, 0x1c, 0xc0, 0xfd, 0x6d, 0xcd, 0x8a, 0x07, + 0x9b, 0xca, 0xa1, 0x5a, 0x35, 0x6d, 0x1c, 0x80, 0x03, 0xf2, 0x16, 0x2b, 0x83, 0x0b, 0x24, 0xc3, 0x0c, 0xf5, 0x32, + 0xd1, 0x06, 0x43, 0xc5, 0x38, 0xb5, 0xf8, 0x5c, 0xea, 0x5c, 0xa7, 0x4f, 0xc3, 0x8a, 0x99, 0xc5, 0x1d, 0xfa, 0x6c, + 0x95, 0x39, 0xf8, 0xda, 0x11, 0xec, 0xf2, 0x93, 0x69, 0xdb, 0x07, 0x25, 0xbf, 0x0d, 0x65, 0x1a, 0xde, 0xc4, 0xb9, + 0x4d, 0xd9, 0xe9, 0x63, 0x65, 0xe1, 0xab, 0xf7, 0x9d, 0x5b, 0xf2, 0xc1, 0xcc, 0x16, 0x91, 0x7e, 0x05, 0x18, 0xf2, + 0xc7, 0xf8, 0x79, 0x32, 0x88, 0xb6, 0x9d, 0xae, 0x73, 0xcd, 0x3b, 0x54, 0x49, 0x45, 0x45, 0xae, 0x84, 0x21, 0x72, + 0x28, 0xe4, 0x32, 0x52, 0xfa, 0x5a, 0x22, 0x6b, 0x33, 0x72, 0x27, 0xd3, 0x8f, 0x96, 0xd3, 0x29, 0x0e, 0x79, 0x69, + 0xad, 0x0b, 0xeb, 0xf2, 0x37, 0xba, 0xb2, 0x4d, 0xfa, 0x4b, 0x3d, 0x91, 0x8b, 0x86, 0xf0, 0xf3, 0xb5, 0xcd, 0x01, + 0x4a, 0xfd, 0xaf, 0xd6, 0x2f, 0xe2, 0xa8, 0xa0, 0x0b, 0x5d, 0x19, 0x88, 0x0f, 0x8a, 0x52, 0x82, 0xed, 0x73, 0x96, + 0x50, 0xd7, 0x3d, 0x30, 0x4e, 0xba, 0xe2, 0xa4, 0xe8, 0x17, 0xef, 0x45, 0x78, 0x6f, 0x9f, 0x1c, 0x56, 0xee, 0x10, + 0xa7, 0xa7, 0x5a, 0xf5, 0x31, 0x32, 0x59, 0x49, 0x4c, 0x34, 0x61, 0x95, 0x37, 0x34, 0x87, 0xad, 0x32, 0x9a, 0xd5, + 0x74, 0x9d, 0x7c, 0x7f, 0xa0, 0x30, 0x12, 0x19, 0xfe, 0x6e, 0x6e, 0x22, 0x03, 0x0d, 0x1c, 0xd5, 0x19, 0xa8, 0xe4, + 0xb8, 0x9f, 0x6b, 0xd6, 0x87, 0xca, 0x4b, 0x00, 0x64, 0xf6, 0x78, 0xa3, 0xac, 0x5b, 0x7e, 0x37, 0xaf, 0x41, 0x40, + 0xaf, 0xff, 0x15, 0x6d, 0xb2, 0x80, 0x68, 0x33, 0xb8, 0x56, 0x53, 0x50, 0x3e, 0x65, 0xa2, 0x3f, 0xda, 0xa0, 0x67, + 0xbf, 0xdb, 0xe6, 0x0c, 0xd5, 0x85, 0xa5, 0xc4, 0xee, 0x5b, 0x94, 0x15, 0x0b, 0xd8, 0xcf, 0x6a, 0x84, 0xee, 0x94, + 0xf1, 0xf3, 0x47, 0xdd, 0xcc, 0x66, 0x61, 0xab, 0x08, 0xe8, 0xd1, 0x57, 0x57, 0x1c, 0x00, 0x0b, 0xe8, 0x12, 0x16, + 0x46, 0xec, 0x58, 0xca, 0x33, 0xcb, 0x54, 0xf6, 0x99, 0x47, 0x74, 0x7d, 0x33, 0xe4, 0x1e, 0x3e, 0xdd, 0x7e, 0x8b, + 0x55, 0x31, 0x8e, 0x27, 0xd6, 0xd5, 0x45, 0x67, 0x50, 0x34, 0x21, 0xe9, 0xf4, 0xcb, 0x19, 0x90, 0xaa, 0x95, 0x9d, + 0x98, 0xab, 0x36, 0x01, 0xf4, 0xf6, 0x5d, 0x49, 0xe0, 0x31, 0x39, 0xbc, 0x1b, 0xcc, 0x2c, 0x30, 0x45, 0xcb, 0x52, + 0x08, 0x7d, 0xb7, 0x14, 0xe5, 0xbc, 0x15, 0x0a, 0x06, 0xb4, 0x0b, 0xc2, 0xdf, 0x38, 0x2e, 0xb1, 0x05, 0x2d, 0xa3, + 0xf5, 0x22, 0x88, 0x8e, 0x40, 0x24, 0x37, 0x46, 0x8e, 0x0f, 0x67, 0xeb, 0x1a, 0x14, 0x43, 0x96, 0xba, 0xc0, 0xa1, + 0x9b, 0x17, 0x6c, 0x97, 0x0a, 0xc9, 0x44, 0xbe, 0x43, 0x43, 0x60, 0x79, 0xee, 0xc4, 0xe9, 0x80, 0xe8, 0xde, 0xdf, + 0x27, 0x4b, 0x56, 0x54, 0xfc, 0x50, 0x86, 0xdb, 0x17, 0x66, 0x70, 0xa8, 0x27, 0xde, 0x0c, 0x3a, 0xe0, 0x4a, 0xef, + 0x53, 0x25, 0x46, 0x32, 0xeb, 0x1d, 0x20, 0x8a, 0x88, 0x32, 0xf3, 0x4c, 0x76, 0x8b, 0xdb, 0xc3, 0x29, 0x60, 0x20, + 0x63, 0xda, 0xa4, 0x27, 0xc3, 0x44, 0x60, 0x88, 0xf9, 0x6a, 0x7c, 0xde, 0x83, 0x1f, 0xdb, 0x7d, 0x44, 0xce, 0x45, + 0xb9, 0x86, 0xc2, 0x36, 0x66, 0x33, 0x5b, 0xf4, 0x04, 0xdf, 0x48, 0xa4, 0xa3, 0x97, 0x31, 0x94, 0x0b, 0x84, 0x83, + 0x95, 0xce, 0x89, 0xe9, 0xc1, 0x8a, 0x2a, 0x40, 0x5c, 0xb9, 0x71, 0xca, 0xa8, 0x01, 0xb3, 0xe4, 0x06, 0x57, 0xd0, + 0x64, 0xd4, 0xe1, 0x57, 0x77, 0xf4, 0xec, 0x63, 0x16, 0xdc, 0x93, 0x97, 0xc1, 0xa1, 0x6e, 0xad, 0xa7, 0x75, 0xf7, + 0x06, 0x12, 0x62, 0x41, 0x59, 0x60, 0xce, 0x4e, 0x87, 0x85, 0x15, 0x6c, 0x6b, 0x6a, 0x85, 0x57, 0xeb, 0x87, 0x16, + 0x56, 0x92, 0xe1, 0x34, 0x88, 0x24, 0xce, 0xc0, 0x34, 0x0a, 0xf1, 0x87, 0xfa, 0x8b, 0x45, 0x5f, 0x9e, 0xf8, 0xad, + 0xfb, 0x6b, 0xa9, 0xb4, 0xfa, 0xfc, 0xb3, 0x58, 0xb8, 0x20, 0x13, 0xfb, 0x8d, 0x5e, 0x58, 0x98, 0x14, 0x56, 0xe0, + 0xaa, 0x7a, 0xc1, 0xb3, 0x64, 0xa5, 0xf0, 0xe4, 0xbb, 0x11, 0x5a, 0x7a, 0xc2, 0xcf, 0xa3, 0xac, 0x1a, 0x7b, 0x33, + 0xa2, 0x51, 0x2d, 0x9f, 0x82, 0xda, 0x1d, 0x1d, 0x08, 0x97, 0xc9, 0xc0, 0xaa, 0xb2, 0x00, 0xf5, 0xe7, 0x97, 0xb9, + 0x47, 0xc2, 0xba, 0x54, 0x4c, 0xd9, 0x07, 0xcf, 0x89, 0xa0, 0xb7, 0x10, 0x85, 0x18, 0x1e, 0x49, 0xdf, 0xa0, 0xfc, + 0xea, 0x8f, 0xfc, 0xbe, 0xd7, 0x93, 0xbf, 0x33, 0x76, 0xbe, 0x69, 0x96, 0x3b, 0xb3, 0xd7, 0xe8, 0xf5, 0xcf, 0x21, + 0x6b, 0x11, 0x06, 0x39, 0x4d, 0x17, 0x82, 0x26, 0x28, 0x5e, 0x18, 0x0d, 0xac, 0xe7, 0x74, 0xad, 0x37, 0x41, 0xee, + 0x85, 0xc4, 0xf8, 0x7f, 0x91, 0xf0, 0x32, 0xa0, 0x72, 0x32, 0x8a, 0x5a, 0xf0, 0x00, 0x5c, 0x55, 0x43, 0x2d, 0x50, + 0x26, 0x0f, 0x4f, 0xa0, 0x25, 0x63, 0x11, 0x9e, 0x65, 0x1f, 0xeb, 0xd4, 0xc1, 0x78, 0x24, 0xf3, 0xb0, 0xa6, 0xc2, + 0xd5, 0x72, 0x36, 0x39, 0x66, 0x76, 0xcc, 0xea, 0x6a, 0x1f, 0xbb, 0x13, 0x26, 0xf1, 0xcc, 0x79, 0xc8, 0x67, 0xdb, + 0xe3, 0x40, 0x53, 0x6f, 0x1e, 0x38, 0xac, 0x69, 0x36, 0x11, 0xe4, 0x9a, 0x06, 0xb6, 0x00, 0x83, 0x9d, 0xac, 0x55, + 0xa3, 0x84, 0x64, 0xcd, 0x0d, 0x80, 0x38, 0x92, 0x51, 0x08, 0xa9, 0x6c, 0xf8, 0x81, 0xb5, 0x54, 0x5f, 0x81, 0x1e, + 0xab, 0x2f, 0x35, 0x0c, 0x84, 0xa8, 0x6d, 0x84, 0x2a, 0x60, 0x0c, 0x5c, 0x99, 0x7f, 0x29, 0x10, 0x5c, 0xd0, 0x5f, + 0xf6, 0x1a, 0xbe, 0xdc, 0xac, 0xdb, 0x8e, 0x21, 0xea, 0x3a, 0x58, 0x8b, 0xc8, 0x78, 0xd5, 0x15, 0xfe, 0x1b, 0x6e, + 0x22, 0x45, 0x0a, 0xc5, 0x12, 0x91, 0xfc, 0x88, 0xf2, 0x1e, 0xe3, 0x1e, 0xea, 0xbd, 0x1d, 0xbc, 0x8e, 0x84, 0x41, + 0x73, 0xa8, 0xd1, 0x4a, 0x52, 0xbc, 0xc7, 0x56, 0x3d, 0xf6, 0x28, 0xb8, 0x9f, 0x2c, 0x35, 0x7c, 0x87, 0x28, 0x5d, + 0xfd, 0x14, 0x50, 0x4f, 0xfe, 0xa3, 0x67, 0x9b, 0xa7, 0x66, 0x1f, 0x11, 0x7d, 0x93, 0xd1, 0x38, 0xb2, 0x50, 0x51, + 0x14, 0x5e, 0x08, 0x81, 0xe7, 0x1c, 0xf1, 0x54, 0x1f, 0x20, 0xe6, 0x21, 0xd3, 0x64, 0xe4, 0x7a, 0x40, 0x0f, 0x34, + 0x39, 0x7a, 0x76, 0x39, 0xa6, 0x8b, 0xf6, 0x61, 0x74, 0x6c, 0x47, 0x88, 0x4b, 0xb5, 0x89, 0x68, 0x4e, 0xab, 0x2e, + 0x5b, 0x48, 0x62, 0x9d, 0xa7, 0x7c, 0xa4, 0x20, 0x07, 0x6e, 0xc2, 0xea, 0x77, 0x8e, 0x43, 0xbb, 0x28, 0xb8, 0x7d, + 0x4d, 0x25, 0x9c, 0x8d, 0x2a, 0xba, 0x2f, 0x83, 0x4f, 0xa2, 0x59, 0x34, 0x80, 0x6c, 0xc0, 0xd7, 0xfb, 0xdb, 0x09, + 0x96, 0x25, 0xd8, 0x45, 0x6d, 0xa6, 0x6c, 0x5e, 0x9e, 0xc3, 0x6c, 0x6b, 0xb8, 0x2f, 0xd0, 0xfa, 0x12, 0xea, 0x5d, + 0xea, 0x33, 0xc2, 0xb7, 0xf2, 0x60, 0x88, 0xc9, 0xca, 0xcd, 0x46, 0x16, 0x83, 0x75, 0x98, 0x75, 0x8f, 0x91, 0x39, + 0x89, 0x7f, 0xa1, 0xce, 0x5c, 0x10, 0x9e, 0x59, 0xc9, 0x82, 0x4f, 0xe8, 0x66, 0xb0, 0x61, 0x3c, 0xc6, 0xcf, 0x51, + 0xf6, 0xe0, 0xfd, 0x4e, 0x92, 0x56, 0x30, 0x1b, 0x92, 0xda, 0x71, 0xb5, 0xd6, 0xf1, 0x8b, 0x0b, 0xf4, 0x20, 0x35, + 0xf1, 0x54, 0x54, 0x76, 0xc4, 0x2c, 0x90, 0xea, 0x25, 0xf6, 0xbe, 0xf9, 0x49, 0x7c, 0xa4, 0x0d, 0x9e, 0xcb, 0x10, + 0x06, 0xf4, 0x46, 0x62, 0x7d, 0xaf, 0x94, 0xa6, 0x47, 0x65, 0x63, 0xd0, 0xda, 0x98, 0xc9, 0x1c, 0x26, 0xd6, 0x5d, + 0xa2, 0x5e, 0x2c, 0x4f, 0xf2, 0x6b, 0x5b, 0xd3, 0x8a, 0xe3, 0x91, 0xf4, 0x55, 0x95, 0x62, 0xfe, 0x18, 0xd0, 0xf8, + 0xd7, 0x14, 0xc9, 0x23, 0x03, 0x0d, 0x06, 0xa9, 0xb1, 0x62, 0x19, 0x80, 0x43, 0x0c, 0x4d, 0x44, 0x6d, 0xa0, 0x1d, + 0xc3, 0x1d, 0x8d, 0x0c, 0xa9, 0x8f, 0x68, 0x86, 0x24, 0xc0, 0x23, 0x9b, 0x98, 0xac, 0x8c, 0x5d, 0x80, 0x2b, 0x70, + 0xfb, 0x78, 0x06, 0x8d, 0xdf, 0x6e, 0xdd, 0x20, 0xa5, 0xa6, 0x9c, 0x2e, 0x02, 0xd6, 0x98, 0x00, 0x9e, 0x52, 0x4d, + 0xb4, 0x6c, 0x48, 0xf5, 0x53, 0x27, 0x60, 0xbf, 0x38, 0xa8, 0x8f, 0xad, 0x69, 0x4a, 0x59, 0x36, 0x0d, 0xbc, 0x94, + 0x34, 0x42, 0x8c, 0xd0, 0x57, 0x38, 0xe5, 0x08, 0xc4, 0x3b, 0xfc, 0xfa, 0xf4, 0x7a, 0x92, 0xde, 0x26, 0xda, 0xd8, + 0x64, 0x80, 0x61, 0xf8, 0x18, 0xe1, 0x17, 0x3d, 0xec, 0x6c, 0xcd, 0xf8, 0x6b, 0x82, 0x64, 0x3c, 0x29, 0x7c, 0x56, + 0x78, 0x36, 0xb5, 0x45, 0x93, 0x10, 0xff, 0x40, 0x74, 0x28, 0x30, 0x3a, 0x15, 0x94, 0xd9, 0x97, 0x8b, 0xea, 0x45, + 0x4e, 0x41, 0xa3, 0x7d, 0x66, 0xb9, 0xb2, 0x2c, 0x5f, 0x5f, 0xfe, 0xe3, 0x5c, 0x77, 0x5c, 0x62, 0xcf, 0x9d, 0x94, + 0xb8, 0x68, 0x65, 0xcd, 0x1f, 0x5a, 0x5b, 0x6f, 0xc9, 0x61, 0x23, 0x17, 0x9d, 0x42, 0x09, 0xff, 0xc4, 0x5f, 0x0a, + 0x82, 0x95, 0x7b, 0xb0, 0x64, 0x2a, 0xe5, 0x82, 0x8b, 0x19, 0xdd, 0x76, 0xfa, 0x5e, 0xb0, 0xd0, 0xd9, 0xd9, 0xc5, + 0x71, 0x82, 0x24, 0xe5, 0x87, 0xfc, 0x33, 0xef, 0xe2, 0x6c, 0x3b, 0xab, 0xe9, 0x68, 0x45, 0xef, 0xd8, 0xbb, 0x1c, + 0x4e, 0x6c, 0x11, 0xa5, 0xd3, 0x07, 0xe7, 0x67, 0x33, 0xf8, 0xe0, 0x28, 0x6a, 0xe9, 0x4c, 0xcd, 0x58, 0xc0, 0xb9, + 0xb9, 0x7b, 0x88, 0xa0, 0xa7, 0x90, 0x88, 0xd1, 0xf7, 0x2e, 0xa8, 0xf7, 0x8a, 0x6d, 0xce, 0x37, 0x89, 0xa0, 0xcd, + 0x0a, 0x9a, 0x45, 0xf4, 0x62, 0x78, 0x2a, 0xbc, 0x76, 0xe7, 0x5a, 0xae, 0x78, 0x5e, 0x42, 0xa3, 0x21, 0x6b, 0x90, + 0x6c, 0xbf, 0xd3, 0xc4, 0x0f, 0xfa, 0xb9, 0xd5, 0x42, 0x6d, 0x65, 0x4a, 0xfd, 0x98, 0x31, 0x4b, 0x9d, 0xb3, 0x92, + 0xfe, 0x9c, 0xfa, 0x0c, 0x6a, 0x9e, 0x6c, 0x75, 0xfa, 0x35, 0x9f, 0x5f, 0x0e, 0xd5, 0xb3, 0x99, 0xf2, 0x0e, 0x61, + 0x09, 0xf3, 0x7d, 0xa2, 0x54, 0x8f, 0xac, 0xbb, 0x25, 0xce, 0x52, 0x54, 0xc7, 0x22, 0x89, 0x22, 0x63, 0x3b, 0xc3, + 0x11, 0x7a, 0x21, 0xf1, 0x6c, 0x56, 0x67, 0xc2, 0xe4, 0x6a, 0x16, 0x6f, 0x07, 0x73, 0x25, 0x9c, 0xc4, 0x22, 0x89, + 0x50, 0xa4, 0x7d, 0x23, 0x5d, 0x4c, 0xf9, 0xa9, 0xce, 0xed, 0x48, 0xa8, 0xf4, 0x16, 0xff, 0x34, 0xb8, 0xc4, 0x44, + 0x2a, 0x50, 0x89, 0xcf, 0xef, 0x96, 0x58, 0x22, 0x49, 0x15, 0x39, 0x14, 0xd4, 0xca, 0xe4, 0x0f, 0x9b, 0xe7, 0x52, + 0x5a, 0x77, 0x47, 0xe0, 0xfa, 0x32, 0x56, 0x12, 0x77, 0xff, 0x32, 0x99, 0x47, 0x01, 0xd8, 0x2f, 0xcb, 0x75, 0x3e, + 0xc4, 0x80, 0xcb, 0xa3, 0x53, 0x8d, 0x20, 0xd8, 0xf1, 0x06, 0xde, 0x0c, 0x24, 0x08, 0x4e, 0x33, 0x12, 0x11, 0x0b, + 0xce, 0x90, 0xc5, 0x93, 0x37, 0x00, 0x24, 0xe7, 0x0f, 0xf1, 0xf3, 0x82, 0x94, 0x1d, 0xa0, 0x0a, 0x47, 0x05, 0x20, + 0x76, 0x48, 0xd0, 0xe8, 0xc2, 0xbb, 0xd9, 0x67, 0xad, 0xd9, 0xf2, 0x7a, 0x55, 0x3c, 0x07, 0x55, 0x43, 0x72, 0x52, + 0x12, 0x46, 0x9c, 0x61, 0xf6, 0x83, 0xa0, 0x44, 0xf9, 0xf6, 0x30, 0x21, 0x8c, 0xcc, 0x96, 0x78, 0xa1, 0xd1, 0x20, + 0xc0, 0xed, 0x23, 0xc4, 0x4c, 0xb6, 0x4d, 0x39, 0x26, 0x5f, 0x73, 0xc6, 0x39, 0x63, 0xce, 0x10, 0x8a, 0x06, 0x66, + 0x6b, 0x09, 0xc4, 0x3a, 0x8b, 0x32, 0x1a, 0x4a, 0x53, 0xfc, 0x4e, 0x8e, 0xa0, 0xd6, 0x91, 0xb7, 0x26, 0x43, 0xbb, + 0x0d, 0xee, 0x44, 0x80, 0x43, 0x0a, 0xf7, 0x4b, 0x60, 0x41, 0x79, 0xe5, 0xb6, 0x64, 0x96, 0xda, 0x7e, 0x48, 0xb6, + 0x92, 0xde, 0x9b, 0x81, 0xc1, 0xbb, 0x58, 0xc3, 0xc5, 0x2c, 0x1d, 0x25, 0x64, 0x15, 0x6c, 0x16, 0xeb, 0xfe, 0xe5, + 0xd7, 0x5d, 0x37, 0x19, 0xb9, 0xad, 0x92, 0xb1, 0xa2, 0x1c, 0x8f, 0xab, 0x39, 0x1b, 0x70, 0x7d, 0x19, 0xa4, 0xe1, + 0x52, 0x21, 0x74, 0xa6, 0x7d, 0xb7, 0xbf, 0x8b, 0x6b, 0xb7, 0x5c, 0x1e, 0x2d, 0xc0, 0xa0, 0x8d, 0x3d, 0x70, 0x8a, + 0x0a, 0x2c, 0x89, 0x0a, 0x49, 0xd8, 0x7c, 0x00, 0x4c, 0xb5, 0x7e, 0x10, 0xe5, 0xf8, 0x77, 0x49, 0x5f, 0x0b, 0x32, + 0x3d, 0xd7, 0x79, 0x7e, 0x96, 0xfa, 0x83, 0x69, 0xf7, 0x71, 0x8c, 0xe1, 0x8c, 0xc3, 0x1c, 0x21, 0x2a, 0x73, 0xf4, + 0xeb, 0xcf, 0xf0, 0xd8, 0xdb, 0x4a, 0xf5, 0x9f, 0x50, 0x9c, 0xdf, 0x2b, 0xa3, 0x79, 0xb6, 0x4c, 0xfa, 0x6c, 0x41, + 0xbf, 0xcf, 0x24, 0x2d, 0xdd, 0x76, 0xf9, 0xc4, 0xff, 0xa6, 0x3a, 0x3c, 0xdd, 0xed, 0x11, 0xe3, 0x22, 0x92, 0x04, + 0x9f, 0x98, 0x13, 0x9e, 0xee, 0x9a, 0x89, 0xba, 0x3c, 0x43, 0x6a, 0xf7, 0xc6, 0x68, 0x9b, 0x4a, 0xf5, 0xb6, 0xac, + 0xd8, 0xf4, 0xa2, 0x22, 0xd8, 0xd5, 0x85, 0x75, 0x79, 0xf7, 0xbb, 0x4f, 0xa9, 0x77, 0x73, 0x10, 0x6e, 0x5c, 0x6d, + 0x57, 0x35, 0x5a, 0xcc, 0x69, 0x01, 0xa5, 0x24, 0x52, 0x12, 0xcd, 0xa6, 0x71, 0xa4, 0x54, 0xf8, 0x79, 0x8e, 0x92, + 0x5b, 0x49, 0x9b, 0x5f, 0x5b, 0xc3, 0x89, 0x2a, 0xa9, 0x8e, 0xd4, 0xd4, 0x61, 0x4d, 0x7a, 0x0a, 0xcc, 0xff, 0xd9, + 0x31, 0x12, 0x82, 0xc2, 0x85, 0x33, 0x0f, 0x28, 0xf5, 0x57, 0x43, 0xb5, 0x93, 0x3e, 0x1e, 0x79, 0x7d, 0x6f, 0x1d, + 0xe7, 0x3a, 0x17, 0xce, 0x38, 0x74, 0xd3, 0xcd, 0x03, 0x3d, 0xfd, 0xae, 0xc7, 0x57, 0xf1, 0xd7, 0x86, 0x64, 0x49, + 0x22, 0x35, 0x73, 0x67, 0x7b, 0x65, 0x4b, 0xfb, 0xea, 0xa1, 0x42, 0x8b, 0xe3, 0xd2, 0x58, 0xed, 0x2b, 0xcc, 0xd3, + 0x1b, 0x35, 0x58, 0x44, 0x94, 0xa6, 0x7e, 0x38, 0x1e, 0xd2, 0x79, 0x0e, 0xd4, 0xd4, 0xe2, 0xe6, 0x29, 0xa7, 0xf5, + 0x13, 0xc6, 0xa9, 0x00, 0x3b, 0x13, 0x45, 0x2e, 0x5e, 0xab, 0xbf, 0x29, 0xfd, 0x0a, 0xf6, 0xd7, 0x2b, 0xa9, 0xfa, + 0x99, 0xc5, 0x2a, 0x9d, 0x19, 0x56, 0xe5, 0xcc, 0x9a, 0xe9, 0x0a, 0xfb, 0x39, 0x17, 0xbb, 0x1c, 0x58, 0x94, 0x24, + 0x79, 0x3a, 0xae, 0xcc, 0x22, 0x9c, 0xdb, 0x4b, 0xe7, 0x91, 0x4e, 0x9d, 0x6c, 0x30, 0x29, 0x13, 0x5a, 0x3d, 0x32, + 0x2d, 0x31, 0x32, 0x4d, 0x20, 0xd8, 0xa5, 0xb7, 0xc8, 0xd2, 0xf6, 0x8b, 0x3b, 0x16, 0x85, 0xda, 0x5c, 0x6d, 0x7a, + 0x1c, 0x85, 0x8c, 0xf9, 0xa5, 0xb5, 0xa7, 0xc4, 0xa5, 0xf3, 0x63, 0x11, 0xed, 0xa7, 0x4b, 0x75, 0xac, 0xd9, 0x89, + 0x40, 0x95, 0x6b, 0x03, 0xf9, 0x79, 0x9b, 0x1e, 0xd2, 0xe7, 0x2d, 0x9c, 0x95, 0x3f, 0x94, 0x61, 0x7d, 0x40, 0x08, + 0x13, 0x81, 0x91, 0xb1, 0x50, 0x5a, 0x49, 0x60, 0x15, 0x78, 0xc5, 0xa8, 0xd9, 0x6c, 0x57, 0x7c, 0x1f, 0x40, 0x3a, + 0xc7, 0x4d, 0x08, 0x07, 0x80, 0xbc, 0x9e, 0x42, 0x75, 0x16, 0xa2, 0x40, 0x33, 0x05, 0x48, 0xf8, 0x21, 0x3d, 0x7f, + 0x01, 0xf3, 0xc7, 0x74, 0xf4, 0x56, 0xad, 0xdc, 0x46, 0x3b, 0x1c, 0xcb, 0x53, 0xe5, 0xa6, 0x1a, 0x87, 0x8b, 0x92, + 0xa8, 0x24, 0x16, 0x35, 0xbc, 0x72, 0x45, 0x9b, 0x33, 0x1f, 0xf9, 0x0d, 0xdb, 0xc4, 0xe3, 0x5f, 0x57, 0x63, 0x5c, + 0x81, 0xaa, 0x51, 0x05, 0x5b, 0xf2, 0x05, 0x98, 0xea, 0x2e, 0x12, 0xd8, 0x62, 0xd3, 0xd8, 0x9c, 0x81, 0x0e, 0xed, + 0xa3, 0xec, 0x49, 0xa9, 0x4a, 0x16, 0xa8, 0xe4, 0x6a, 0x29, 0xac, 0xb6, 0xa6, 0x51, 0x9b, 0x90, 0xf7, 0xbf, 0xa1, + 0x79, 0xeb, 0x4b, 0x3e, 0x61, 0x7b, 0x88, 0xe8, 0x33, 0x7c, 0xee, 0xa3, 0x5a, 0x7c, 0x0f, 0x28, 0x9c, 0x2d, 0x05, + 0x23, 0x53, 0x1c, 0xda, 0xe3, 0x05, 0x4a, 0x93, 0x79, 0x78, 0xa8, 0xa3, 0x0a, 0x1b, 0xf2, 0x11, 0x0e, 0xd8, 0x7e, + 0x4c, 0x61, 0x89, 0x0a, 0x25, 0xfa, 0x2e, 0xda, 0xcd, 0xc1, 0x77, 0xa5, 0x03, 0xde, 0x96, 0x21, 0x2e, 0xa6, 0x9b, + 0x9d, 0x78, 0x8b, 0x96, 0xe5, 0xab, 0x38, 0xd8, 0x66, 0x84, 0xa1, 0x6c, 0x0a, 0x70, 0xe7, 0xbd, 0xaa, 0x50, 0xe4, + 0xf8, 0xd6, 0x0c, 0x8e, 0xea, 0x0d, 0xd2, 0x45, 0x13, 0xa0, 0x0e, 0x46, 0x3d, 0xf0, 0x13, 0x82, 0x1c, 0x50, 0x19, + 0xbd, 0xdb, 0xa2, 0x2d, 0xae, 0x05, 0xcf, 0x84, 0x80, 0x34, 0xad, 0x48, 0xb5, 0x1b, 0xa5, 0x51, 0x1f, 0x0d, 0xcd, + 0xbe, 0x89, 0x45, 0x02, 0x90, 0xcc, 0xe2, 0x55, 0x49, 0xa4, 0x02, 0xd8, 0x02, 0x3b, 0x36, 0x8b, 0x6e, 0xf8, 0x66, + 0x7d, 0x32, 0x60, 0x68, 0xe9, 0xb5, 0xef, 0xc9, 0xea, 0xa3, 0xf6, 0xb9, 0x86, 0x78, 0xc5, 0x71, 0x8e, 0x34, 0x99, + 0x2a, 0xea, 0x7c, 0xb2, 0x8e, 0xf2, 0x58, 0x9b, 0xcb, 0xe5, 0x8d, 0x0d, 0x65, 0xd0, 0x63, 0x83, 0x45, 0x4a, 0x5c, + 0x3b, 0x66, 0xbf, 0xbe, 0xb8, 0xc8, 0xa0, 0xe3, 0x9c, 0x3e, 0x90, 0x30, 0x4d, 0x27, 0x11, 0xea, 0x8e, 0x95, 0xaf, + 0xab, 0xd0, 0x2c, 0x08, 0xfb, 0xfe, 0x22, 0x19, 0x6b, 0xd8, 0x78, 0x37, 0x64, 0x73, 0x7d, 0xd5, 0xde, 0x0f, 0x50, + 0x07, 0xe2, 0x62, 0xc0, 0xc5, 0x5b, 0x50, 0xc6, 0xcc, 0xbf, 0xa3, 0x5e, 0x2b, 0xa5, 0x34, 0x6a, 0x79, 0x18, 0x6a, + 0x78, 0xab, 0xbd, 0xcc, 0x7f, 0x3c, 0xfb, 0x90, 0x0f, 0x05, 0x2a, 0x54, 0x21, 0x35, 0x4d, 0xa2, 0x6e, 0xd7, 0x41, + 0x6c, 0x6b, 0x27, 0x99, 0x5a, 0xb1, 0x88, 0x94, 0x47, 0x80, 0xbb, 0x70, 0x78, 0xb7, 0xfa, 0x85, 0x11, 0xdf, 0xec, + 0x73, 0x2d, 0xb4, 0x25, 0x9a, 0xb3, 0x23, 0xde, 0x45, 0x2b, 0x3b, 0x9c, 0x5a, 0x20, 0x1d, 0x3b, 0x15, 0xdb, 0x25, + 0x8a, 0xde, 0x63, 0x81, 0xad, 0x66, 0x6b, 0xeb, 0xb7, 0x56, 0xf4, 0x21, 0xac, 0x16, 0xb4, 0xb6, 0xe7, 0x32, 0x8d, + 0xcd, 0xc4, 0x09, 0x62, 0x01, 0x34, 0x7b, 0xfb, 0xaa, 0x24, 0xef, 0x33, 0x0b, 0x2e, 0x4b, 0xb1, 0x44, 0x8a, 0xb0, + 0x03, 0x3a, 0x89, 0x06, 0x4c, 0x54, 0x05, 0xc7, 0x46, 0xec, 0xf9, 0xa2, 0xde, 0x37, 0xae, 0x4a, 0x32, 0x28, 0x93, + 0xd6, 0x6d, 0xd5, 0x8b, 0xc9, 0xf7, 0x7e, 0x16, 0x48, 0x3e, 0x14, 0x0e, 0x60, 0xc7, 0x25, 0x5c, 0x7c, 0x16, 0x8c, + 0xdc, 0x2a, 0x65, 0x2d, 0xc0, 0x9c, 0xce, 0x99, 0xbf, 0x5a, 0x7a, 0x34, 0x2d, 0x29, 0x27, 0x0e, 0xd3, 0xf7, 0xe7, + 0x10, 0xc9, 0x15, 0x48, 0x3f, 0xef, 0x3d, 0xef, 0x15, 0x7d, 0xe3, 0x8f, 0x57, 0xfb, 0x94, 0x19, 0xcd, 0xa6, 0x2c, + 0xf5, 0x64, 0xc9, 0xd3, 0x2d, 0x15, 0x1c, 0xa3, 0x8b, 0x56, 0x37, 0x6c, 0xcd, 0x8a, 0x35, 0x23, 0xcb, 0xf0, 0x8f, + 0x60, 0x85, 0x6f, 0x60, 0x5d, 0x2c, 0x01, 0xcd, 0xdf, 0x18, 0x1f, 0x85, 0x3c, 0x2e, 0x3e, 0xd0, 0xf9, 0x19, 0x21, + 0xae, 0xc2, 0x54, 0x91, 0x70, 0xbe, 0x55, 0x6a, 0xa5, 0x04, 0x15, 0xd3, 0xf2, 0x99, 0x16, 0xdf, 0xa8, 0x6d, 0x95, + 0xd9, 0x5b, 0x7e, 0x99, 0xe4, 0xca, 0x74, 0x7e, 0x9e, 0x9c, 0x49, 0xf1, 0xf2, 0xc3, 0x12, 0x55, 0xe6, 0x9f, 0x46, + 0x68, 0xa3, 0xef, 0xe1, 0xc7, 0x0e, 0x3f, 0xc8, 0xbc, 0x40, 0x24, 0xd5, 0xb8, 0xc0, 0x38, 0x2a, 0x3f, 0x4d, 0xab, + 0x11, 0x33, 0x45, 0xf8, 0xc6, 0xa9, 0x03, 0xcb, 0xf7, 0xb9, 0x54, 0x73, 0x2e, 0x42, 0x05, 0x10, 0x7b, 0x1a, 0x3b, + 0xef, 0xc2, 0x9c, 0x31, 0x15, 0x09, 0x84, 0x71, 0x85, 0x76, 0x49, 0x30, 0x76, 0x4b, 0xa9, 0xb6, 0xd5, 0xbb, 0x05, + 0xf3, 0x9a, 0x8a, 0x08, 0x98, 0xc2, 0x3b, 0xd0, 0xbc, 0x99, 0x2d, 0x6d, 0xd0, 0x39, 0xb1, 0xa3, 0x02, 0xfb, 0x31, + 0xa6, 0xbc, 0xc3, 0xde, 0x6f, 0xa6, 0xcf, 0x19, 0xe7, 0xd0, 0x3d, 0x0f, 0xf5, 0xa6, 0x33, 0x5c, 0xf9, 0x86, 0x3e, + 0x9b, 0x11, 0x67, 0x0b, 0x24, 0x5f, 0x23, 0x5b, 0xb1, 0xae, 0x5a, 0x82, 0xba, 0x07, 0x92, 0xbd, 0x7d, 0x75, 0xdd, + 0x5b, 0x7d, 0x2e, 0x08, 0x1a, 0xdd, 0xad, 0x00, 0xbb, 0x83, 0x05, 0xef, 0x56, 0x67, 0xe2, 0x89, 0x03, 0x80, 0xec, + 0xd2, 0x7f, 0x12, 0x36, 0xd0, 0x9d, 0x76, 0x7f, 0xed, 0x84, 0xb2, 0xa0, 0x75, 0x36, 0xe5, 0x31, 0xb4, 0x65, 0x17, + 0x11, 0x43, 0x76, 0x1d, 0xf6, 0xac, 0x9b, 0xfb, 0x42, 0x58, 0x81, 0xc7, 0x3d, 0xb0, 0xbe, 0x08, 0x7c, 0x4a, 0x04, + 0x24, 0xe4, 0x5c, 0x88, 0xbf, 0x75, 0xa1, 0x66, 0x19, 0x77, 0x9b, 0x0e, 0xb1, 0x9b, 0x24, 0xf4, 0x07, 0x55, 0xe1, + 0xad, 0xa5, 0x95, 0xcf, 0x02, 0xca, 0x7c, 0x24, 0x23, 0x03, 0xe7, 0xdc, 0xd8, 0x9e, 0x76, 0x5e, 0x9a, 0x31, 0x2f, + 0x15, 0x5a, 0x66, 0xf2, 0x6e, 0xd5, 0xc0, 0xb3, 0xf6, 0xbf, 0x9b, 0xe3, 0xc4, 0x86, 0xe6, 0xb1, 0x1d, 0x73, 0xb4, + 0xbd, 0x18, 0xf7, 0x2d, 0xfb, 0xea, 0xe5, 0x32, 0x2e, 0x9b, 0x67, 0xbd, 0x5b, 0xbb, 0x55, 0xec, 0xa7, 0x88, 0x0a, + 0x9b, 0xc2, 0x64, 0xaa, 0x49, 0x0c, 0x83, 0xc0, 0x68, 0x01, 0xec, 0x4d, 0x34, 0xc3, 0x2e, 0xe6, 0xa0, 0xb9, 0x34, + 0xeb, 0x6e, 0xf6, 0x38, 0x7d, 0x9b, 0xf9, 0x4a, 0xd5, 0x5e, 0x55, 0xa3, 0x44, 0xce, 0xe9, 0xb0, 0x7f, 0x29, 0xed, + 0x3f, 0x8a, 0xbc, 0xa9, 0x61, 0x2c, 0x0e, 0x44, 0x63, 0x01, 0xc1, 0x65, 0x7a, 0xab, 0xcd, 0xb2, 0x08, 0xc9, 0xa9, + 0x15, 0xe5, 0x1f, 0x34, 0x80, 0x54, 0x5c, 0xad, 0x16, 0x37, 0xe3, 0x58, 0x70, 0x8c, 0x4a, 0x6d, 0x0c, 0x4f, 0xff, + 0x24, 0x1e, 0x52, 0xd1, 0x56, 0x97, 0x13, 0xcd, 0x4b, 0xb5, 0xe5, 0x10, 0x40, 0x20, 0x57, 0x1b, 0xd6, 0x38, 0xf4, + 0x57, 0x27, 0x73, 0x23, 0xd3, 0x61, 0x66, 0xaa, 0xc0, 0xf8, 0x5b, 0x45, 0x53, 0x30, 0x39, 0x17, 0x49, 0xcc, 0xdc, + 0xce, 0xc0, 0xb2, 0x06, 0xe8, 0x20, 0x7a, 0xc3, 0xb7, 0x93, 0x1f, 0xea, 0x4f, 0x2b, 0x8b, 0x22, 0x4e, 0x1d, 0x93, + 0xd3, 0xd7, 0x76, 0x50, 0x50, 0xab, 0xed, 0x5c, 0xc4, 0x6b, 0x9e, 0x13, 0x68, 0x5f, 0xf9, 0xd5, 0xec, 0xf4, 0xfa, + 0x85, 0xd3, 0xef, 0x90, 0x15, 0x48, 0x9d, 0xe2, 0x5f, 0xba, 0x32, 0xca, 0xd5, 0xce, 0x79, 0x36, 0xfd, 0xf2, 0x98, + 0x24, 0xdb, 0xc6, 0xbf, 0x46, 0x2e, 0x39, 0x20, 0xf9, 0x13, 0xe7, 0xc0, 0xc8, 0x16, 0xd3, 0x24, 0x61, 0xaa, 0xd7, + 0x24, 0xcd, 0x59, 0x58, 0xc7, 0x6e, 0x3a, 0xfe, 0x73, 0xec, 0xa2, 0x27, 0x91, 0x90, 0x5a, 0x6f, 0x69, 0xa4, 0x85, + 0x75, 0xef, 0x8c, 0x5c, 0xc8, 0xe6, 0xa1, 0x4c, 0x01, 0x19, 0xd3, 0xcd, 0xba, 0x4b, 0x25, 0x12, 0xb5, 0x60, 0x69, + 0x68, 0xb7, 0x93, 0xe1, 0x10, 0xb5, 0xf6, 0x91, 0xec, 0x54, 0xf4, 0x2e, 0x54, 0x85, 0xa1, 0x8e, 0xe4, 0x4b, 0x61, + 0x25, 0x16, 0x58, 0x7b, 0x29, 0xd7, 0x92, 0x05, 0x5d, 0x79, 0x79, 0x24, 0x14, 0xeb, 0x00, 0xb6, 0xd6, 0xa5, 0xd1, + 0x0d, 0xa0, 0x13, 0xc5, 0xc0, 0x75, 0xc8, 0x00, 0x94, 0x31, 0x85, 0xca, 0x2d, 0x2d, 0x2e, 0xb9, 0x16, 0xa5, 0x98, + 0x03, 0x52, 0xbf, 0xc6, 0xe0, 0x8c, 0xf9, 0xbd, 0x8f, 0x29, 0xc4, 0x91, 0x31, 0xbc, 0x6a, 0x49, 0xda, 0x32, 0xd7, + 0xd6, 0x8a, 0x69, 0x9d, 0x30, 0x75, 0x96, 0xfd, 0x34, 0xf8, 0xce, 0xbf, 0xa3, 0x8e, 0xb4, 0xbc, 0xc5, 0x91, 0x8a, + 0x70, 0x68, 0x7b, 0x62, 0x2e, 0x4c, 0x29, 0x3c, 0x66, 0xb7, 0x77, 0x84, 0x6e, 0x7a, 0x29, 0xe0, 0xb1, 0x70, 0x63, + 0x2a, 0x30, 0x8e, 0x1e, 0x3f, 0x14, 0x4e, 0x84, 0xe1, 0xd0, 0x54, 0x9d, 0xf0, 0x6e, 0x9a, 0x32, 0x0b, 0x72, 0x6a, + 0x24, 0x6c, 0x78, 0xb0, 0xee, 0x07, 0x50, 0x14, 0x09, 0x69, 0x16, 0x57, 0x8d, 0x26, 0x8a, 0xeb, 0x8a, 0x0b, 0xbb, + 0x2f, 0xc7, 0xf9, 0x45, 0x25, 0x0e, 0xdd, 0xb3, 0xaa, 0x63, 0x8b, 0xc4, 0x67, 0x53, 0x55, 0x46, 0x44, 0xd5, 0x7b, + 0x09, 0x81, 0xb9, 0xad, 0xa5, 0x1b, 0x7f, 0xec, 0x0a, 0x57, 0x06, 0x0f, 0x0c, 0x21, 0xd2, 0xf4, 0x6a, 0x5d, 0xa2, + 0xe4, 0xed, 0xea, 0x0f, 0xfb, 0x61, 0xfd, 0xc1, 0xd8, 0x64, 0x07, 0xb7, 0x0a, 0xa4, 0xcd, 0x39, 0xbf, 0x66, 0xa6, + 0xb5, 0x6c, 0xb5, 0x0f, 0x6a, 0x94, 0x07, 0x9b, 0xcb, 0x34, 0x14, 0xf3, 0x4f, 0xef, 0x0c, 0x1f, 0x9c, 0x70, 0x91, + 0xf8, 0x02, 0x12, 0x71, 0xd8, 0x9e, 0x3e, 0x3e, 0x52, 0xf9, 0x5b, 0x27, 0x54, 0xd8, 0x8d, 0x52, 0xb6, 0x83, 0xf2, + 0xbe, 0x3a, 0xdc, 0x13, 0x13, 0x35, 0xd8, 0x67, 0x97, 0xa5, 0xa3, 0x01, 0x92, 0x94, 0x26, 0xf6, 0x25, 0x8e, 0xf7, + 0xc5, 0x0c, 0xeb, 0x05, 0x22, 0x5e, 0x75, 0xb2, 0x14, 0x4a, 0xa6, 0xec, 0xf9, 0xec, 0x78, 0x1d, 0x64, 0xf2, 0x11, + 0x55, 0x1d, 0xd2, 0xdc, 0xd4, 0x72, 0x97, 0x13, 0x03, 0xdd, 0x6b, 0xd3, 0x9f, 0xdf, 0x37, 0x86, 0x6c, 0x2b, 0x91, + 0x6f, 0x7c, 0x7b, 0xd4, 0x3f, 0xbd, 0x7e, 0xa1, 0x21, 0xd9, 0x9b, 0x65, 0xec, 0x6e, 0x7f, 0xb8, 0x2c, 0xea, 0xa8, + 0xea, 0x07, 0x55, 0x30, 0x4b, 0xea, 0xa9, 0xe9, 0x2c, 0xa4, 0x04, 0x13, 0x0e, 0x04, 0x9c, 0xb5, 0x1e, 0x84, 0xaa, + 0xcb, 0xbf, 0xb6, 0x57, 0x57, 0xbb, 0xf1, 0x62, 0xe1, 0x69, 0x64, 0x23, 0x31, 0xd4, 0x61, 0xe9, 0x3b, 0xb3, 0x85, + 0xf0, 0x0c, 0xbf, 0xef, 0x6a, 0x24, 0x2e, 0x35, 0x00, 0x5f, 0x2f, 0xdf, 0x9d, 0xfb, 0xe1, 0xf0, 0x21, 0xb0, 0x17, + 0xcc, 0x8c, 0xf7, 0x59, 0x69, 0x8a, 0x25, 0x0d, 0x3f, 0x46, 0x36, 0xb3, 0xae, 0x7d, 0x12, 0x82, 0x08, 0xac, 0x21, + 0x42, 0x95, 0x87, 0x66, 0x0e, 0x65, 0xac, 0x1c, 0xab, 0x68, 0xed, 0xd9, 0x6f, 0x30, 0x25, 0xb2, 0xd9, 0x22, 0xa0, + 0x23, 0xfb, 0x7e, 0x79, 0x51, 0xcb, 0xf0, 0xba, 0x7f, 0x79, 0xf8, 0x22, 0x17, 0xb5, 0x59, 0x03, 0xf8, 0x3b, 0x92, + 0xd5, 0xb2, 0x37, 0x96, 0x5f, 0xe8, 0x14, 0x6c, 0xb5, 0x39, 0x30, 0x22, 0x92, 0x36, 0x8c, 0xb8, 0x20, 0x99, 0x33, + 0x31, 0x15, 0x42, 0x96, 0x1e, 0xf7, 0xf1, 0x32, 0x05, 0xc0, 0xe9, 0x72, 0x65, 0xc4, 0x05, 0x81, 0x90, 0x8e, 0xc3, + 0x98, 0x16, 0xd2, 0xb2, 0x9e, 0xed, 0x42, 0xb3, 0x51, 0xa3, 0xd0, 0x35, 0x87, 0x44, 0x8d, 0x99, 0x75, 0x8f, 0x43, + 0x5c, 0x6a, 0x3b, 0x21, 0x2b, 0xbf, 0xb9, 0x9a, 0x01, 0xd0, 0x98, 0x48, 0x2e, 0x97, 0xc3, 0x44, 0x96, 0x98, 0xcf, + 0x98, 0xb4, 0xe9, 0xeb, 0xc3, 0x37, 0x31, 0x3d, 0x43, 0xec, 0x1a, 0xeb, 0x0f, 0xd1, 0xf2, 0xdc, 0x8b, 0x10, 0xd4, + 0xba, 0x6c, 0xd9, 0xa3, 0x68, 0x2b, 0x64, 0xa2, 0x6d, 0x49, 0xd8, 0x02, 0x0d, 0xec, 0x33, 0x9e, 0x0d, 0x97, 0x83, + 0x28, 0x4b, 0x40, 0x6a, 0x29, 0x87, 0xfc, 0x1a, 0xed, 0x11, 0x62, 0x0c, 0x16, 0xac, 0x81, 0xe5, 0xbe, 0xe1, 0x30, + 0x0a, 0x12, 0xec, 0x81, 0xff, 0xbf, 0x20, 0x96, 0xab, 0x6f, 0x27, 0x7b, 0x5e, 0x57, 0x25, 0xda, 0x06, 0x03, 0xe0, + 0xa0, 0xe3, 0x11, 0x06, 0x8d, 0x6b, 0x1a, 0xa8, 0xae, 0x27, 0x97, 0x0b, 0x33, 0x36, 0x55, 0x90, 0x7a, 0x06, 0xdc, + 0x12, 0x6e, 0xfb, 0x59, 0xc6, 0x1c, 0x0c, 0x6c, 0x9c, 0xdd, 0x8d, 0xed, 0x1a, 0x43, 0xf0, 0xe8, 0x04, 0xed, 0x74, + 0xa7, 0x84, 0x3c, 0xaf, 0x1f, 0xad, 0xd5, 0xb0, 0xc3, 0xe7, 0xad, 0x69, 0xcf, 0x23, 0xcc, 0x88, 0xb8, 0x69, 0xba, + 0x60, 0x63, 0x29, 0xc1, 0x52, 0xa4, 0x88, 0x01, 0x6c, 0x47, 0xd9, 0x0d, 0x80, 0x16, 0xd8, 0x1f, 0xca, 0x6b, 0x8d, + 0x1e, 0x3d, 0x1b, 0x3e, 0xc7, 0xa8, 0xea, 0x32, 0x87, 0x91, 0x7a, 0xee, 0x50, 0x37, 0x1e, 0x78, 0x7e, 0xaa, 0xd6, + 0x28, 0x14, 0x8a, 0x25, 0x70, 0xf4, 0xf3, 0x7d, 0x1a, 0x89, 0x67, 0x99, 0x21, 0xec, 0xe4, 0x66, 0xf3, 0x04, 0xc4, + 0x3e, 0x34, 0x32, 0x21, 0x80, 0x10, 0x2c, 0x84, 0xd5, 0x1e, 0x50, 0xce, 0xdf, 0x13, 0xf6, 0x7d, 0x44, 0xc7, 0x4d, + 0x80, 0x07, 0x53, 0x50, 0x9c, 0xac, 0x7d, 0x2a, 0x22, 0x52, 0xf9, 0x49, 0x92, 0x6c, 0xc6, 0x49, 0x9d, 0x04, 0x66, + 0x47, 0x9c, 0x92, 0xa5, 0x58, 0x38, 0x2f, 0x9e, 0x70, 0x60, 0xd3, 0x35, 0x05, 0x4c, 0x27, 0xbe, 0xc8, 0x49, 0xd9, + 0x0c, 0x5a, 0x38, 0x1f, 0xe7, 0xb6, 0x8d, 0x05, 0x47, 0x65, 0x19, 0x3b, 0x7b, 0xab, 0xc6, 0x08, 0x1d, 0xf6, 0x4d, + 0x82, 0x7a, 0x3f, 0xa6, 0xb0, 0x76, 0xda, 0xe3, 0x23, 0x26, 0xc1, 0xa1, 0x42, 0xe8, 0x26, 0xa8, 0x59, 0xa5, 0x3f, + 0xea, 0x8e, 0x39, 0x35, 0x92, 0xa4, 0x3c, 0x2e, 0x37, 0x24, 0xa9, 0x93, 0x7d, 0xf6, 0x68, 0x4f, 0x1e, 0x28, 0x9c, + 0x26, 0x3c, 0xd1, 0x95, 0x02, 0x06, 0xc1, 0x8b, 0x04, 0xbb, 0xba, 0x2c, 0x14, 0xc9, 0x40, 0x16, 0x43, 0xbb, 0x01, + 0x67, 0x57, 0xe6, 0x94, 0x84, 0x7c, 0xe6, 0x0b, 0x9e, 0xd9, 0x6e, 0x86, 0xe8, 0x26, 0x5b, 0xd4, 0x90, 0x51, 0x30, + 0xb4, 0x5b, 0x28, 0x22, 0x74, 0xeb, 0xc2, 0xdf, 0xe1, 0x0f, 0xcf, 0x52, 0xd9, 0x5c, 0x70, 0x9d, 0x2e, 0xbc, 0xc6, + 0x5f, 0x7a, 0xd6, 0x8a, 0x9d, 0x6f, 0xad, 0x9d, 0x4b, 0x96, 0x8b, 0x5e, 0xf3, 0x1f, 0xb9, 0xc7, 0x05, 0x3a, 0xb1, + 0x05, 0xd1, 0x86, 0x26, 0xa8, 0x0c, 0xa7, 0x81, 0x0b, 0x0f, 0x14, 0x52, 0x7b, 0x1c, 0x96, 0xb2, 0x45, 0xf4, 0x93, + 0x79, 0xae, 0xae, 0xc1, 0x22, 0x31, 0x6b, 0xa5, 0xe8, 0x45, 0x53, 0xa1, 0x88, 0x8c, 0xae, 0x06, 0xa2, 0x54, 0x97, + 0x43, 0x9a, 0x02, 0x91, 0x53, 0x92, 0x78, 0x25, 0x73, 0x06, 0x45, 0x3e, 0xe8, 0x45, 0xff, 0x8b, 0x13, 0x51, 0x0f, + 0xf9, 0xfc, 0x27, 0x55, 0x3e, 0xcb, 0xa2, 0x7e, 0x14, 0x76, 0x7d, 0x19, 0x9b, 0x6c, 0x18, 0x03, 0x18, 0x34, 0xcc, + 0x21, 0xbb, 0x18, 0xd9, 0xaa, 0x76, 0xdd, 0x0c, 0x92, 0x73, 0x43, 0x7e, 0x36, 0x73, 0xc0, 0xfc, 0xfe, 0x5b, 0x28, + 0x1b, 0xbc, 0xc4, 0x8c, 0xc3, 0x7d, 0xe4, 0x27, 0x6f, 0x22, 0x0b, 0xfe, 0x70, 0x1a, 0x3a, 0x40, 0xd3, 0x21, 0xd4, + 0xe6, 0x8a, 0x09, 0x33, 0x03, 0x9b, 0xb2, 0x20, 0xa6, 0x45, 0x4f, 0x89, 0x1a, 0xff, 0xbd, 0x7f, 0xd6, 0x00, 0x34, + 0x7b, 0xe4, 0xcf, 0xd6, 0xe8, 0x40, 0xb7, 0xea, 0xd2, 0x47, 0xf7, 0x26, 0x99, 0x06, 0x00, 0x97, 0xdb, 0xeb, 0xb5, + 0xd8, 0x6e, 0xa7, 0x55, 0xc8, 0x3e, 0x98, 0xe1, 0xc6, 0xf1, 0x94, 0x9c, 0xb7, 0x29, 0x1b, 0x0b, 0x84, 0xa7, 0xcc, + 0x0a, 0x12, 0xbb, 0x6f, 0xdd, 0xb3, 0xb2, 0x7f, 0x8c, 0xff, 0xa5, 0xf1, 0xcb, 0x22, 0x3f, 0xdf, 0x6e, 0xa5, 0x12, + 0x78, 0xa5, 0x9f, 0xd1, 0x7b, 0x17, 0xc0, 0x72, 0x07, 0x91, 0x8c, 0x96, 0xf7, 0xd4, 0xa2, 0xea, 0xa9, 0x5f, 0x64, + 0xab, 0x71, 0xe3, 0xc4, 0x8e, 0xf2, 0xe6, 0xf3, 0x82, 0x8d, 0x40, 0xc5, 0xc3, 0x6b, 0x46, 0x98, 0xfe, 0x7d, 0x32, + 0x71, 0xea, 0x1d, 0x3b, 0x7b, 0x8f, 0x20, 0xeb, 0x89, 0xed, 0xdb, 0xb3, 0x2c, 0xfe, 0x1f, 0x8b, 0x93, 0x75, 0x02, + 0x4f, 0x0d, 0x82, 0xac, 0xfb, 0xcc, 0x0b, 0x2b, 0x40, 0x65, 0xf7, 0x28, 0xe3, 0xcb, 0xc3, 0xd0, 0x7f, 0xfd, 0xcc, + 0x19, 0x35, 0xba, 0x70, 0x8a, 0xe1, 0x9c, 0xa2, 0x31, 0x84, 0xe3, 0x8f, 0x4f, 0x27, 0xbd, 0xb8, 0x67, 0xfc, 0xa7, + 0x49, 0x2f, 0xac, 0xea, 0x35, 0x6d, 0x48, 0x1c, 0xff, 0xb0, 0xf9, 0x9b, 0x45, 0x1e, 0xec, 0x7c, 0xb5, 0x42, 0x8a, + 0xac, 0x0b, 0xa9, 0x4e, 0xab, 0x56, 0x55, 0x17, 0x03, 0xce, 0xd9, 0x1f, 0x8b, 0x97, 0x3a, 0xbb, 0x5f, 0xf4, 0x3f, + 0x9a, 0x79, 0x4d, 0xeb, 0xa3, 0x0f, 0xee, 0xa6, 0x50, 0x35, 0xfb, 0x19, 0xdd, 0x3b, 0xbd, 0xa3, 0x9c, 0xb2, 0x99, + 0x4b, 0x7c, 0xee, 0xab, 0xa5, 0xe7, 0x09, 0xb7, 0x16, 0x1a, 0x99, 0xa1, 0x3b, 0x75, 0x8f, 0xe0, 0x52, 0x24, 0x4d, + 0xcb, 0xde, 0xc2, 0x35, 0x13, 0xe9, 0x4c, 0x7f, 0x76, 0x92, 0xd2, 0x9b, 0xce, 0x67, 0x35, 0x45, 0xcc, 0xaf, 0x88, + 0x99, 0x71, 0x96, 0x04, 0x4f, 0x21, 0x22, 0xd0, 0xda, 0x8a, 0xf2, 0xa9, 0xa2, 0xba, 0xe2, 0x57, 0xbf, 0x9e, 0x65, + 0x81, 0x9f, 0x99, 0x4d, 0x75, 0x2b, 0x57, 0xf4, 0xd1, 0x69, 0x9e, 0xe5, 0x3a, 0x76, 0x20, 0x67, 0x1b, 0xe0, 0xc0, + 0xfe, 0x4d, 0x47, 0x30, 0xac, 0xad, 0xb9, 0x3f, 0x12, 0xbd, 0x31, 0x0a, 0xfe, 0x42, 0x00, 0x46, 0xa4, 0x68, 0xc3, + 0x3e, 0xda, 0x42, 0x17, 0x32, 0xaa, 0xf7, 0x27, 0x6e, 0xff, 0xbc, 0x71, 0xbd, 0xf3, 0x6b, 0xa7, 0x35, 0xa7, 0x54, + 0xe6, 0xe9, 0x74, 0xb4, 0x91, 0xdd, 0xf5, 0xb0, 0x0c, 0xf2, 0x5b, 0xbe, 0xd0, 0xe8, 0xc5, 0x2f, 0x1d, 0x6c, 0x69, + 0xf9, 0x11, 0xa9, 0x7a, 0x92, 0x08, 0xe4, 0x58, 0xcb, 0xc3, 0xab, 0xb9, 0x23, 0x95, 0x0a, 0x1c, 0xd5, 0x3d, 0x19, + 0xf9, 0x66, 0x4e, 0xd9, 0xb5, 0xa4, 0x1d, 0xc1, 0xc6, 0xb0, 0x6c, 0xbe, 0xe6, 0xd2, 0x2c, 0xb5, 0x5e, 0xd9, 0xb3, + 0x13, 0xe1, 0x05, 0x8b, 0x57, 0x62, 0x9b, 0x82, 0xcb, 0xaf, 0xc6, 0x92, 0xb9, 0x79, 0x3d, 0x91, 0x80, 0x59, 0xe6, + 0xd2, 0x6e, 0xf2, 0x19, 0xe9, 0x4a, 0xfd, 0x39, 0x2c, 0x4c, 0x9f, 0x7c, 0x63, 0x31, 0x41, 0xdb, 0xaa, 0x55, 0xb9, + 0xf2, 0x1c, 0xdf, 0xd0, 0xa4, 0xd8, 0x3b, 0xda, 0x33, 0xe9, 0x21, 0x1c, 0x89, 0xc1, 0xcd, 0xbc, 0xa5, 0x92, 0x32, + 0x8d, 0x63, 0x27, 0x49, 0xff, 0x55, 0x5f, 0x86, 0x49, 0x82, 0x83, 0x58, 0xfd, 0x07, 0xd5, 0x98, 0x01, 0x87, 0xd4, + 0x47, 0x27, 0x2a, 0x82, 0xd1, 0x4c, 0x21, 0xba, 0x41, 0xfd, 0x4a, 0x9d, 0x88, 0x67, 0x2f, 0x56, 0x38, 0xe9, 0xcb, + 0x1c, 0x69, 0x5e, 0xf8, 0x8e, 0xdd, 0x3e, 0x32, 0x80, 0x46, 0x61, 0x6e, 0x8c, 0x81, 0x5d, 0xd6, 0xa4, 0x2d, 0x05, + 0x37, 0x7a, 0x03, 0x4d, 0xe0, 0xe6, 0x3d, 0x9d, 0x85, 0x3e, 0x17, 0xe9, 0xc4, 0xe2, 0x8e, 0x76, 0x31, 0xb9, 0xd6, + 0x7c, 0x5d, 0xb0, 0x0b, 0xf9, 0xbb, 0xb9, 0x56, 0xde, 0xb6, 0x69, 0x2e, 0x54, 0x20, 0xc8, 0x51, 0xe0, 0x94, 0xcb, + 0x7b, 0xa2, 0x46, 0xc7, 0xc1, 0xeb, 0xd4, 0x86, 0xd2, 0x1f, 0xf8, 0x75, 0x10, 0x88, 0xce, 0x7e, 0xd0, 0xa6, 0xdf, + 0xb7, 0x54, 0x85, 0x59, 0xd4, 0x43, 0x2c, 0x89, 0x49, 0x77, 0x77, 0xeb, 0xa3, 0x8e, 0xcf, 0xea, 0x1a, 0xb7, 0xf0, + 0x12, 0x83, 0x2b, 0x38, 0x42, 0xab, 0x58, 0x48, 0x9e, 0x81, 0x4f, 0xb7, 0xb0, 0xf1, 0x63, 0xe6, 0x6e, 0x47, 0xe4, + 0xfe, 0xea, 0x7d, 0xc5, 0x91, 0xdd, 0x62, 0xac, 0x9e, 0x3c, 0x45, 0xec, 0x1d, 0xad, 0x32, 0xc3, 0x95, 0x6b, 0xde, + 0x2b, 0xdc, 0xf6, 0x9e, 0x4f, 0xf1, 0xc0, 0x0c, 0x02, 0x7b, 0x46, 0xcc, 0x8e, 0xb1, 0x7e, 0x6d, 0xd8, 0xdb, 0xbe, + 0x73, 0x5d, 0x0a, 0x18, 0xb5, 0x2e, 0xe8, 0x83, 0x20, 0xbe, 0xcf, 0x0c, 0x58, 0x7b, 0x0e, 0xcc, 0xde, 0xe8, 0x8e, + 0xdb, 0x24, 0xec, 0x4a, 0x7d, 0x3c, 0x3e, 0x64, 0xbd, 0x2b, 0x3d, 0x2a, 0x45, 0x1f, 0x05, 0x2e, 0x9a, 0x00, 0x31, + 0x07, 0x47, 0xb2, 0x17, 0x7b, 0xf2, 0x89, 0x98, 0x0b, 0x91, 0x8b, 0x66, 0xb8, 0x07, 0x04, 0x23, 0x87, 0x15, 0xb6, + 0xff, 0x88, 0xd2, 0x86, 0x87, 0x5b, 0x2c, 0x64, 0x98, 0xf3, 0x1a, 0xd7, 0xdd, 0xfd, 0x3b, 0x60, 0xce, 0x5d, 0xbd, + 0x45, 0xdf, 0xe9, 0x31, 0x28, 0xbd, 0x4f, 0x83, 0xa8, 0x55, 0xe4, 0x1e, 0x5e, 0x84, 0xf0, 0xba, 0xc8, 0x8b, 0x46, + 0x20, 0xdd, 0x1d, 0x86, 0xe1, 0x57, 0x10, 0x31, 0x7d, 0x2d, 0x01, 0x7f, 0xa2, 0x30, 0x10, 0x0b, 0x5e, 0x6e, 0xaa, + 0x4a, 0x5d, 0xd9, 0x7a, 0x0c, 0xb5, 0xf0, 0x0c, 0xac, 0xaa, 0x93, 0x8c, 0xe0, 0x6e, 0x73, 0x96, 0x32, 0xbf, 0xad, + 0xc8, 0x8f, 0x65, 0x5d, 0x1c, 0xd2, 0xa6, 0xbd, 0x8a, 0xdf, 0x32, 0xec, 0x05, 0x10, 0xa3, 0x2a, 0x33, 0x53, 0x25, + 0x22, 0x5f, 0x17, 0xa4, 0x8a, 0x94, 0x3d, 0x4b, 0xb6, 0x57, 0xf4, 0x57, 0xaf, 0xd8, 0x12, 0x67, 0xb6, 0x25, 0x27, + 0xfc, 0x54, 0x4d, 0xe2, 0xf9, 0xaf, 0xf2, 0xce, 0xfd, 0x6d, 0xfa, 0xfe, 0x7c, 0x98, 0xc4, 0x59, 0x2e, 0xe9, 0xba, + 0xb5, 0xb8, 0xf8, 0xa4, 0xf5, 0xb7, 0xab, 0x3d, 0x6a, 0xdf, 0xad, 0xe5, 0xf4, 0x76, 0xe4, 0x9a, 0xf9, 0x12, 0xd2, + 0xac, 0xf5, 0xe1, 0x24, 0x7f, 0x95, 0x61, 0x97, 0x37, 0x7a, 0xd0, 0xb4, 0x64, 0xfa, 0xe2, 0xe7, 0x8a, 0x6d, 0x19, + 0xba, 0x12, 0xbd, 0xf3, 0xd3, 0x17, 0xe3, 0xae, 0x11, 0xb3, 0x35, 0x90, 0x3c, 0x61, 0x5e, 0x44, 0x63, 0xcf, 0x8d, + 0x05, 0x02, 0xbd, 0x4f, 0xfb, 0x16, 0xcc, 0xd2, 0x6f, 0x9c, 0x28, 0xb9, 0x4f, 0xb0, 0x3f, 0xd2, 0x22, 0x18, 0xb8, + 0x73, 0x57, 0xbd, 0xe0, 0x38, 0x0b, 0x7d, 0xd4, 0xb5, 0xdc, 0x17, 0x31, 0x72, 0x9b, 0xe3, 0xf4, 0x6e, 0x29, 0x99, + 0x08, 0xfb, 0xc5, 0x53, 0xce, 0xac, 0xef, 0x7e, 0x99, 0x25, 0xad, 0xd5, 0x02, 0xfd, 0x8a, 0xab, 0xe7, 0x6e, 0xfd, + 0x27, 0x10, 0xbd, 0x9f, 0x76, 0x58, 0x2c, 0xad, 0xd4, 0x9d, 0xaa, 0xd2, 0x37, 0x78, 0x52, 0x06, 0xc8, 0x59, 0x40, + 0x67, 0xda, 0x5a, 0xee, 0x16, 0x46, 0xfd, 0xa5, 0xc7, 0xb9, 0xfe, 0xde, 0xca, 0x18, 0x1c, 0x42, 0xb4, 0xfd, 0x0a, + 0xe7, 0x71, 0x7b, 0x25, 0x5e, 0x0b, 0xaf, 0x28, 0x34, 0x5b, 0x1e, 0xbf, 0x54, 0x30, 0x89, 0x7e, 0x12, 0x91, 0x3b, + 0x3f, 0x5b, 0xb3, 0x30, 0x31, 0x9f, 0xce, 0x2d, 0xbf, 0x47, 0xa7, 0xe6, 0x02, 0x5a, 0xee, 0xf9, 0x81, 0x8b, 0xf9, + 0x3f, 0xcb, 0x2c, 0x4b, 0x6a, 0x85, 0x66, 0xd9, 0x36, 0xc0, 0xd1, 0x0d, 0x4f, 0x71, 0xe3, 0x39, 0x0e, 0x28, 0xb4, + 0x83, 0x52, 0x6f, 0xb5, 0x40, 0x8d, 0x14, 0x61, 0xa1, 0xa0, 0x90, 0x7e, 0x44, 0xf3, 0x28, 0x3b, 0x62, 0xc0, 0x48, + 0xb7, 0xfa, 0x9b, 0x5c, 0x5b, 0x64, 0x45, 0xab, 0xfd, 0xb2, 0x7c, 0xbf, 0x2f, 0x82, 0xe8, 0xbf, 0x5d, 0x80, 0x22, + 0xd6, 0x86, 0xec, 0x4d, 0xc0, 0x34, 0xa2, 0x98, 0xa2, 0xe0, 0xdb, 0x80, 0xa4, 0x50, 0x29, 0x7b, 0x17, 0xb6, 0x08, + 0x33, 0x97, 0x5a, 0x52, 0xc6, 0x98, 0x78, 0xde, 0x00, 0x74, 0xa4, 0xff, 0xda, 0xf8, 0x2e, 0x3b, 0x33, 0x1e, 0x26, + 0xe5, 0x1e, 0x11, 0x91, 0xa0, 0x9e, 0xca, 0x4a, 0xc0, 0x7e, 0xb3, 0x29, 0xbe, 0x15, 0x94, 0xa4, 0x49, 0xed, 0x45, + 0xb0, 0xdb, 0x86, 0x0c, 0x2e, 0xa3, 0xb5, 0x86, 0x82, 0x86, 0xef, 0x0d, 0xe3, 0x01, 0xab, 0x5c, 0xf4, 0x12, 0x9b, + 0xfc, 0x08, 0x9e, 0xa9, 0xe8, 0x2e, 0xdf, 0xa2, 0x8f, 0x77, 0x54, 0xe6, 0x65, 0xa7, 0x75, 0xed, 0xdd, 0x81, 0x41, + 0x18, 0x36, 0x3e, 0x35, 0xd0, 0x91, 0xbe, 0x1e, 0xb0, 0x41, 0xf3, 0x78, 0x86, 0x0d, 0x38, 0xa5, 0x2b, 0x32, 0x5a, + 0xe7, 0x23, 0xcb, 0x17, 0x7b, 0xfc, 0x3e, 0x1a, 0x21, 0x63, 0xe2, 0x08, 0xec, 0xa8, 0x01, 0x1e, 0x12, 0x66, 0x08, + 0x3f, 0xf6, 0x0e, 0xf6, 0xb5, 0x81, 0xff, 0x4a, 0x13, 0x50, 0x40, 0x8e, 0xf6, 0xb8, 0x90, 0x54, 0x3c, 0x86, 0x19, + 0x83, 0xc2, 0x87, 0x64, 0x28, 0x73, 0xfc, 0xef, 0xbb, 0x92, 0x62, 0xcd, 0x70, 0x57, 0x8c, 0x4c, 0x1b, 0xee, 0xbe, + 0x6b, 0xcc, 0x6f, 0xe9, 0xde, 0x51, 0x14, 0x3d, 0x1d, 0x03, 0x0f, 0xa1, 0x14, 0xa1, 0xec, 0xcc, 0x84, 0x2a, 0x00, + 0xfd, 0xa2, 0x19, 0x6d, 0x40, 0xeb, 0xc7, 0xc8, 0x1d, 0xdf, 0x5e, 0xc1, 0xc9, 0x45, 0xa2, 0xc0, 0xba, 0xf8, 0xfa, + 0x97, 0x4a, 0x7a, 0xef, 0xde, 0x25, 0x5b, 0xe5, 0xca, 0x9c, 0xda, 0xe2, 0xa1, 0x0b, 0xbe, 0x4c, 0xd7, 0xc7, 0xde, + 0xcb, 0x13, 0xa4, 0xa6, 0x61, 0xb5, 0x8e, 0x6d, 0xc2, 0x93, 0x16, 0xbb, 0xe4, 0xed, 0xfc, 0xe5, 0x49, 0x36, 0xf1, + 0x8a, 0xa5, 0x40, 0xa7, 0x67, 0x56, 0xc5, 0x36, 0xd2, 0xd3, 0x65, 0xc3, 0x67, 0x06, 0xf8, 0x3c, 0x1b, 0xc8, 0x3d, + 0xcf, 0xf5, 0xe7, 0xfa, 0xed, 0x92, 0x87, 0x84, 0x92, 0xdd, 0xd6, 0x38, 0xbd, 0x6b, 0x6c, 0x33, 0x1f, 0xcd, 0xdc, + 0x3e, 0xb6, 0x3e, 0xf3, 0x91, 0xc9, 0xd2, 0x05, 0x25, 0x61, 0x7b, 0x3c, 0x24, 0x9d, 0x6c, 0xb2, 0xe0, 0xcc, 0xa9, + 0x2f, 0x91, 0xcb, 0xe2, 0xbc, 0xae, 0x34, 0x17, 0x36, 0x2b, 0xe8, 0x32, 0x80, 0x53, 0x9d, 0x3a, 0x09, 0xae, 0x2a, + 0x02, 0xa7, 0xa6, 0x66, 0xaa, 0x28, 0x9e, 0xb2, 0x66, 0xbb, 0x39, 0x51, 0xfd, 0x14, 0x2d, 0x2e, 0x75, 0x2a, 0x4a, + 0xd4, 0x4c, 0xb6, 0xcc, 0x14, 0xc8, 0x64, 0x51, 0xa4, 0x39, 0x89, 0x15, 0x0e, 0xfa, 0x9e, 0x53, 0x24, 0x7b, 0xd1, + 0x6e, 0x3e, 0x5e, 0xd9, 0x5a, 0xb2, 0xc2, 0x68, 0x66, 0xab, 0x79, 0x76, 0x22, 0x15, 0xdb, 0x07, 0xca, 0xa1, 0x70, + 0xdf, 0x26, 0xb0, 0x52, 0x23, 0xe5, 0xa5, 0xa8, 0x23, 0x35, 0x3c, 0xc5, 0x5f, 0x9b, 0x6e, 0x88, 0xd1, 0x6c, 0xd8, + 0xd1, 0x46, 0xb3, 0xd9, 0x0c, 0x8a, 0x4d, 0x8d, 0x43, 0xab, 0xd4, 0x74, 0x1b, 0x91, 0xaf, 0x50, 0x35, 0xb2, 0x6f, + 0xac, 0x2c, 0x88, 0x25, 0x73, 0x88, 0xd7, 0x50, 0x98, 0x24, 0xf7, 0x28, 0xb6, 0xe8, 0xf5, 0xa2, 0xcd, 0xcd, 0x91, + 0x63, 0x43, 0x76, 0xae, 0xe2, 0x5c, 0xa6, 0x2b, 0x91, 0x47, 0x81, 0x50, 0x58, 0x89, 0xa4, 0x04, 0x93, 0x31, 0x4f, + 0xdf, 0xf8, 0x29, 0xe9, 0xb9, 0x47, 0x40, 0x34, 0xfb, 0x82, 0x6a, 0x45, 0x7d, 0x11, 0x23, 0x3e, 0x92, 0x90, 0x63, + 0xf8, 0x8a, 0x61, 0xf8, 0xde, 0xa6, 0xa2, 0xff, 0x6a, 0xe7, 0x53, 0x13, 0x65, 0x72, 0x54, 0xed, 0x10, 0x69, 0x03, + 0xb1, 0x35, 0x40, 0x3c, 0x4d, 0xc7, 0x12, 0x94, 0x46, 0x8f, 0xc1, 0xce, 0xe7, 0xe5, 0x69, 0x27, 0xd4, 0xe2, 0x48, + 0x77, 0x99, 0x9b, 0x00, 0x67, 0xfd, 0x30, 0xbd, 0x4d, 0xcc, 0xee, 0xfe, 0xcc, 0x01, 0xdd, 0x89, 0x71, 0x84, 0x8f, + 0x66, 0x97, 0x55, 0x08, 0x4f, 0xfc, 0x3b, 0xaf, 0xda, 0x94, 0x84, 0x13, 0xe2, 0x8d, 0x63, 0x03, 0x98, 0xce, 0xb4, + 0xa7, 0x6a, 0x39, 0x10, 0x29, 0x7e, 0x0d, 0xbe, 0xc1, 0x95, 0xd0, 0xa0, 0x20, 0x51, 0x3f, 0x8f, 0x5c, 0x13, 0x53, + 0x3d, 0xce, 0x7f, 0x44, 0x28, 0x03, 0x83, 0x04, 0x32, 0x2a, 0xd8, 0x3d, 0x6f, 0x8d, 0x28, 0xd6, 0x7a, 0xd2, 0xb2, + 0xcb, 0x99, 0xeb, 0x36, 0xb5, 0x33, 0x7b, 0xdf, 0x0a, 0x0e, 0x04, 0xfd, 0xe5, 0x56, 0xa6, 0x1f, 0x01, 0x06, 0xc3, + 0xac, 0x30, 0xff, 0x89, 0x0c, 0x9a, 0x2b, 0x64, 0xd4, 0x5d, 0x77, 0xd5, 0x3b, 0xc1, 0xd8, 0x99, 0x8c, 0x23, 0x9f, + 0xfc, 0x3c, 0x70, 0xf7, 0xad, 0x48, 0x35, 0x9e, 0xb9, 0x8d, 0x91, 0x4f, 0x26, 0x81, 0xd9, 0xb6, 0x6e, 0x54, 0x53, + 0x26, 0x38, 0x12, 0x31, 0x95, 0x7e, 0x73, 0x1f, 0xb7, 0xe1, 0x59, 0x7e, 0xf0, 0xdf, 0x6f, 0xd3, 0xc4, 0xb9, 0x17, + 0x76, 0x61, 0xba, 0x89, 0x37, 0x0e, 0xba, 0xdf, 0xb5, 0x8f, 0xe6, 0x1a, 0x0f, 0x53, 0x91, 0xd4, 0x76, 0xa2, 0xce, + 0x47, 0xea, 0xe1, 0x35, 0x9d, 0x9f, 0x49, 0xb3, 0xce, 0xf5, 0x9f, 0xaa, 0x0e, 0x06, 0xfd, 0x15, 0x73, 0xb6, 0x45, + 0xbc, 0xd7, 0x9e, 0x6b, 0x29, 0xbc, 0x83, 0xaf, 0xcc, 0xb9, 0x15, 0xf4, 0x2b, 0x17, 0x95, 0x67, 0xaf, 0x49, 0xd7, + 0x78, 0x52, 0x56, 0x13, 0x36, 0xf5, 0x20, 0x4e, 0xf9, 0xab, 0xe0, 0x18, 0xa0, 0x37, 0x54, 0x8d, 0x91, 0xb2, 0x8b, + 0xf7, 0xd5, 0xc0, 0x99, 0x0a, 0xf1, 0x8f, 0x82, 0xa1, 0x51, 0xda, 0x96, 0xea, 0x18, 0x5b, 0xef, 0x31, 0x8f, 0x47, + 0x95, 0xcb, 0xea, 0x09, 0x0b, 0x4e, 0x9d, 0x9d, 0xdf, 0xfd, 0x88, 0x6b, 0x1e, 0x60, 0x9d, 0xd5, 0xfe, 0x0a, 0x9c, + 0xd7, 0xfe, 0x33, 0xdd, 0x7c, 0x28, 0xba, 0x27, 0x5a, 0x6f, 0xe6, 0xde, 0xf3, 0x6c, 0xd6, 0x9f, 0xef, 0x45, 0x68, + 0x35, 0x5c, 0x67, 0x7c, 0x7a, 0xcb, 0xef, 0x40, 0x67, 0x3b, 0xe8, 0x1a, 0xef, 0x2b, 0xcd, 0x7b, 0x3b, 0x0b, 0x56, + 0xaa, 0xa8, 0x75, 0x8e, 0x1d, 0xba, 0xd6, 0x78, 0x3c, 0xb8, 0xc8, 0xa4, 0xb1, 0x3a, 0x59, 0x79, 0x68, 0x85, 0xca, + 0xd7, 0x8b, 0xb8, 0x63, 0x27, 0xd1, 0xcd, 0xb2, 0x11, 0x25, 0x12, 0xe4, 0x6f, 0x83, 0x42, 0x31, 0x1c, 0x32, 0xe1, + 0x61, 0xdc, 0x9b, 0x08, 0x61, 0x5e, 0x4b, 0xb9, 0x10, 0xab, 0x1d, 0x5e, 0xaf, 0xd0, 0x23, 0xe0, 0x60, 0x49, 0x95, + 0xb4, 0x91, 0x88, 0xba, 0x94, 0x7d, 0x58, 0xdd, 0xfe, 0x50, 0x2f, 0xee, 0xca, 0x5f, 0xd5, 0xb6, 0x66, 0xd1, 0xfc, + 0x8b, 0x12, 0x8e, 0x95, 0x08, 0x9b, 0x29, 0xb6, 0x75, 0xf4, 0x7f, 0x44, 0x85, 0x0e, 0x9d, 0x0b, 0x80, 0xda, 0x0f, + 0x95, 0x05, 0x8a, 0x62, 0x04, 0x68, 0x3f, 0xa9, 0xb2, 0x90, 0x7a, 0xc7, 0x1f, 0xcc, 0xae, 0x5b, 0x86, 0x2c, 0x17, + 0xc1, 0x58, 0x9d, 0x6d, 0x00, 0x08, 0xab, 0x4e, 0x60, 0x02, 0x51, 0x34, 0x8a, 0xb2, 0x29, 0x37, 0xd8, 0x2d, 0x5e, + 0x41, 0xb4, 0xfa, 0xfa, 0x4c, 0xf4, 0x8c, 0xac, 0xa4, 0x2a, 0x59, 0xe6, 0xfb, 0x57, 0x16, 0xcc, 0x95, 0x34, 0x7c, + 0x6b, 0xcf, 0xed, 0x6c, 0xd1, 0x79, 0x7f, 0x57, 0xd3, 0xbf, 0xb0, 0x9b, 0xe1, 0x6f, 0xba, 0x01, 0x33, 0xcc, 0x27, + 0xb7, 0xdf, 0x4f, 0xb1, 0x26, 0x1c, 0xff, 0xc8, 0x2a, 0x86, 0x85, 0x2b, 0x08, 0x16, 0x35, 0x46, 0x9c, 0x92, 0x7f, + 0xec, 0x03, 0x05, 0xda, 0xc3, 0x86, 0x02, 0x83, 0x51, 0xe5, 0xa1, 0x12, 0xe9, 0x53, 0xf1, 0xcb, 0x36, 0x90, 0x41, + 0x27, 0x1c, 0x4a, 0x06, 0x76, 0x6a, 0xd7, 0x2a, 0x31, 0x5b, 0x73, 0xeb, 0x3f, 0x66, 0x05, 0x9b, 0x61, 0xc0, 0x12, + 0xf5, 0x90, 0x46, 0x7a, 0x59, 0xb5, 0x08, 0xef, 0x0d, 0x4d, 0xdd, 0x43, 0x90, 0x5a, 0x16, 0x09, 0x7f, 0x60, 0x1e, + 0xa0, 0x46, 0x30, 0x66, 0x9a, 0x67, 0xa5, 0x1c, 0x42, 0x2e, 0xd3, 0xe3, 0x54, 0x14, 0xa3, 0x96, 0xe5, 0x3a, 0x63, + 0x15, 0x47, 0x5e, 0xb3, 0x38, 0x6f, 0x66, 0x51, 0xae, 0x51, 0x36, 0x2c, 0xb8, 0xfe, 0x0c, 0x89, 0x46, 0xb1, 0x41, + 0x43, 0xec, 0x8e, 0x73, 0x52, 0xa6, 0x39, 0x47, 0x1d, 0x92, 0x5b, 0x72, 0x8f, 0x58, 0xcd, 0x6c, 0x25, 0x4c, 0x8e, + 0x56, 0x6d, 0x46, 0xd8, 0xee, 0x68, 0x1c, 0x33, 0x4d, 0x1c, 0x4f, 0x21, 0xf4, 0x40, 0x9b, 0x3d, 0x2d, 0xd9, 0x71, + 0xf1, 0x7f, 0x90, 0x02, 0xba, 0x79, 0xb4, 0x42, 0x30, 0x17, 0xfb, 0x18, 0xa5, 0x86, 0x9b, 0x63, 0x17, 0xd8, 0xb0, + 0xfd, 0xe7, 0x26, 0xba, 0xa2, 0xe3, 0xb9, 0x5e, 0xa9, 0x91, 0x83, 0x38, 0xb1, 0x3e, 0xdb, 0x83, 0xd0, 0x7a, 0x44, + 0xc2, 0x81, 0xb2, 0xce, 0x7a, 0x65, 0x1e, 0xeb, 0xd2, 0x7f, 0xfd, 0x4b, 0x6d, 0x09, 0x41, 0x60, 0x58, 0x3d, 0xd8, + 0xfe, 0x04, 0x56, 0x5c, 0xc8, 0x12, 0x99, 0xf1, 0xc2, 0xbf, 0x62, 0x87, 0xaf, 0x69, 0x56, 0x56, 0x3a, 0xc7, 0xe5, + 0xcc, 0x42, 0xa7, 0xa1, 0x6a, 0x8e, 0x79, 0x1e, 0x32, 0x16, 0xd3, 0x0b, 0x83, 0x9c, 0x0b, 0x02, 0x1a, 0x9a, 0x73, + 0xee, 0xca, 0x7a, 0x93, 0xe0, 0x36, 0x82, 0x62, 0x29, 0x40, 0x57, 0xe8, 0x32, 0xbd, 0xf3, 0xcd, 0x30, 0x0e, 0x86, + 0xdc, 0xcc, 0x00, 0x84, 0x2d, 0x11, 0x54, 0x32, 0xf0, 0xac, 0xd8, 0xb3, 0x92, 0x73, 0x30, 0xe7, 0x15, 0xea, 0xbd, + 0x46, 0xfa, 0x1b, 0x24, 0x5c, 0xa0, 0x5a, 0x29, 0x70, 0x32, 0xa0, 0xcb, 0x52, 0x2b, 0x34, 0x2f, 0x11, 0x62, 0xac, + 0x01, 0x49, 0x6d, 0xe2, 0x97, 0xf3, 0x02, 0xf7, 0xbc, 0x9f, 0x0d, 0x67, 0x5d, 0x97, 0x00, 0xf2, 0x30, 0x2f, 0xbf, + 0xbd, 0xcc, 0x70, 0x90, 0x13, 0x90, 0xb8, 0x18, 0x98, 0x39, 0xa1, 0x9d, 0x5d, 0xc1, 0x96, 0xba, 0x18, 0x55, 0xb8, + 0xad, 0x61, 0xb2, 0x14, 0x95, 0x6d, 0xb8, 0x3e, 0x86, 0xce, 0x48, 0xfa, 0xce, 0x4f, 0x33, 0x09, 0x33, 0x74, 0xcd, + 0xc9, 0x54, 0xee, 0x04, 0x9b, 0x4f, 0x9a, 0x81, 0xbe, 0xd8, 0xfa, 0x73, 0xe8, 0x7f, 0xda, 0xd8, 0x04, 0xd3, 0xf7, + 0x8c, 0x64, 0xc4, 0x54, 0xa2, 0xcf, 0x1b, 0xcc, 0x3e, 0xed, 0xf7, 0xf9, 0x0e, 0x16, 0xeb, 0xcb, 0xd8, 0xcb, 0x8a, + 0x8d, 0xfa, 0xd8, 0x5a, 0xc6, 0x24, 0x71, 0x2c, 0xb9, 0x3d, 0x28, 0x29, 0xa8, 0xcc, 0x9b, 0xa8, 0x21, 0x23, 0xa6, + 0x35, 0x27, 0x3b, 0xf1, 0xbf, 0x73, 0xc5, 0xcc, 0xc4, 0xc0, 0x8f, 0xb1, 0xc7, 0x3e, 0xbe, 0x7a, 0xe2, 0xad, 0xf6, + 0x23, 0x67, 0xe8, 0x98, 0x3c, 0x40, 0x20, 0x17, 0x98, 0x97, 0x2e, 0x30, 0xe7, 0xd6, 0x8a, 0x35, 0x6b, 0x6a, 0xe5, + 0x3f, 0xbb, 0x2b, 0x7d, 0x60, 0xec, 0x13, 0x41, 0x7f, 0x36, 0xed, 0x66, 0xec, 0x1b, 0xb3, 0x57, 0x03, 0x4e, 0x1d, + 0xcc, 0x6c, 0xbc, 0xa9, 0xf4, 0x1f, 0x6a, 0x73, 0xc5, 0x02, 0x14, 0x39, 0x1b, 0xf9, 0xa4, 0xa9, 0x08, 0xfe, 0xb8, + 0x3a, 0x7b, 0xb1, 0xdd, 0xa2, 0x50, 0x70, 0x65, 0x34, 0xe1, 0x5d, 0x46, 0x3e, 0xd1, 0xd0, 0x06, 0x6f, 0xe4, 0x8d, + 0x6d, 0x5c, 0x46, 0xfb, 0x68, 0x3f, 0x07, 0xb1, 0x0b, 0x82, 0xb6, 0x26, 0x16, 0x04, 0x59, 0x53, 0xe7, 0x0d, 0x23, + 0x12, 0xfc, 0xd6, 0x5a, 0xe9, 0xbc, 0x8e, 0xbd, 0xd2, 0x1d, 0xe7, 0x43, 0x22, 0x46, 0xe0, 0xb6, 0xe8, 0x7a, 0x4b, + 0x42, 0x19, 0x97, 0x8e, 0x4e, 0x26, 0x78, 0xd4, 0x26, 0x4e, 0xaa, 0x6d, 0xaf, 0x47, 0x1d, 0x1e, 0xf5, 0xdd, 0xbc, + 0x18, 0x94, 0xb6, 0x3b, 0xfa, 0x6f, 0xe1, 0xad, 0xcc, 0x91, 0xc7, 0xb5, 0xbe, 0xd3, 0xdc, 0x02, 0xbd, 0x89, 0xe8, + 0x44, 0x51, 0x27, 0x9c, 0xbc, 0x52, 0x8e, 0xff, 0x0b, 0x85, 0x15, 0x0c, 0x81, 0xc9, 0x4c, 0x24, 0xaa, 0x2d, 0x48, + 0x67, 0xa1, 0xbf, 0xf5, 0xf1, 0xb5, 0x42, 0x16, 0xd8, 0x62, 0x06, 0x71, 0xa8, 0x07, 0x8d, 0xe0, 0x25, 0x14, 0x88, + 0xe2, 0xde, 0x19, 0x1a, 0x83, 0x1e, 0x94, 0x3b, 0xa4, 0x81, 0x62, 0xd0, 0xb2, 0x14, 0x1a, 0xda, 0x84, 0x54, 0xbb, + 0xdf, 0x1b, 0xca, 0xfa, 0x25, 0x37, 0xd4, 0x28, 0xa2, 0x51, 0x6f, 0x1d, 0x24, 0x20, 0xe8, 0x15, 0x07, 0x69, 0xa0, + 0xbc, 0x5e, 0x12, 0x23, 0x96, 0xf1, 0x38, 0xc8, 0xd5, 0xc2, 0xe3, 0x95, 0x90, 0x53, 0xb3, 0x42, 0xc8, 0x31, 0x80, + 0x61, 0xec, 0x81, 0x7b, 0x39, 0xec, 0x60, 0x11, 0xf0, 0xbc, 0x5c, 0x51, 0xcf, 0x46, 0xb1, 0xb0, 0xfd, 0xbb, 0xbc, + 0x98, 0x5f, 0xd2, 0xde, 0x26, 0x29, 0x8f, 0x55, 0x9a, 0x4a, 0xf0, 0xdd, 0x9f, 0xde, 0xc5, 0x7c, 0x2c, 0x59, 0xb3, + 0xa5, 0x32, 0x07, 0x13, 0xa2, 0xeb, 0x90, 0x91, 0x3e, 0x55, 0xc5, 0xb1, 0x49, 0x01, 0x35, 0x1c, 0x87, 0x9d, 0x0b, + 0xc2, 0xe3, 0x84, 0x35, 0x9c, 0x4b, 0xcc, 0x61, 0x87, 0x0a, 0x36, 0xc2, 0xe8, 0x86, 0x12, 0x62, 0x49, 0x6d, 0xc4, + 0xb7, 0x03, 0x5c, 0x82, 0xef, 0x17, 0x5a, 0x79, 0x1f, 0x20, 0xfe, 0xd8, 0xa4, 0x33, 0x40, 0x2e, 0xb1, 0xb2, 0x98, + 0xb0, 0xed, 0xdf, 0x2a, 0x6d, 0x2b, 0x0f, 0xd3, 0xcd, 0xbd, 0x39, 0xbb, 0x03, 0x85, 0x33, 0x27, 0x19, 0xf9, 0x31, + 0xe9, 0x51, 0x39, 0x93, 0xff, 0xdc, 0x30, 0x06, 0x64, 0xe6, 0x0e, 0xf6, 0x95, 0xc0, 0x98, 0xbe, 0xd2, 0xd1, 0x84, + 0x7f, 0x89, 0x94, 0x9f, 0x8d, 0x46, 0x4c, 0x5e, 0x61, 0xc8, 0x55, 0xfa, 0x4a, 0xbf, 0xcf, 0x5c, 0xf4, 0x52, 0xde, + 0x38, 0xc6, 0xa8, 0xb8, 0xc9, 0xf8, 0xc5, 0xc8, 0x16, 0x22, 0xf5, 0x66, 0xcc, 0xb6, 0x3f, 0x5b, 0xa2, 0x7b, 0x86, + 0x07, 0x92, 0xa0, 0x71, 0xa3, 0x40, 0x01, 0x76, 0x31, 0xc1, 0x90, 0xdc, 0x01, 0x93, 0xa6, 0x69, 0x9e, 0xa7, 0x50, + 0xd7, 0x6a, 0x38, 0xa9, 0x6c, 0xab, 0xbb, 0xac, 0x4c, 0x65, 0xdb, 0xc1, 0x70, 0x8d, 0x82, 0xc4, 0x51, 0xe3, 0x14, + 0x15, 0xb3, 0xea, 0x69, 0x52, 0x86, 0x05, 0x44, 0x5a, 0x71, 0x8e, 0xdf, 0x5c, 0x9a, 0x4c, 0x67, 0xa7, 0xd8, 0x2b, + 0x3c, 0x4f, 0x85, 0x08, 0x76, 0x67, 0x15, 0x09, 0xbb, 0xb6, 0x65, 0x1d, 0x2d, 0x64, 0xee, 0x5b, 0x17, 0xe8, 0x12, + 0xe2, 0x07, 0x6f, 0xf5, 0xdb, 0xfd, 0x04, 0xec, 0x20, 0x8c, 0xf5, 0x11, 0x5d, 0x7c, 0xd4, 0x0b, 0x4a, 0x2b, 0x3f, + 0x09, 0xce, 0xd9, 0x66, 0xe9, 0xfd, 0x2f, 0x58, 0xdf, 0x94, 0x17, 0x0b, 0x0a, 0x85, 0x15, 0xcb, 0x52, 0x5c, 0xb5, + 0x8c, 0xcf, 0x51, 0x85, 0x55, 0xc8, 0xb1, 0x87, 0x1e, 0x37, 0x10, 0xa9, 0x65, 0x91, 0x34, 0x69, 0xee, 0xac, 0x44, + 0xa6, 0x6b, 0xb0, 0xf3, 0x4a, 0x00, 0x76, 0x6c, 0x52, 0xd5, 0x8b, 0x85, 0xa7, 0x24, 0xc1, 0xd1, 0xad, 0x90, 0xbb, + 0x50, 0x65, 0x0f, 0x14, 0x62, 0x58, 0x07, 0x58, 0x38, 0x2b, 0x58, 0x12, 0xb6, 0x0f, 0xab, 0xf1, 0x63, 0x54, 0x5b, + 0xc0, 0xf8, 0x10, 0x42, 0x7d, 0xb7, 0x83, 0x8e, 0xa2, 0xa3, 0x35, 0x9a, 0xdc, 0xe3, 0x00, 0x19, 0xf4, 0x73, 0x3f, + 0x15, 0x5c, 0xf2, 0x80, 0xbc, 0x18, 0x39, 0x89, 0xab, 0xf1, 0xa6, 0x65, 0xea, 0x5c, 0xf9, 0xee, 0x4b, 0x1b, 0x61, + 0x5d, 0x20, 0x2e, 0xe4, 0x7d, 0xec, 0x90, 0x7d, 0x77, 0x18, 0xad, 0xae, 0x9b, 0x27, 0x8b, 0xfc, 0x59, 0x56, 0x4d, + 0x45, 0xf8, 0xd3, 0xf7, 0x1b, 0x6a, 0x73, 0x16, 0x50, 0xee, 0xbd, 0x5e, 0x70, 0x8a, 0x7a, 0x47, 0x05, 0x22, 0x98, + 0x64, 0xf8, 0xed, 0x23, 0xd2, 0x16, 0x24, 0x62, 0xcd, 0x87, 0x4b, 0xaf, 0x59, 0x7f, 0x0b, 0x82, 0x55, 0x13, 0xe1, + 0xec, 0x57, 0x1a, 0xc4, 0xc1, 0x4b, 0x11, 0x92, 0xae, 0x08, 0x06, 0x3a, 0x2a, 0x88, 0xad, 0xd8, 0xca, 0x5e, 0x56, + 0x6b, 0x08, 0x44, 0x9c, 0x83, 0xcd, 0x67, 0x96, 0xe1, 0x39, 0xf1, 0xea, 0x97, 0x07, 0x29, 0x5c, 0x8c, 0x41, 0xff, + 0xab, 0x65, 0xe1, 0x07, 0x07, 0x07, 0x56, 0x46, 0x56, 0x8e, 0x7a, 0xd7, 0x4b, 0xe5, 0xb6, 0xac, 0xe3, 0xd6, 0xaa, + 0xf7, 0xe4, 0x05, 0x28, 0x8d, 0x36, 0x83, 0x64, 0xb7, 0x8e, 0x99, 0x1a, 0xc3, 0x43, 0x56, 0x8b, 0xfa, 0x98, 0x70, + 0x87, 0xbd, 0x91, 0x86, 0xbd, 0x83, 0x89, 0x68, 0xbc, 0x6f, 0xff, 0xc4, 0x48, 0x43, 0xc2, 0x74, 0xcc, 0x21, 0x77, + 0x50, 0x66, 0x4c, 0x4f, 0x05, 0x6d, 0xc7, 0x11, 0xcf, 0x45, 0x92, 0xce, 0xfd, 0x2b, 0xc3, 0xfb, 0x0b, 0x19, 0x5b, + 0x42, 0x46, 0x77, 0x24, 0xa5, 0x08, 0xd7, 0xd2, 0x60, 0x60, 0x8c, 0x60, 0x3e, 0x25, 0x9a, 0x88, 0x65, 0xb7, 0xb9, + 0x20, 0xb1, 0xcf, 0xd5, 0x92, 0xbd, 0x55, 0x2c, 0xa6, 0x04, 0x2d, 0x8a, 0x5e, 0xbc, 0x5c, 0x99, 0x31, 0xe1, 0xd1, + 0xb5, 0x71, 0x13, 0x23, 0x76, 0x67, 0x56, 0x7b, 0x1b, 0x3c, 0x68, 0x9f, 0x7f, 0xbd, 0x51, 0xbc, 0xb8, 0x5d, 0xbe, + 0x84, 0xe0, 0x07, 0x4f, 0x93, 0xc5, 0x50, 0x06, 0xb9, 0xd8, 0x70, 0xc1, 0x03, 0x59, 0x44, 0x6d, 0xb7, 0x1e, 0x23, + 0x36, 0xcf, 0x27, 0x9f, 0xb6, 0x30, 0x3c, 0x93, 0x93, 0xc1, 0xfe, 0x45, 0x07, 0xbf, 0x01, 0x5a, 0x37, 0x29, 0xf2, + 0xef, 0x4a, 0xd5, 0x41, 0x46, 0xf0, 0xf1, 0xcb, 0xed, 0x2f, 0xca, 0x50, 0xd3, 0x33, 0x9a, 0x86, 0xdd, 0xf2, 0xf7, + 0xc9, 0x29, 0xd8, 0x77, 0x65, 0x00, 0xa8, 0xd3, 0xa5, 0x8c, 0xf8, 0x9c, 0x7c, 0x83, 0x30, 0x00, 0x22, 0xbf, 0xf9, + 0x55, 0x3b, 0x3e, 0x36, 0xc7, 0xe5, 0x0f, 0x6d, 0x7b, 0x96, 0x88, 0xfe, 0xae, 0x0d, 0xb3, 0x1d, 0xfb, 0x80, 0x15, + 0x0f, 0xa3, 0x44, 0xb4, 0xac, 0xf9, 0x90, 0xb9, 0x4f, 0xf1, 0xb0, 0x79, 0xb4, 0x6a, 0x23, 0x8a, 0x6c, 0xb0, 0x5d, + 0xb2, 0xbf, 0xd0, 0xd2, 0xf9, 0x66, 0x87, 0x66, 0x50, 0xb7, 0x47, 0xc8, 0xab, 0x08, 0x20, 0x1e, 0x83, 0xc1, 0x7f, + 0x6d, 0xe6, 0x3d, 0x5b, 0xac, 0x00, 0x3f, 0x3b, 0x76, 0xfe, 0xf2, 0x7c, 0x6a, 0x11, 0x04, 0x7d, 0xd6, 0x3c, 0xaa, + 0x47, 0x44, 0xd2, 0x4f, 0x67, 0x5b, 0xbd, 0x4f, 0x87, 0x51, 0x89, 0x47, 0x6c, 0xda, 0xfe, 0x1d, 0x8b, 0xba, 0xd8, + 0xde, 0xb3, 0xe9, 0xfc, 0xb9, 0x29, 0x74, 0x06, 0x91, 0xda, 0xc6, 0x99, 0x8c, 0x64, 0x47, 0xa6, 0x01, 0x0d, 0xd1, + 0x5e, 0x28, 0xaf, 0x1f, 0x50, 0x32, 0x0a, 0xe4, 0x18, 0x41, 0xae, 0x8d, 0x8c, 0x2d, 0x27, 0x4b, 0x10, 0x86, 0x25, + 0xce, 0xef, 0xa9, 0x43, 0xd0, 0x4b, 0x85, 0xe4, 0xec, 0x22, 0x5c, 0x6f, 0x87, 0x34, 0x1a, 0x00, 0x4a, 0x8d, 0xb7, + 0x09, 0x1e, 0xb6, 0x20, 0x46, 0x2a, 0xb2, 0x22, 0xf1, 0xa7, 0xc4, 0x45, 0xe5, 0x18, 0x8f, 0x00, 0x24, 0xc6, 0xf1, + 0x50, 0xea, 0x3c, 0xa8, 0x43, 0xf2, 0x8a, 0x89, 0x39, 0xd2, 0xb3, 0x0a, 0x3d, 0x98, 0x69, 0x68, 0x73, 0x35, 0x9a, + 0x2a, 0x68, 0x0e, 0x4a, 0xff, 0x81, 0xea, 0x2a, 0x1f, 0x92, 0x47, 0x06, 0x41, 0x18, 0xae, 0xd6, 0x5b, 0xea, 0xf7, + 0x15, 0x42, 0x8b, 0x03, 0x33, 0xc9, 0x20, 0xce, 0x8d, 0x0f, 0x5b, 0x5d, 0xe3, 0x8b, 0x7a, 0x02, 0x34, 0x27, 0xae, + 0x7c, 0xf8, 0x78, 0x32, 0x50, 0x38, 0x41, 0xc9, 0xe8, 0x4f, 0x50, 0x53, 0x2d, 0xe9, 0x76, 0x1e, 0x37, 0xdd, 0x94, + 0xaf, 0x92, 0x5b, 0x6a, 0x66, 0x29, 0x7a, 0x2d, 0xe5, 0x81, 0x66, 0xbb, 0x95, 0xf5, 0xd7, 0x7f, 0x6a, 0xf8, 0x04, + 0xd0, 0x45, 0xc2, 0xca, 0xc4, 0xb7, 0xa8, 0xc1, 0x2f, 0x3e, 0x1c, 0x9c, 0x8c, 0x61, 0x7b, 0xa8, 0xc5, 0xdc, 0xe1, + 0x38, 0xc7, 0xfe, 0x3d, 0x90, 0x1b, 0xdc, 0x4a, 0xa0, 0xe4, 0x6b, 0x59, 0x84, 0x99, 0xcc, 0x62, 0xa0, 0x72, 0x35, + 0xe8, 0x3a, 0xb0, 0x90, 0x35, 0xb5, 0xe6, 0x87, 0xfe, 0xa7, 0x0a, 0x32, 0xf7, 0x6c, 0x95, 0x02, 0x24, 0xc8, 0xb7, + 0xd2, 0x36, 0xed, 0x7d, 0x8b, 0xdc, 0x41, 0xf7, 0x08, 0x16, 0xb5, 0xdd, 0x61, 0x02, 0x68, 0xa1, 0x83, 0x10, 0x52, + 0xe7, 0x53, 0x28, 0x7a, 0xb9, 0x49, 0x26, 0x74, 0xae, 0x05, 0x9e, 0x2f, 0x1d, 0x1c, 0xfd, 0xfb, 0xe3, 0x81, 0x72, + 0x45, 0x02, 0x97, 0x13, 0x7c, 0x0a, 0x9b, 0xda, 0x9c, 0x01, 0x65, 0xa4, 0x7d, 0x75, 0xb8, 0x62, 0x1f, 0x05, 0xac, + 0x0b, 0x9d, 0x59, 0x08, 0x15, 0x99, 0xec, 0x48, 0xd8, 0x17, 0x45, 0x33, 0xc4, 0x79, 0xc1, 0x55, 0x6c, 0x03, 0x9f, + 0xdb, 0xa4, 0x83, 0x98, 0x8b, 0xb6, 0x05, 0x1f, 0x0b, 0xaa, 0xcc, 0x09, 0x8b, 0x6e, 0x80, 0xd1, 0x5e, 0x7b, 0xa9, + 0xf5, 0x83, 0x76, 0x42, 0x67, 0xc5, 0xbd, 0xeb, 0x2a, 0xc2, 0xc0, 0x27, 0xd8, 0xa9, 0xfd, 0x2b, 0x8a, 0xe3, 0x6f, + 0x9b, 0x71, 0xb4, 0xe0, 0x53, 0x04, 0x06, 0x90, 0x90, 0x6e, 0x98, 0x6d, 0xcd, 0x08, 0x3a, 0x7e, 0x08, 0x35, 0x4a, + 0x01, 0x29, 0x8d, 0x30, 0x38, 0xca, 0xe4, 0x37, 0x41, 0x86, 0xe4, 0xbc, 0x9c, 0xa3, 0x87, 0x21, 0x46, 0x0e, 0x48, + 0x65, 0xae, 0x6c, 0xc7, 0x5e, 0x55, 0x4f, 0x85, 0x3c, 0x71, 0x0e, 0x62, 0x31, 0xf4, 0xc8, 0x88, 0x3f, 0xc8, 0x54, + 0x67, 0xa0, 0x89, 0x01, 0x33, 0x82, 0x03, 0xb1, 0x29, 0x68, 0x84, 0xc0, 0x09, 0x59, 0xb6, 0x7c, 0x29, 0x56, 0x01, + 0x89, 0x50, 0xc4, 0xa2, 0x25, 0x92, 0x1f, 0x31, 0x32, 0x30, 0x43, 0x12, 0xe8, 0x31, 0x7b, 0x4d, 0x07, 0xc6, 0x05, + 0x18, 0x53, 0xa9, 0x1e, 0x40, 0x3e, 0x05, 0xa3, 0xb0, 0x88, 0x50, 0xcb, 0x5d, 0x79, 0x91, 0x34, 0x34, 0x58, 0xc3, + 0xb1, 0x68, 0x2e, 0xe6, 0x28, 0xbd, 0x67, 0xca, 0x10, 0x24, 0x57, 0xad, 0x8c, 0xb0, 0xd3, 0x9f, 0xc7, 0x21, 0xe4, + 0xab, 0x0e, 0x42, 0x9b, 0x1b, 0x67, 0x11, 0x20, 0xf4, 0x48, 0x6c, 0x63, 0x8c, 0x80, 0xa4, 0xa1, 0x03, 0xa9, 0x0b, + 0x10, 0x21, 0x21, 0x44, 0x92, 0x80, 0xe6, 0x7c, 0x8b, 0x44, 0x7c, 0x06, 0x61, 0xae, 0x0b, 0xd2, 0x64, 0x89, 0x4a, + 0xbf, 0x6f, 0x96, 0x61, 0xb9, 0xc3, 0xc9, 0x2c, 0xc8, 0x55, 0x95, 0xb3, 0x00, 0x89, 0x84, 0xd9, 0xea, 0x84, 0xa1, + 0xf3, 0x46, 0xfb, 0x49, 0xc0, 0xd9, 0xc2, 0x84, 0x0c, 0x04, 0xa3, 0x58, 0x14, 0x85, 0x4a, 0xf5, 0x49, 0x81, 0xc3, + 0x08, 0x0d, 0xef, 0x2e, 0x0a, 0x37, 0xf3, 0x64, 0x2d, 0xab, 0xe2, 0x11, 0x93, 0xfb, 0xa1, 0x96, 0x38, 0xa7, 0x40, + 0x72, 0x82, 0xa2, 0xd1, 0xfd, 0xd7, 0xcf, 0x1d, 0x95, 0x44, 0x78, 0xd1, 0xa2, 0xf4, 0x6b, 0x8b, 0xdb, 0x5c, 0xcd, + 0x09, 0x34, 0x69, 0x66, 0xc8, 0x37, 0x9d, 0x8a, 0xf9, 0x95, 0xc1, 0xe5, 0x2e, 0xd8, 0x10, 0x40, 0x9b, 0x41, 0xef, + 0x4b, 0xeb, 0x53, 0xfa, 0x01, 0x46, 0xdf, 0xb8, 0xf3, 0xc2, 0x68, 0x27, 0xeb, 0xbd, 0xa1, 0x0b, 0xeb, 0x67, 0x57, + 0xb5, 0xd3, 0x71, 0x44, 0x02, 0x67, 0x2d, 0x74, 0xc8, 0xe6, 0x95, 0xb0, 0x9c, 0xd9, 0xe2, 0xec, 0xd1, 0xaa, 0xb5, + 0x1c, 0x91, 0x8e, 0x34, 0x1c, 0x90, 0xe3, 0xd9, 0x07, 0xa8, 0xf3, 0x08, 0x18, 0x49, 0x39, 0xf3, 0x5e, 0x71, 0x9c, + 0x37, 0x44, 0x1a, 0xea, 0x39, 0x2f, 0x00, 0xec, 0xca, 0x22, 0x29, 0x79, 0x1d, 0x72, 0x2d, 0xfd, 0xe9, 0x98, 0x47, + 0x8c, 0xb1, 0x73, 0x2a, 0x23, 0x8c, 0x4e, 0xae, 0x6b, 0x8e, 0x8c, 0xb2, 0x0b, 0x26, 0x54, 0xf3, 0xae, 0x34, 0xe5, + 0x81, 0x2c, 0xb2, 0xe9, 0x4a, 0x0b, 0x4e, 0x47, 0x62, 0xae, 0x6e, 0x56, 0x51, 0x3d, 0x4c, 0x10, 0xb1, 0xde, 0xbe, + 0xc1, 0xe4, 0x11, 0xcf, 0x27, 0x82, 0x54, 0xa4, 0xcd, 0xe9, 0x59, 0xc9, 0x07, 0xcc, 0x16, 0x68, 0xb4, 0xf2, 0x5e, + 0x00, 0x94, 0xdf, 0x94, 0xa8, 0x48, 0xb9, 0x6c, 0xd1, 0x41, 0x34, 0xe2, 0xd7, 0x41, 0x36, 0xeb, 0x3d, 0x39, 0x9e, + 0x6f, 0x8d, 0xac, 0x86, 0xc8, 0xd0, 0xea, 0xe8, 0x37, 0x74, 0xe8, 0x2b, 0xc2, 0xa4, 0xd3, 0xf3, 0xd8, 0xd6, 0x02, + 0x2d, 0x86, 0x8a, 0xa7, 0x62, 0x8c, 0x93, 0xea, 0x1a, 0xb1, 0x4c, 0xa9, 0x6f, 0x31, 0xd1, 0x15, 0xf4, 0x93, 0x2d, + 0x05, 0x9b, 0x6f, 0x59, 0xc9, 0x8b, 0x8c, 0x08, 0x7b, 0x8d, 0xf0, 0x62, 0x18, 0x03, 0xf4, 0xaa, 0xa5, 0x74, 0x1e, + 0xe8, 0xad, 0xe8, 0x8a, 0x79, 0xec, 0xc3, 0xeb, 0x2e, 0x49, 0x5e, 0xe0, 0xd6, 0x3c, 0x66, 0x35, 0x96, 0xdf, 0xbc, + 0xfe, 0xc6, 0x54, 0x25, 0xd6, 0xca, 0xca, 0x4f, 0xba, 0x6c, 0xdf, 0x0f, 0x49, 0x83, 0xbc, 0x4d, 0x6b, 0xfb, 0xbd, + 0xc9, 0x37, 0x10, 0x1b, 0x8c, 0xa2, 0x99, 0x2e, 0x16, 0x87, 0x05, 0xd2, 0xaf, 0x97, 0xa0, 0x2b, 0xd3, 0x0c, 0xd2, + 0xbe, 0xaf, 0x2f, 0x7f, 0x03, 0x98, 0x11, 0x63, 0x1f, 0x72, 0x22, 0x5a, 0x89, 0x66, 0xcb, 0xfc, 0xec, 0xec, 0x2d, + 0x08, 0x01, 0x33, 0xd9, 0xcf, 0x0f, 0x33, 0x43, 0xc2, 0x5e, 0x33, 0x13, 0xa1, 0xc0, 0x9a, 0x66, 0x9e, 0x5d, 0xcd, + 0xed, 0xd3, 0x52, 0xb4, 0x78, 0xac, 0x75, 0x95, 0xfa, 0x5e, 0xc6, 0x93, 0x8b, 0xd8, 0x9e, 0x67, 0x68, 0x3d, 0x63, + 0xa4, 0x41, 0x87, 0x17, 0x22, 0x62, 0x8b, 0x67, 0xff, 0x81, 0x99, 0x19, 0x85, 0x80, 0x6a, 0x0a, 0x7d, 0x7b, 0x8b, + 0x78, 0x2c, 0x4d, 0x9e, 0x91, 0xd9, 0xf7, 0x24, 0xdf, 0xac, 0x93, 0xf7, 0x5e, 0xaf, 0x5c, 0xad, 0x70, 0x6a, 0x85, + 0x1b, 0xe8, 0x51, 0xbf, 0xd5, 0x90, 0x28, 0x42, 0x0e, 0xe3, 0xd2, 0x2f, 0xea, 0x08, 0xe7, 0x02, 0xaf, 0xa7, 0x6e, + 0xeb, 0x7a, 0x48, 0x35, 0x05, 0x71, 0xee, 0xb6, 0x70, 0x46, 0x6f, 0xcd, 0x91, 0xa1, 0x3b, 0xce, 0xf2, 0x42, 0x5d, + 0xdd, 0x1d, 0x98, 0x76, 0x68, 0x68, 0x78, 0x5c, 0xd7, 0xa3, 0xc9, 0x23, 0x11, 0x4d, 0xdc, 0x5a, 0xac, 0xbf, 0x23, + 0xca, 0x3c, 0x0d, 0x60, 0xa7, 0x31, 0xea, 0xbf, 0x4b, 0xf6, 0x68, 0x74, 0xc7, 0x24, 0x91, 0x0d, 0x99, 0x6d, 0x40, + 0x9b, 0x83, 0x23, 0x3d, 0xf5, 0x15, 0x95, 0xdf, 0x4b, 0x14, 0x1c, 0x2f, 0xc5, 0x2d, 0x97, 0xf8, 0xab, 0x78, 0xe8, + 0xe9, 0x24, 0xa6, 0xc1, 0x0d, 0x59, 0x5c, 0x19, 0xe0, 0x32, 0x69, 0x0b, 0x0b, 0x68, 0xd8, 0xc0, 0x02, 0x0a, 0xa3, + 0xcf, 0x61, 0x92, 0x88, 0x7b, 0x38, 0x64, 0xbb, 0xc9, 0x7b, 0x71, 0x4c, 0x14, 0xcf, 0xd5, 0xe4, 0xe8, 0x82, 0x17, + 0xd3, 0x41, 0xd4, 0xec, 0x34, 0xd2, 0xcf, 0x30, 0xbd, 0x97, 0xad, 0xeb, 0xc8, 0x00, 0x61, 0x06, 0x15, 0xea, 0x17, + 0xd2, 0x3e, 0x7b, 0x39, 0x64, 0x40, 0xd1, 0xa0, 0xce, 0x86, 0x1d, 0x62, 0x51, 0xc8, 0x6b, 0x17, 0x4f, 0xb8, 0x96, + 0x78, 0x8f, 0x1e, 0x65, 0x58, 0x5c, 0xe6, 0x63, 0xb4, 0xf3, 0x56, 0x96, 0xa6, 0x0b, 0xcb, 0xf9, 0x5d, 0x8c, 0x16, + 0xe8, 0x70, 0xf5, 0xb8, 0x48, 0xf7, 0x53, 0x7b, 0x5e, 0xf8, 0x9f, 0x43, 0x17, 0x5d, 0xfb, 0x4c, 0x26, 0x75, 0x25, + 0x8f, 0x11, 0xf5, 0x55, 0x2f, 0xac, 0xe2, 0xde, 0x6b, 0xcd, 0xf4, 0x51, 0x8e, 0x32, 0x0f, 0x55, 0x66, 0x0d, 0xc6, + 0xd3, 0x92, 0x0c, 0x1f, 0x1d, 0x01, 0x0e, 0x41, 0x13, 0x82, 0x99, 0xfb, 0x92, 0x18, 0xa3, 0x12, 0x30, 0xee, 0x2c, + 0xb0, 0xbc, 0x9e, 0xdd, 0xd3, 0xd0, 0x16, 0x5a, 0x3e, 0xe5, 0xfc, 0x83, 0x2d, 0x96, 0xf9, 0xa9, 0xb0, 0x59, 0xe2, + 0xe2, 0x8e, 0x85, 0x3c, 0xea, 0x45, 0x55, 0xda, 0x5a, 0xf9, 0x8a, 0x54, 0x76, 0x43, 0x16, 0x5e, 0xd6, 0x2d, 0x2f, + 0x45, 0xe7, 0x55, 0x8c, 0x72, 0x92, 0x63, 0x0c, 0xc5, 0x10, 0xe0, 0xcd, 0x1c, 0x74, 0xf7, 0x02, 0x67, 0x72, 0x03, + 0x99, 0xe9, 0xeb, 0xd8, 0x52, 0x41, 0x1e, 0xec, 0xea, 0x99, 0x85, 0x07, 0x90, 0xc8, 0xf2, 0xf1, 0x9c, 0x8c, 0x2d, + 0xcb, 0x93, 0xef, 0xe5, 0x93, 0x60, 0x06, 0xaf, 0x02, 0x64, 0xd9, 0x79, 0xcd, 0xc1, 0x9f, 0x75, 0x87, 0x73, 0x4b, + 0x6b, 0x83, 0x6a, 0x1f, 0x7a, 0xce, 0x96, 0x0c, 0xbe, 0x12, 0x60, 0x34, 0x13, 0xa8, 0x2c, 0x41, 0x30, 0x4b, 0x8b, + 0xf9, 0x82, 0x60, 0x8e, 0xa3, 0x50, 0xb0, 0x3a, 0xe5, 0xe7, 0x61, 0x53, 0x14, 0x45, 0x3c, 0xfc, 0x3c, 0x0e, 0x95, + 0x67, 0x84, 0x55, 0x7c, 0xad, 0x88, 0xf2, 0xa1, 0xc6, 0x93, 0x81, 0x14, 0x40, 0xff, 0xa6, 0x2b, 0xa2, 0xfd, 0x15, + 0x69, 0x14, 0x14, 0xf6, 0x99, 0xbb, 0xd0, 0xce, 0x1a, 0x71, 0x91, 0x7e, 0x93, 0x61, 0x5e, 0x89, 0x67, 0x7e, 0x65, + 0x5d, 0xd6, 0x3a, 0xdf, 0x83, 0x6a, 0x3f, 0x52, 0xda, 0x59, 0xce, 0x2c, 0x39, 0x40, 0xbb, 0xa6, 0x69, 0x33, 0x9f, + 0x90, 0xb3, 0xb8, 0xda, 0x61, 0x0a, 0x52, 0x81, 0x57, 0x4d, 0x23, 0x95, 0xe2, 0xbc, 0x13, 0x05, 0x1c, 0x2e, 0xa7, + 0xf8, 0xbf, 0x39, 0x51, 0xbb, 0xf9, 0x05, 0x79, 0x6c, 0xef, 0xea, 0x97, 0x83, 0xac, 0x2d, 0x1c, 0x1d, 0x5c, 0xe7, + 0xb8, 0x89, 0x1a, 0xa2, 0x2a, 0x78, 0x6b, 0xc8, 0x97, 0xe6, 0x21, 0x05, 0x96, 0x23, 0x2d, 0x5a, 0x7d, 0x1e, 0xf7, + 0x89, 0x68, 0x9f, 0xba, 0x70, 0x3a, 0x2e, 0x33, 0x36, 0x87, 0xba, 0xc8, 0x8f, 0x49, 0xdb, 0x03, 0x06, 0x96, 0x7a, + 0xa2, 0x8d, 0x0f, 0x5d, 0xc4, 0x6d, 0x77, 0x06, 0xd2, 0xf5, 0x72, 0x1a, 0x4a, 0x66, 0x31, 0x70, 0xe1, 0x68, 0xcc, + 0xe3, 0x06, 0x9d, 0x76, 0xc5, 0x46, 0x64, 0x77, 0x30, 0x5c, 0x89, 0x51, 0xd5, 0x61, 0xec, 0x2e, 0x6a, 0x4e, 0xb0, + 0x52, 0x3d, 0xf6, 0x59, 0x74, 0x40, 0x82, 0x27, 0x14, 0x1c, 0x79, 0xe0, 0x11, 0x3e, 0xab, 0x83, 0x0e, 0x8f, 0x3a, + 0x03, 0xab, 0xea, 0x06, 0xdb, 0xea, 0x30, 0x06, 0xca, 0x11, 0x84, 0x22, 0xf2, 0xdd, 0x82, 0x3a, 0x85, 0xc7, 0xfc, + 0x86, 0x30, 0xa5, 0xf4, 0x7c, 0xce, 0xf6, 0xe2, 0xdb, 0x01, 0xfb, 0xdd, 0x27, 0x5e, 0xd2, 0x35, 0x8c, 0xc3, 0x0f, + 0xff, 0xaa, 0xc5, 0xf2, 0xeb, 0x01, 0xe6, 0xf7, 0x41, 0xaa, 0x4b, 0x58, 0xcb, 0x19, 0xc0, 0x1f, 0x6d, 0x19, 0x77, + 0x0d, 0x86, 0xf5, 0x11, 0x2a, 0x22, 0x3c, 0xe2, 0xa0, 0x7f, 0xaa, 0x05, 0x80, 0xe2, 0x38, 0xad, 0x80, 0xc8, 0x42, + 0x34, 0x3f, 0x2f, 0x67, 0x5f, 0x96, 0x65, 0x68, 0x4b, 0x4b, 0x56, 0x8f, 0x13, 0x69, 0xd8, 0x4c, 0x82, 0x4a, 0x88, + 0x5e, 0x11, 0x31, 0x22, 0x66, 0x86, 0xd6, 0x4b, 0xfb, 0x3d, 0x75, 0x57, 0x10, 0x46, 0xad, 0xdb, 0x70, 0xaf, 0xeb, + 0x51, 0x6f, 0xa4, 0xd9, 0xaf, 0xb5, 0x32, 0x80, 0x7d, 0x4b, 0xbe, 0xc0, 0x91, 0x84, 0x2d, 0xed, 0xf8, 0xef, 0x03, + 0xb1, 0xe8, 0x1f, 0x42, 0xd8, 0xc4, 0x26, 0xc8, 0x19, 0xbc, 0xd4, 0x3a, 0x7b, 0x1b, 0x24, 0xc2, 0x24, 0xd6, 0x6a, + 0x3d, 0x85, 0x24, 0x9a, 0x00, 0x52, 0xa1, 0x7d, 0xc6, 0xf4, 0x8a, 0x54, 0x9c, 0x3f, 0xdf, 0xb5, 0x6c, 0xae, 0x9a, + 0xf2, 0x89, 0x95, 0x23, 0xce, 0xd6, 0x4f, 0x96, 0x24, 0x9b, 0xf0, 0x5d, 0x22, 0xc1, 0x37, 0x16, 0xbb, 0xca, 0xab, + 0x7c, 0x0d, 0x9a, 0x14, 0x02, 0x1d, 0x5c, 0xee, 0x1c, 0x32, 0xd4, 0x62, 0x19, 0xd5, 0xd1, 0x16, 0x8b, 0x4c, 0xef, + 0x77, 0xca, 0xea, 0xb3, 0x08, 0x0d, 0x27, 0x16, 0xc3, 0x28, 0x95, 0x5e, 0x6c, 0xd1, 0xca, 0x9f, 0xf4, 0x7f, 0xc8, + 0x02, 0xa5, 0xea, 0x78, 0x89, 0x5b, 0x35, 0x74, 0x87, 0xae, 0xa8, 0x37, 0xa2, 0xb5, 0x63, 0xff, 0xf2, 0xc6, 0xa4, + 0x8e, 0x35, 0x6d, 0x10, 0xbc, 0x0e, 0xfa, 0x99, 0x29, 0x38, 0xd9, 0x78, 0x15, 0xe9, 0x14, 0x06, 0x04, 0x0a, 0x61, + 0x08, 0xf6, 0x19, 0xc9, 0xa6, 0xa5, 0x74, 0x67, 0x17, 0x27, 0xea, 0xd8, 0x38, 0x33, 0xca, 0xda, 0x45, 0xbc, 0xb4, + 0xf1, 0xd6, 0x13, 0x7a, 0xf1, 0xbd, 0x78, 0xb6, 0xe2, 0xa4, 0xb6, 0x8c, 0x88, 0x17, 0x1c, 0x0f, 0x97, 0x31, 0x87, + 0x6a, 0xe3, 0xd6, 0x82, 0x1e, 0x13, 0x5a, 0x0d, 0x9b, 0x9d, 0xb5, 0x9c, 0xf2, 0xb5, 0x18, 0x17, 0xe5, 0x8b, 0x37, + 0x0b, 0x28, 0x03, 0x42, 0x47, 0x8b, 0x48, 0x02, 0x9f, 0x15, 0x76, 0x63, 0x8e, 0x27, 0xc9, 0x92, 0xf9, 0xb5, 0x92, + 0x47, 0x80, 0x99, 0x18, 0x2e, 0xde, 0x86, 0xac, 0x9e, 0xa0, 0x4b, 0x76, 0xb0, 0x52, 0x37, 0x08, 0xb2, 0x04, 0x3b, + 0xc0, 0x5f, 0x78, 0x3f, 0xc6, 0xde, 0x39, 0xbf, 0xd9, 0x3a, 0xfc, 0x3f, 0xc1, 0x83, 0x79, 0x58, 0xdb, 0xee, 0x17, + 0x1b, 0xf5, 0xe5, 0xff, 0x4f, 0x75, 0x0d, 0xad, 0x03, 0x1f, 0x3e, 0x80, 0xf0, 0x78, 0x79, 0xa8, 0x45, 0xab, 0xad, + 0xbd, 0xc3, 0x90, 0x4c, 0x9c, 0x28, 0x2b, 0x76, 0x54, 0xef, 0x50, 0xb4, 0x9b, 0xf9, 0xb3, 0x23, 0x03, 0xd4, 0x3f, + 0x98, 0x78, 0x1f, 0x34, 0xd2, 0xdd, 0x2f, 0x20, 0x13, 0xeb, 0x51, 0x87, 0x5c, 0xa5, 0xf4, 0xf3, 0x73, 0xf7, 0xd6, + 0x7d, 0x94, 0xae, 0xd2, 0xc1, 0xfd, 0x45, 0x57, 0xed, 0xc1, 0x06, 0x17, 0x3b, 0xc5, 0xad, 0x5a, 0xfb, 0xa4, 0x74, + 0x95, 0x25, 0x3e, 0x04, 0x20, 0xc0, 0x56, 0x99, 0xc9, 0xca, 0x53, 0xbe, 0x85, 0x84, 0x77, 0xad, 0x4f, 0x67, 0x7f, + 0xbd, 0x0e, 0x6f, 0x14, 0x6b, 0xbb, 0x8b, 0x47, 0x6b, 0x07, 0x04, 0xe5, 0xdc, 0x6b, 0x28, 0x27, 0x10, 0xe2, 0x25, + 0x62, 0xae, 0x00, 0x97, 0xc3, 0xc8, 0x78, 0x8a, 0x1c, 0x39, 0x44, 0xb7, 0x11, 0xc1, 0xba, 0x4a, 0x5b, 0x15, 0xc7, + 0x5e, 0xcb, 0x23, 0xb3, 0x85, 0x71, 0x13, 0x11, 0x87, 0x45, 0x05, 0x46, 0x9e, 0x86, 0x1d, 0xce, 0x76, 0x86, 0x5e, + 0xcd, 0x42, 0x16, 0xa4, 0x09, 0xdb, 0xa5, 0x7e, 0x1f, 0x4e, 0x4e, 0x58, 0x7d, 0xd5, 0x42, 0xec, 0x05, 0x70, 0x9a, + 0xbc, 0x35, 0xe4, 0x57, 0x67, 0x7a, 0x46, 0xb8, 0x2c, 0x92, 0x7b, 0x2c, 0x04, 0xa1, 0xb2, 0xb5, 0x5d, 0x26, 0xcb, + 0xd2, 0x31, 0xc4, 0xfb, 0x8c, 0x21, 0xcc, 0xf0, 0x82, 0x40, 0xa6, 0x09, 0x4a, 0x19, 0x7e, 0x0b, 0xf7, 0x5c, 0x60, + 0x6c, 0x90, 0x9b, 0xe9, 0x30, 0x12, 0xae, 0xe8, 0x76, 0x80, 0xc8, 0xd2, 0x7c, 0xa2, 0x58, 0x4d, 0x55, 0x87, 0x7d, + 0x67, 0x12, 0xa2, 0xf6, 0x88, 0xf5, 0x78, 0x4a, 0xb7, 0xdb, 0x49, 0xbe, 0xca, 0x5c, 0x8a, 0x21, 0xa2, 0x4a, 0x47, + 0xee, 0x92, 0x6b, 0xe2, 0x94, 0x58, 0x5a, 0x65, 0x1c, 0x24, 0xb4, 0x63, 0xa1, 0x6d, 0x3c, 0xa5, 0x07, 0x91, 0xb6, + 0x8b, 0x5d, 0x52, 0xa5, 0x93, 0xc7, 0xfc, 0x88, 0x18, 0x32, 0xd3, 0x2f, 0xb0, 0xb6, 0xbf, 0xdc, 0x7c, 0x0a, 0x47, + 0x45, 0x62, 0xe7, 0x8e, 0xc0, 0x1f, 0x03, 0x6c, 0x5e, 0x4a, 0x4b, 0x61, 0x54, 0xa1, 0x73, 0xd5, 0x56, 0x2f, 0x0c, + 0x65, 0x43, 0x88, 0x40, 0x32, 0xcb, 0x12, 0x3e, 0xca, 0x1a, 0x06, 0x39, 0xf5, 0xbd, 0x06, 0x64, 0xdb, 0x83, 0x60, + 0xf9, 0x48, 0x95, 0xa5, 0xbe, 0xbf, 0x7c, 0x36, 0x09, 0x1f, 0xeb, 0x10, 0x66, 0x19, 0x70, 0xcd, 0x7a, 0xef, 0x86, + 0xc6, 0xfd, 0x61, 0x06, 0xf5, 0x2f, 0x5c, 0xe9, 0x1b, 0x7c, 0x8d, 0x3c, 0x16, 0x2e, 0xf5, 0xc8, 0x7b, 0x4b, 0x9e, + 0x6d, 0x53, 0xf2, 0x99, 0x16, 0x2b, 0xde, 0xc0, 0x67, 0x11, 0xef, 0x5a, 0xf1, 0x7d, 0x59, 0xdd, 0xd9, 0x76, 0xe6, + 0x04, 0xd3, 0x0c, 0xf6, 0x60, 0x86, 0xee, 0xfa, 0xa0, 0x95, 0x4a, 0x53, 0x47, 0xfa, 0xf6, 0xc1, 0xc7, 0xad, 0xf7, + 0x7f, 0x21, 0x4d, 0x74, 0x03, 0x84, 0xa2, 0xd2, 0xd7, 0x21, 0xca, 0x0e, 0x69, 0x62, 0xda, 0xa1, 0x4a, 0x14, 0x1d, + 0x3a, 0x65, 0x96, 0xa5, 0x00, 0xc3, 0x37, 0x96, 0x1f, 0x29, 0x5c, 0x2b, 0xc9, 0x0d, 0x84, 0x5a, 0x83, 0xf8, 0x6c, + 0x32, 0xbd, 0x2f, 0xd3, 0x82, 0x02, 0x16, 0x4c, 0xbe, 0x8e, 0x61, 0x17, 0xe9, 0xef, 0xe6, 0x0d, 0x09, 0xce, 0x09, + 0x87, 0x23, 0x1b, 0x08, 0xa0, 0x4c, 0xdb, 0x05, 0x17, 0xf7, 0x1b, 0xca, 0x9f, 0x5b, 0x69, 0xcf, 0x90, 0x5a, 0x70, + 0x18, 0xe8, 0x25, 0xfa, 0xbf, 0xee, 0x0c, 0x1f, 0xca, 0xe3, 0x85, 0x83, 0x39, 0x11, 0x6e, 0x71, 0xf6, 0x95, 0x65, + 0x56, 0xb9, 0xe2, 0xfe, 0xc0, 0xc8, 0x44, 0x6b, 0xd7, 0xd7, 0x07, 0xab, 0x15, 0xb5, 0x0a, 0x35, 0xf4, 0x95, 0xfb, + 0x9f, 0xe9, 0x5e, 0xee, 0x99, 0x31, 0x0f, 0xc5, 0xdc, 0x61, 0x5e, 0x34, 0x34, 0x3e, 0x43, 0x34, 0x44, 0xa9, 0xb1, + 0x1a, 0x70, 0x32, 0x26, 0xf5, 0xf1, 0xa0, 0xc3, 0x52, 0x3a, 0x27, 0x46, 0x95, 0x5a, 0x64, 0x90, 0x60, 0x72, 0x3c, + 0x97, 0x36, 0x87, 0x02, 0x11, 0x34, 0xf3, 0x1a, 0x1a, 0xfd, 0x28, 0x87, 0x15, 0x6e, 0x2c, 0xcb, 0x25, 0x86, 0x8c, + 0x20, 0xa8, 0x2c, 0x1b, 0x37, 0x75, 0x93, 0xa0, 0x28, 0x9c, 0xfa, 0xb1, 0x41, 0x41, 0xf1, 0xdb, 0x99, 0x2f, 0x4d, + 0x76, 0xdc, 0x3d, 0x1a, 0xc0, 0xa2, 0x58, 0x97, 0x78, 0xd9, 0xc5, 0x44, 0x6e, 0x72, 0x83, 0x55, 0x46, 0x20, 0xe6, + 0xf0, 0x27, 0xa8, 0x92, 0x22, 0xa6, 0x8b, 0xb8, 0xb9, 0x34, 0x17, 0x47, 0x32, 0xb5, 0xab, 0x07, 0x6e, 0x43, 0xa3, + 0x5a, 0x4d, 0xf4, 0xda, 0x32, 0x3f, 0x91, 0x88, 0x4e, 0x58, 0x3c, 0x91, 0x57, 0x4c, 0x44, 0x12, 0x0c, 0x0c, 0x28, + 0xda, 0x16, 0x42, 0x51, 0xe8, 0x35, 0x9f, 0xae, 0x96, 0xf3, 0x73, 0xb9, 0x05, 0x49, 0xa1, 0xd1, 0xef, 0x13, 0x48, + 0xf5, 0xd3, 0xa6, 0x3f, 0x61, 0xf1, 0x3f, 0x89, 0x09, 0xb7, 0x3d, 0xf4, 0x0c, 0xc4, 0xa7, 0x1e, 0xe0, 0xd3, 0x53, + 0x07, 0x0a, 0xd3, 0xcb, 0x17, 0xc1, 0x83, 0x22, 0xea, 0xc6, 0x9c, 0x58, 0xf2, 0x18, 0x4a, 0x7c, 0x5f, 0x95, 0x4f, + 0x31, 0xa3, 0xda, 0x4a, 0xe1, 0x9e, 0x04, 0x8a, 0x26, 0xae, 0x64, 0xf3, 0x39, 0x65, 0x5c, 0x86, 0xe2, 0xe3, 0x84, + 0xf3, 0x86, 0xe5, 0x52, 0x16, 0x4a, 0x5e, 0xe1, 0xfd, 0x60, 0x0e, 0x21, 0xcb, 0x15, 0xa9, 0x21, 0xbf, 0x2a, 0x61, + 0x7f, 0x0f, 0xa4, 0x71, 0x05, 0x63, 0xb6, 0xf6, 0x0a, 0xeb, 0xc7, 0x62, 0xa5, 0x1f, 0x90, 0x6b, 0xc4, 0x3d, 0x1c, + 0x32, 0x00, 0xc3, 0x7e, 0x77, 0x44, 0xcd, 0x48, 0x85, 0x0b, 0x73, 0xf7, 0x92, 0x40, 0xc2, 0x36, 0x08, 0x9b, 0xed, + 0x8b, 0x79, 0xf8, 0xf8, 0x57, 0x6b, 0xce, 0x0e, 0xd6, 0x4a, 0xb8, 0x74, 0x74, 0x95, 0x09, 0xf2, 0xf2, 0x31, 0x12, + 0x67, 0x6e, 0xa7, 0xa9, 0x65, 0x41, 0x54, 0x5a, 0x8c, 0x67, 0x2b, 0x71, 0xb3, 0x4c, 0xe1, 0xb1, 0xc7, 0x04, 0xed, + 0xcc, 0x4b, 0x70, 0x09, 0x88, 0x3e, 0xc8, 0xf8, 0xca, 0x3a, 0x89, 0x5e, 0x79, 0x36, 0xfe, 0x2c, 0xbb, 0xf7, 0xa8, + 0xff, 0xaa, 0x48, 0xed, 0x7a, 0xd6, 0xdd, 0xa1, 0x24, 0x15, 0x4c, 0xbb, 0x1b, 0xf0, 0x71, 0xd2, 0x4f, 0x4c, 0xbe, + 0x51, 0x10, 0x37, 0xc0, 0xd9, 0x77, 0xe3, 0x40, 0xb7, 0x80, 0xf5, 0xe6, 0x83, 0x44, 0x03, 0x57, 0x23, 0xd2, 0xb9, + 0x59, 0xaf, 0xaf, 0x4d, 0x0b, 0x05, 0x20, 0x05, 0xb3, 0x92, 0x90, 0xbc, 0x2b, 0x17, 0x6d, 0x7d, 0x22, 0xb6, 0x00, + 0x62, 0xba, 0x81, 0xc4, 0x71, 0x44, 0xb9, 0xc6, 0xa3, 0x6f, 0x96, 0x1e, 0x3d, 0xeb, 0x88, 0xdd, 0x3f, 0x85, 0xd6, + 0xf4, 0xb2, 0x83, 0xed, 0x9c, 0x22, 0xa8, 0x50, 0x86, 0x8e, 0xea, 0xd9, 0x0d, 0x9b, 0x5b, 0xc7, 0xb2, 0xd0, 0xa3, + 0x87, 0x20, 0x96, 0xcc, 0x7b, 0xdb, 0x08, 0x8d, 0x10, 0xdf, 0xfd, 0x42, 0xc0, 0x38, 0x5a, 0xff, 0x42, 0xab, 0x6c, + 0xa8, 0xe3, 0xd4, 0xc6, 0x83, 0x8f, 0x9b, 0x55, 0x61, 0xe5, 0x92, 0xf9, 0xdc, 0xbb, 0x63, 0x8a, 0x7a, 0x2a, 0xdf, + 0x7a, 0x2d, 0x7b, 0x32, 0x3a, 0x6a, 0x68, 0x8f, 0x7c, 0xd2, 0xd6, 0xb7, 0x86, 0xad, 0x48, 0x1a, 0xc9, 0xa4, 0xb9, + 0xf3, 0xc1, 0x09, 0xb5, 0x79, 0xd8, 0x21, 0x71, 0xc2, 0xdc, 0xfa, 0xdd, 0x3c, 0x92, 0xb2, 0x78, 0x04, 0x5b, 0xf8, + 0x66, 0x68, 0xd3, 0x30, 0x26, 0x1d, 0x27, 0xe0, 0xba, 0xd2, 0x3f, 0xcd, 0xa0, 0xc4, 0x6a, 0x61, 0x61, 0x3c, 0x03, + 0x98, 0x8a, 0x29, 0xe2, 0xa5, 0x0a, 0x86, 0x1a, 0x24, 0xe7, 0x6a, 0x10, 0xcc, 0x74, 0xcc, 0xd8, 0x99, 0x97, 0x79, + 0x0f, 0x6d, 0x6d, 0xcc, 0xc2, 0x42, 0xcf, 0xc6, 0xd4, 0x3c, 0xaa, 0x14, 0x30, 0x35, 0x82, 0x6e, 0x87, 0x71, 0x71, + 0xb7, 0x47, 0x7e, 0x5a, 0x8e, 0x9c, 0x5d, 0x0c, 0x8e, 0xc7, 0x5e, 0x66, 0x8b, 0x53, 0x0f, 0x9e, 0x07, 0x98, 0x11, + 0x2a, 0x6c, 0x15, 0x2f, 0xd0, 0x9e, 0x35, 0xfd, 0x07, 0xbe, 0x89, 0x8d, 0x31, 0x98, 0x37, 0xc6, 0xd1, 0x9a, 0xa5, + 0x2b, 0xde, 0xd3, 0x30, 0x42, 0x16, 0x31, 0x22, 0xcb, 0x59, 0x53, 0xcc, 0xad, 0x54, 0x31, 0x9e, 0x41, 0x22, 0x58, + 0xbe, 0xc2, 0x54, 0x00, 0xe1, 0x60, 0x76, 0xa3, 0xc1, 0x6e, 0xd6, 0xc7, 0xb5, 0x7e, 0x04, 0x44, 0x60, 0x00, 0xd5, + 0xc5, 0x39, 0xd7, 0x26, 0x3a, 0x00, 0x96, 0xdf, 0x47, 0x00, 0x20, 0x09, 0xcc, 0x50, 0x24, 0xa0, 0xe8, 0x55, 0x4b, + 0x5f, 0xf3, 0x62, 0x0e, 0x9d, 0x1e, 0x0a, 0x82, 0x60, 0x2b, 0xf7, 0xe8, 0x34, 0x48, 0xb3, 0xb9, 0x41, 0x1f, 0xf1, + 0xed, 0x59, 0x51, 0x89, 0x83, 0xcb, 0xaf, 0x8a, 0xa0, 0xf8, 0x27, 0x43, 0xf6, 0x26, 0x63, 0xa6, 0x23, 0xde, 0xea, + 0xc8, 0xa3, 0x85, 0x7c, 0x31, 0x4e, 0x17, 0x9f, 0xa1, 0xd8, 0x43, 0x36, 0x28, 0xab, 0x64, 0xec, 0xc4, 0x93, 0xa1, + 0x11, 0x49, 0xfd, 0xe3, 0x30, 0xf7, 0x45, 0x3d, 0x8a, 0xd2, 0x3c, 0xad, 0x27, 0xd4, 0x8a, 0xa9, 0x76, 0x23, 0xb0, + 0x26, 0xe5, 0x99, 0xd0, 0x19, 0x5b, 0xea, 0x97, 0x0a, 0x52, 0x76, 0x6a, 0x4c, 0xc5, 0x4e, 0xce, 0x8b, 0x9c, 0xa3, + 0xa7, 0x3c, 0x08, 0xe3, 0xc0, 0xd8, 0x9f, 0x4e, 0x97, 0xd5, 0xee, 0xd9, 0x09, 0xe2, 0xf1, 0x6a, 0xa8, 0xf6, 0x21, + 0x5d, 0xab, 0x26, 0xa6, 0x40, 0xd3, 0x9e, 0xa6, 0xff, 0x25, 0x81, 0x3e, 0x0f, 0xc1, 0x9e, 0xe9, 0xb3, 0x91, 0x6a, + 0x07, 0xd1, 0xfe, 0xa0, 0x85, 0x77, 0xf8, 0x1a, 0x25, 0x54, 0xbf, 0xe7, 0x04, 0xe8, 0xf8, 0x06, 0x6b, 0xc4, 0x96, + 0x24, 0xce, 0xe7, 0x22, 0x95, 0x9d, 0x63, 0x46, 0x2d, 0x20, 0x17, 0x44, 0x81, 0xe7, 0x3a, 0x8d, 0xca, 0x42, 0x96, + 0xbc, 0xc1, 0x8d, 0x9f, 0xfd, 0x9a, 0x29, 0x14, 0xfe, 0x69, 0x38, 0x08, 0x58, 0x06, 0xb0, 0x30, 0x9f, 0x5e, 0x61, + 0xce, 0x99, 0x9d, 0x25, 0x0c, 0x59, 0x80, 0x96, 0x3a, 0x7a, 0x0b, 0x9d, 0x04, 0x00, 0x44, 0x47, 0xc5, 0x18, 0xc8, + 0xab, 0x1d, 0x55, 0x9f, 0xc0, 0xa1, 0x77, 0xd2, 0x73, 0x69, 0xee, 0x26, 0x10, 0x45, 0x08, 0x08, 0x90, 0xd8, 0x1a, + 0x0a, 0x22, 0x6f, 0x39, 0x88, 0xa8, 0x4a, 0xec, 0x04, 0xb7, 0x42, 0xb3, 0xe0, 0x46, 0x32, 0x22, 0x8d, 0x00, 0x7a, + 0x05, 0x08, 0x31, 0x23, 0x50, 0xe6, 0x3c, 0xd2, 0xf8, 0x05, 0x1e, 0x26, 0x2f, 0x44, 0xc1, 0xe7, 0x14, 0xb5, 0xde, + 0x83, 0xe8, 0x9e, 0x9b, 0xb3, 0xf6, 0xc7, 0x84, 0x10, 0x3d, 0x02, 0x6b, 0x28, 0xab, 0x7f, 0x45, 0x29, 0x60, 0x34, + 0xc0, 0xd9, 0xde, 0xe1, 0xdc, 0x63, 0xfe, 0x51, 0xf2, 0xa0, 0x0a, 0x1d, 0xf3, 0x88, 0x5c, 0x3a, 0x9f, 0x74, 0xab, + 0xb0, 0x5e, 0xd4, 0x0e, 0x6c, 0xb7, 0x1e, 0x8f, 0xd5, 0x4b, 0x75, 0xad, 0x41, 0x1a, 0x8a, 0xff, 0xa2, 0xfc, 0x68, + 0x0c, 0x95, 0xf3, 0x8b, 0xf1, 0xa0, 0x7b, 0xd1, 0x61, 0xbd, 0x8b, 0x5c, 0x40, 0x45, 0x09, 0x00, 0xb4, 0xdb, 0xa1, + 0x9d, 0x33, 0x9b, 0x7f, 0xbb, 0xfd, 0x85, 0xaf, 0x2c, 0x55, 0x8b, 0x3a, 0xcf, 0x1a, 0x0a, 0xce, 0xcb, 0x71, 0xfe, + 0x2f, 0x3c, 0xd8, 0xcb, 0x93, 0xce, 0x98, 0x2a, 0x42, 0x9c, 0xba, 0x33, 0xfb, 0x26, 0x1f, 0x87, 0x2d, 0x21, 0x76, + 0xaa, 0x9b, 0xbf, 0xd9, 0xcc, 0x83, 0xa9, 0xaf, 0x76, 0x80, 0x1b, 0x37, 0xb7, 0xcc, 0xd8, 0xab, 0xc7, 0xd0, 0x31, + 0x01, 0xe0, 0xad, 0x25, 0x8a, 0x22, 0xe2, 0x25, 0xe1, 0xdf, 0x1f, 0x8f, 0x0f, 0x55, 0xc3, 0x07, 0x7d, 0x1b, 0xef, + 0x44, 0xa1, 0x29, 0x30, 0xc1, 0x3a, 0x60, 0x98, 0x0f, 0xe8, 0x7b, 0x85, 0xcd, 0x8c, 0x1a, 0xdf, 0x76, 0xba, 0x28, + 0x40, 0x4c, 0x61, 0x70, 0xa5, 0xf1, 0x49, 0x5e, 0x64, 0x3c, 0xa8, 0x02, 0x6d, 0xde, 0x26, 0xfb, 0xaa, 0x30, 0x34, + 0x3c, 0xed, 0xd6, 0x43, 0x8f, 0x1d, 0x34, 0x8b, 0x5b, 0xc3, 0xf8, 0x85, 0x74, 0x90, 0xbf, 0xb1, 0xc9, 0x2c, 0x51, + 0xfc, 0xfe, 0x47, 0xe7, 0x24, 0xf7, 0x7c, 0xd0, 0x4e, 0x8a, 0x9a, 0x0a, 0x9d, 0x3f, 0x2b, 0x1f, 0x97, 0xf3, 0xb3, + 0xf0, 0xee, 0x2c, 0xd4, 0x1d, 0x59, 0x0a, 0x12, 0x39, 0x0d, 0x4d, 0xae, 0xd5, 0x62, 0xcd, 0x89, 0x8b, 0xb7, 0xb6, + 0xc5, 0x27, 0x70, 0xb3, 0xe4, 0x0c, 0x61, 0x2a, 0xde, 0xc4, 0x84, 0xe0, 0x30, 0x10, 0x14, 0x86, 0x8b, 0xe2, 0x10, + 0x09, 0x83, 0x37, 0x3b, 0x3c, 0xb1, 0x5b, 0x06, 0x1b, 0x5f, 0xcd, 0x1b, 0x65, 0x9e, 0xb1, 0x9e, 0x98, 0x81, 0x6a, + 0x16, 0x55, 0xd7, 0x8b, 0x01, 0x56, 0xff, 0x84, 0xd7, 0xd2, 0x89, 0xd9, 0x7a, 0x90, 0x25, 0xa9, 0x61, 0x53, 0x2e, + 0x51, 0x4d, 0x19, 0xdb, 0x58, 0x43, 0xc1, 0xb5, 0xc3, 0x23, 0xfd, 0xe1, 0xfa, 0x4f, 0xce, 0x67, 0x89, 0x67, 0xa1, + 0xe7, 0x2b, 0x87, 0xc0, 0x5a, 0xec, 0xb2, 0x76, 0x7d, 0xe8, 0x6b, 0x36, 0x47, 0x61, 0x1b, 0x0d, 0xa5, 0x74, 0x16, + 0x2f, 0x88, 0xae, 0x83, 0x32, 0x90, 0x2e, 0x1d, 0x26, 0x3a, 0x7b, 0x5f, 0x35, 0xeb, 0x0e, 0x34, 0xde, 0xf4, 0x88, + 0x44, 0x1b, 0xbb, 0x6a, 0x30, 0xaf, 0xe8, 0x9c, 0xa2, 0x9b, 0x63, 0x4b, 0xa0, 0xbf, 0xda, 0x1c, 0x6e, 0x4c, 0x5f, + 0x02, 0x31, 0xa5, 0x80, 0x7c, 0xcb, 0xa6, 0xe6, 0x9e, 0xf3, 0x40, 0x3e, 0x61, 0x2a, 0x34, 0x64, 0xed, 0x3a, 0xec, + 0xc6, 0x1a, 0x2f, 0x39, 0x22, 0xf5, 0xcf, 0xb5, 0x08, 0x0b, 0xaf, 0x2e, 0x58, 0xb6, 0xc5, 0x47, 0x27, 0xac, 0x49, + 0xd2, 0xb6, 0x87, 0x05, 0xb4, 0xd8, 0x61, 0x51, 0x9e, 0x5a, 0xcf, 0x25, 0x2e, 0x66, 0x62, 0x7c, 0x4d, 0x97, 0x2e, + 0x39, 0xb0, 0xec, 0x1c, 0x01, 0x8d, 0x07, 0x2b, 0xbd, 0x15, 0xbe, 0x55, 0x74, 0xbf, 0x6a, 0x46, 0x25, 0xce, 0x34, + 0x90, 0xd6, 0x0b, 0x58, 0x23, 0xd4, 0xb5, 0xfc, 0xc0, 0x19, 0xc7, 0x02, 0x6c, 0xcb, 0xf4, 0xfe, 0x76, 0x29, 0x2d, + 0xc4, 0x0e, 0x01, 0x9e, 0x71, 0x17, 0xfd, 0x03, 0xcd, 0x0a, 0x60, 0x4c, 0x4e, 0x4d, 0xc8, 0xc5, 0x7b, 0xdd, 0x10, + 0x32, 0xa6, 0x7f, 0xd2, 0x3e, 0xb6, 0x6c, 0x47, 0x87, 0x04, 0x1c, 0x19, 0x06, 0xc6, 0xad, 0x57, 0x29, 0x6b, 0x77, + 0x33, 0x1c, 0x23, 0xaa, 0xa5, 0x15, 0xf7, 0xcb, 0x44, 0x81, 0x67, 0xc0, 0x6e, 0x5c, 0x34, 0xed, 0xb5, 0x41, 0x2e, + 0x91, 0x9d, 0xc1, 0xab, 0x53, 0x45, 0x66, 0x61, 0x8c, 0x5d, 0x25, 0x0b, 0x3c, 0x3e, 0xf6, 0x84, 0x31, 0xfe, 0x27, + 0x29, 0x41, 0xf9, 0xfe, 0xbb, 0xa4, 0x93, 0x0a, 0x95, 0xc2, 0x1e, 0x4e, 0xaf, 0xe3, 0x2b, 0xfa, 0x2a, 0x11, 0x58, + 0xf3, 0xa8, 0x7e, 0xdc, 0x00, 0x83, 0xaa, 0x0d, 0x78, 0x74, 0x43, 0x29, 0xde, 0x54, 0xf8, 0x26, 0x77, 0xa1, 0x55, + 0x51, 0x8e, 0xca, 0x01, 0x6b, 0x8e, 0xdc, 0x1c, 0x59, 0x22, 0xd8, 0xb2, 0x76, 0x90, 0xa2, 0x02, 0xc3, 0x9e, 0x55, + 0x83, 0xb4, 0x2a, 0x3d, 0x1c, 0x19, 0x7f, 0x4d, 0x80, 0x16, 0x40, 0x18, 0x96, 0x3f, 0x33, 0x93, 0x8c, 0x97, 0x29, + 0x2b, 0xb9, 0xa9, 0xe6, 0x28, 0x9a, 0x98, 0x86, 0x4e, 0xee, 0xe9, 0x84, 0x1f, 0x6a, 0x8e, 0x38, 0x1b, 0x04, 0xb5, + 0x55, 0xd5, 0x3a, 0x83, 0x61, 0x50, 0x27, 0x1d, 0x01, 0xf2, 0x51, 0xd2, 0x60, 0xc2, 0x73, 0x73, 0x8e, 0x9e, 0xc7, + 0x79, 0x19, 0x96, 0x93, 0x76, 0x36, 0x4b, 0x00, 0x3e, 0xb5, 0x14, 0xb6, 0x90, 0x81, 0x31, 0x8c, 0x3f, 0x02, 0x72, + 0xc7, 0xa7, 0xcf, 0x4b, 0xcb, 0x1e, 0x95, 0x5e, 0xde, 0xfc, 0xf0, 0xf1, 0x07, 0x83, 0x37, 0x18, 0x2a, 0x1a, 0xbc, + 0x7b, 0xaf, 0x2f, 0xe9, 0x3b, 0x99, 0x60, 0xac, 0x41, 0xe7, 0x20, 0x8a, 0x55, 0x68, 0x47, 0xb6, 0x2a, 0xeb, 0x22, + 0x27, 0xdb, 0xd7, 0x27, 0xe5, 0xe7, 0x97, 0x22, 0x94, 0x6a, 0x41, 0x21, 0x6f, 0xb1, 0x8a, 0x0d, 0x42, 0x28, 0x54, + 0xe0, 0xa0, 0x08, 0x01, 0x8e, 0x22, 0xee, 0xee, 0x34, 0x14, 0x00, 0x52, 0x52, 0x14, 0xcc, 0xa9, 0xcb, 0xda, 0xdb, + 0x5c, 0x60, 0xb3, 0x73, 0xa6, 0xee, 0x23, 0x3e, 0xc7, 0x84, 0xd5, 0x39, 0x47, 0x8a, 0x04, 0xb2, 0xb6, 0xec, 0xd6, + 0x22, 0x4b, 0x75, 0x77, 0x34, 0x64, 0xc8, 0xac, 0x20, 0xe7, 0x5e, 0x3e, 0x2b, 0x10, 0x5a, 0x41, 0xfe, 0x93, 0x26, + 0x36, 0x60, 0x8c, 0x63, 0xfb, 0xc7, 0xef, 0x54, 0xf0, 0x37, 0x5f, 0xc3, 0x3d, 0xf9, 0x6d, 0x3a, 0xc1, 0x2a, 0xc5, + 0x60, 0x50, 0xf3, 0x2b, 0xe7, 0x4c, 0xaf, 0xcd, 0x18, 0x88, 0x89, 0x63, 0x56, 0xbe, 0x87, 0x57, 0xe9, 0x8b, 0x52, + 0xb4, 0x19, 0x54, 0xa4, 0x4c, 0x2a, 0x80, 0x84, 0x26, 0xed, 0x21, 0xf5, 0x1a, 0x4c, 0xca, 0xb2, 0x29, 0xb6, 0x69, + 0xae, 0xd4, 0xf6, 0xb1, 0xa3, 0xa6, 0xd6, 0x83, 0x32, 0x89, 0x87, 0x38, 0x7d, 0x16, 0x78, 0x1c, 0x63, 0x42, 0x88, + 0x14, 0x12, 0x7f, 0x71, 0xa6, 0xd5, 0xe3, 0x2b, 0x2a, 0xee, 0xb9, 0x8f, 0xa0, 0x63, 0x0c, 0x8d, 0xe9, 0x54, 0xb0, + 0x1b, 0xd2, 0x19, 0x12, 0x7b, 0x9d, 0x1b, 0x99, 0xee, 0xd6, 0xab, 0x0e, 0x1f, 0x8c, 0xcc, 0x4f, 0x79, 0xc7, 0xae, + 0xf7, 0x46, 0x06, 0x6b, 0x9d, 0xd2, 0xd3, 0x9a, 0xf2, 0xf4, 0x7f, 0xc3, 0x15, 0xee, 0xa8, 0x2e, 0x2d, 0x12, 0x5d, + 0x9e, 0x21, 0xc1, 0xb8, 0x48, 0x8a, 0xb4, 0xde, 0x25, 0x4c, 0x36, 0xbd, 0x62, 0xed, 0x9a, 0xd1, 0x65, 0x61, 0x7e, + 0xc8, 0xe6, 0x17, 0x5d, 0x8b, 0xf1, 0x0e, 0xac, 0xb3, 0xaf, 0xf2, 0xcc, 0x39, 0x46, 0x9e, 0xc1, 0x8c, 0x85, 0xbd, + 0x2a, 0xa8, 0x43, 0x5a, 0x58, 0x07, 0xa8, 0x1e, 0xa3, 0x28, 0xe3, 0xd1, 0x4b, 0x9b, 0x42, 0x7a, 0xa0, 0xdb, 0xee, + 0x95, 0x5f, 0x5e, 0x45, 0x85, 0x02, 0x20, 0x2e, 0x44, 0x58, 0x78, 0x34, 0x83, 0xc1, 0x05, 0x0a, 0x85, 0xb7, 0x39, + 0xe8, 0xc5, 0x35, 0x9c, 0xb7, 0x1f, 0xa4, 0xd4, 0x70, 0x8a, 0x29, 0x1d, 0x27, 0x5f, 0x70, 0x67, 0xbd, 0xac, 0x40, + 0x7e, 0x38, 0xb3, 0x16, 0xbb, 0x66, 0x97, 0x42, 0x36, 0xa4, 0xe8, 0xaa, 0xdd, 0xed, 0x9d, 0xb2, 0xb6, 0x67, 0xe6, + 0xc3, 0xb2, 0xa6, 0x68, 0x56, 0x12, 0x85, 0x9e, 0x43, 0x14, 0x43, 0xc5, 0xd0, 0xcc, 0xb5, 0x65, 0x5d, 0xd4, 0x52, + 0x0d, 0x95, 0xba, 0x46, 0x50, 0xd5, 0xcd, 0x51, 0xfd, 0x73, 0xd6, 0xe3, 0xdc, 0xb5, 0xc1, 0xd0, 0x7a, 0xf2, 0x30, + 0x5e, 0xc6, 0xea, 0x1c, 0x1f, 0x2f, 0x7c, 0x8e, 0x73, 0xdb, 0xbe, 0x57, 0xf7, 0x3b, 0x05, 0x6d, 0x59, 0x7c, 0x13, + 0xff, 0x83, 0xea, 0xff, 0xb2, 0x01, 0x23, 0x93, 0x8f, 0x0f, 0xcb, 0x99, 0xd6, 0x17, 0x59, 0x4c, 0x76, 0xe4, 0xb1, + 0x33, 0x4d, 0x9e, 0xb1, 0xb0, 0x57, 0x77, 0x6f, 0x23, 0x67, 0xc1, 0x61, 0x73, 0xe6, 0x10, 0x06, 0xb2, 0x32, 0xfe, + 0xb0, 0x65, 0xb4, 0x6e, 0x9d, 0x36, 0x75, 0xf8, 0x30, 0x34, 0x31, 0xd9, 0x6b, 0x3c, 0xc5, 0x10, 0xe6, 0xd9, 0x94, + 0xb1, 0x2d, 0xe0, 0x45, 0x65, 0x28, 0xe2, 0x32, 0xae, 0x39, 0x82, 0x29, 0xad, 0x06, 0xf6, 0x59, 0x45, 0xf1, 0x1c, + 0x55, 0xba, 0xa8, 0x9e, 0xdb, 0x37, 0x3d, 0x60, 0x48, 0x46, 0xce, 0x7e, 0xb9, 0xfa, 0x18, 0x1a, 0x58, 0xb7, 0xa3, + 0xaf, 0x06, 0x3c, 0x43, 0x24, 0xfa, 0xbc, 0x33, 0x36, 0x20, 0xb6, 0x58, 0x99, 0xe5, 0x50, 0x48, 0xfe, 0x71, 0x3b, + 0x5c, 0xc6, 0xea, 0x53, 0x7e, 0xa4, 0x2f, 0x59, 0xec, 0x86, 0xa6, 0xd6, 0xc1, 0x5f, 0xa9, 0x0a, 0x22, 0xe5, 0x5d, + 0x4b, 0x75, 0x97, 0x21, 0x6d, 0x4a, 0x3d, 0xfa, 0x7b, 0xa0, 0x2c, 0x8d, 0x58, 0x89, 0xa5, 0x51, 0x35, 0x26, 0xfe, + 0xef, 0xf4, 0x29, 0x3a, 0x23, 0x3f, 0xb5, 0xb0, 0xe2, 0xbe, 0x22, 0x16, 0x2e, 0xe1, 0x98, 0xe9, 0xd5, 0x16, 0x1d, + 0x15, 0x22, 0x28, 0xe0, 0xb3, 0x45, 0xef, 0xcd, 0x86, 0x4c, 0x04, 0x8d, 0xb7, 0x79, 0x7a, 0x1d, 0x4f, 0xf7, 0xf3, + 0x19, 0xd9, 0x11, 0x9a, 0x2e, 0xac, 0x4d, 0x41, 0xe1, 0x20, 0x70, 0x6e, 0x21, 0xd0, 0x5c, 0x95, 0x81, 0x09, 0x8e, + 0xf3, 0x62, 0xcb, 0x27, 0x50, 0x9d, 0xee, 0x81, 0x34, 0xa8, 0x5a, 0x9e, 0x6a, 0x95, 0xba, 0x8f, 0xe9, 0xb4, 0xd5, + 0x3a, 0x6b, 0x83, 0x52, 0xfc, 0x00, 0xbb, 0xa0, 0x80, 0x56, 0x2f, 0x51, 0x82, 0xb8, 0x39, 0x34, 0x5f, 0xca, 0x5e, + 0x33, 0xe7, 0x68, 0xef, 0xd0, 0x92, 0x71, 0x41, 0xfb, 0xfb, 0xfb, 0x03, 0x21, 0x73, 0x14, 0xad, 0x83, 0xa6, 0x64, + 0x2e, 0xf7, 0x88, 0xab, 0x48, 0xe5, 0x9f, 0x17, 0x6c, 0xa8, 0xe0, 0xe5, 0xf6, 0x77, 0xa8, 0x1f, 0x16, 0x75, 0xd1, + 0x7e, 0x0b, 0xf1, 0x1a, 0xf9, 0x47, 0xf0, 0xfe, 0x28, 0x20, 0x1a, 0x7e, 0x9a, 0xf0, 0x3b, 0x68, 0xb3, 0x57, 0xf7, + 0x0b, 0xdf, 0xf7, 0x7d, 0x8b, 0xdd, 0xe0, 0xad, 0xef, 0x9f, 0x3a, 0x58, 0x85, 0xc3, 0x1e, 0xb8, 0x9e, 0x18, 0xdd, + 0xfe, 0xfc, 0xfc, 0xbe, 0x86, 0x8a, 0x2f, 0xce, 0xb0, 0x9b, 0xa9, 0x7c, 0xa0, 0xee, 0x9d, 0xdc, 0xd2, 0x7e, 0xa1, + 0xe6, 0x35, 0x04, 0xa4, 0x5c, 0x38, 0x27, 0xae, 0x4f, 0x0a, 0x5c, 0x81, 0x16, 0x52, 0x3a, 0xba, 0x2d, 0xf1, 0x9e, + 0x35, 0xa4, 0xfd, 0xb0, 0x01, 0x36, 0x9d, 0xf6, 0x1d, 0x52, 0x71, 0x98, 0xc9, 0xd2, 0x6c, 0x42, 0xfe, 0x6b, 0x8e, + 0x3a, 0x55, 0x07, 0xf7, 0x79, 0xb1, 0x2e, 0x0c, 0xeb, 0x6e, 0x3c, 0xce, 0x9f, 0xaa, 0x3d, 0x61, 0xc4, 0x0d, 0x63, + 0x75, 0xc8, 0x6f, 0x90, 0x06, 0xf4, 0x76, 0x34, 0x93, 0x22, 0xfb, 0x81, 0x00, 0x80, 0xaf, 0xd6, 0x8c, 0xa5, 0x41, + 0xd9, 0x37, 0xfd, 0x1c, 0x2a, 0x34, 0x41, 0x8c, 0xca, 0x5e, 0x03, 0x24, 0xe0, 0x22, 0x5b, 0x97, 0xc5, 0x7b, 0xa1, + 0x22, 0xa1, 0x5b, 0x97, 0xd0, 0xa9, 0xde, 0xc9, 0x10, 0x56, 0x5d, 0x22, 0xc2, 0x9c, 0xf6, 0x84, 0xaf, 0xeb, 0x7c, + 0xf8, 0x3c, 0x16, 0x7b, 0xce, 0xd3, 0xcf, 0xb0, 0xb9, 0x30, 0x0d, 0x0d, 0x44, 0x33, 0x0e, 0xdd, 0x8f, 0xd4, 0x96, + 0xe2, 0xd6, 0xac, 0x62, 0x3c, 0xfe, 0x72, 0x5e, 0x55, 0x64, 0xfd, 0xe5, 0x22, 0xc3, 0x14, 0xe1, 0x66, 0x16, 0xf5, + 0xf2, 0xa2, 0x10, 0x66, 0xa7, 0x8b, 0x06, 0x82, 0x66, 0xb4, 0x6d, 0x3d, 0xb8, 0xa1, 0xb4, 0x11, 0xfa, 0x45, 0x95, + 0x68, 0x6d, 0xd5, 0xf7, 0xfd, 0x06, 0xd9, 0xe5, 0x1c, 0x07, 0x6d, 0x5e, 0xc0, 0xf1, 0xbd, 0x7f, 0xea, 0x97, 0xab, + 0xbd, 0x75, 0x9a, 0xbf, 0xe0, 0x16, 0x5f, 0x90, 0xb0, 0xfc, 0x30, 0xc3, 0x41, 0x29, 0x21, 0xc3, 0xc9, 0x47, 0x38, + 0x17, 0xd6, 0xe8, 0x92, 0xcf, 0xf6, 0x5c, 0x18, 0xe8, 0x60, 0x45, 0xb4, 0x23, 0xbe, 0xe1, 0xa7, 0xba, 0x2d, 0x44, + 0x10, 0x3b, 0x58, 0xc6, 0x80, 0x67, 0x64, 0x72, 0x22, 0xa3, 0x3a, 0x4c, 0x60, 0x9a, 0x4d, 0x98, 0x06, 0x76, 0x9b, + 0x00, 0x9a, 0x3a, 0x18, 0xa7, 0x38, 0x03, 0x7d, 0x18, 0xaa, 0xad, 0x67, 0x25, 0x19, 0xf3, 0x81, 0xa0, 0x9d, 0xed, + 0x8f, 0x1a, 0x65, 0x5e, 0x6c, 0x37, 0xdb, 0x48, 0xf3, 0xaa, 0x14, 0x43, 0x3b, 0x90, 0xd9, 0x91, 0x34, 0x64, 0xea, + 0x1e, 0xd4, 0xb8, 0x50, 0xa8, 0x36, 0x0c, 0xc2, 0x01, 0x4a, 0x91, 0xa6, 0x39, 0xf5, 0x08, 0xb3, 0xe8, 0xd6, 0x14, + 0xde, 0x59, 0x66, 0xb8, 0x5a, 0x22, 0xa0, 0x04, 0x11, 0xc7, 0x5d, 0x74, 0x18, 0xc5, 0x83, 0xbd, 0x51, 0x77, 0x4a, + 0xa8, 0xaf, 0x5c, 0x2c, 0xd6, 0xa3, 0xad, 0x16, 0x7b, 0x82, 0x69, 0x5a, 0xd7, 0xfb, 0x81, 0x18, 0xed, 0xf9, 0x66, + 0x22, 0x55, 0xea, 0x12, 0x54, 0x95, 0xde, 0xb7, 0x1f, 0xb2, 0x8a, 0x3d, 0x86, 0xc7, 0x4a, 0xa5, 0x44, 0xb1, 0x53, + 0xd3, 0xce, 0xe2, 0x34, 0x45, 0xda, 0x65, 0x99, 0x78, 0x13, 0xfa, 0x1d, 0x49, 0xbb, 0x2d, 0xb3, 0xb6, 0x17, 0x8b, + 0x9b, 0x93, 0x48, 0xb1, 0x1c, 0xac, 0x35, 0xbc, 0x2d, 0x73, 0xec, 0x82, 0xb7, 0x39, 0xb7, 0x7e, 0xc1, 0x58, 0x43, + 0xeb, 0x33, 0xd6, 0xdf, 0xa4, 0x47, 0x46, 0x14, 0xa0, 0xfa, 0x37, 0x59, 0x08, 0x12, 0x37, 0xcc, 0xf8, 0x1d, 0xb5, + 0x61, 0x51, 0x5d, 0xd4, 0x3d, 0x4b, 0xac, 0x88, 0x58, 0x38, 0x7f, 0x5f, 0x9d, 0x05, 0x72, 0xe9, 0x6c, 0xc5, 0x35, + 0x0f, 0x47, 0x5d, 0x76, 0x3d, 0xb8, 0x53, 0x18, 0x53, 0xf3, 0xc9, 0x42, 0xf5, 0x86, 0x7b, 0x2e, 0x3e, 0xd7, 0x12, + 0x5e, 0x57, 0xfb, 0xdc, 0x9c, 0xe6, 0xf2, 0x2d, 0x2e, 0xab, 0x2a, 0xb5, 0x99, 0xc0, 0xa4, 0x6b, 0xad, 0xfe, 0x38, + 0x82, 0x35, 0x14, 0x91, 0xb8, 0x49, 0xd4, 0xc1, 0x66, 0x59, 0x87, 0x72, 0x9b, 0x09, 0x56, 0x92, 0x0d, 0xf6, 0x80, + 0x70, 0x6a, 0xb1, 0x99, 0x63, 0xa7, 0x0d, 0xe1, 0xf0, 0x1d, 0xb7, 0xa6, 0x88, 0x8a, 0x53, 0x77, 0xe1, 0xa9, 0x65, + 0xf9, 0xc3, 0xec, 0x6a, 0x4d, 0xd3, 0xf5, 0x1d, 0x6a, 0x64, 0x49, 0xb8, 0x72, 0x2f, 0x63, 0x98, 0x0f, 0x2d, 0xe4, + 0x59, 0xaa, 0x8e, 0x60, 0xd0, 0xd2, 0x2d, 0x37, 0xfc, 0x7d, 0xf8, 0x74, 0x5c, 0x6b, 0x22, 0xda, 0x38, 0xbe, 0xdc, + 0x43, 0x2a, 0x27, 0xfb, 0x49, 0xcc, 0x0b, 0x95, 0xd3, 0xe9, 0x49, 0x91, 0x80, 0x87, 0x9b, 0xb8, 0x70, 0x89, 0x72, + 0x2d, 0xcb, 0x74, 0x35, 0xc9, 0xa9, 0xa1, 0x42, 0xce, 0x8c, 0xa1, 0xc5, 0xfb, 0x59, 0xc4, 0x30, 0x63, 0x13, 0x66, + 0x66, 0x53, 0x53, 0xd3, 0x0e, 0x45, 0xee, 0x43, 0x25, 0x8f, 0xc4, 0x64, 0xe5, 0xd0, 0x38, 0x3a, 0x35, 0xdd, 0x63, + 0x70, 0x5d, 0x21, 0x9c, 0x6a, 0x54, 0xfb, 0x01, 0x74, 0x71, 0xfe, 0x85, 0xdb, 0x51, 0xbf, 0x1c, 0x8c, 0x7e, 0x6b, + 0x54, 0x13, 0x95, 0xf9, 0xd0, 0x0c, 0x5d, 0x3b, 0x32, 0x98, 0x1c, 0x03, 0xe0, 0x26, 0x13, 0x84, 0x0d, 0x1f, 0x57, + 0x60, 0x16, 0x7b, 0xaa, 0xaf, 0x7f, 0x0e, 0x52, 0x38, 0x97, 0xa9, 0x67, 0x61, 0xd4, 0x72, 0x80, 0x4b, 0x03, 0x0b, + 0xe3, 0x4a, 0x43, 0x0c, 0x9b, 0xdf, 0x8f, 0xb6, 0x89, 0x4c, 0xd2, 0x3d, 0xab, 0x29, 0x00, 0x9a, 0x4e, 0x41, 0xe4, + 0xdf, 0xa3, 0xe4, 0x05, 0xc7, 0xd1, 0x29, 0x3d, 0xfd, 0xa2, 0xd4, 0xa3, 0x19, 0xb4, 0xf7, 0x78, 0x75, 0xc1, 0xac, + 0x27, 0x23, 0xed, 0x88, 0x87, 0xd9, 0x09, 0xe4, 0x07, 0x48, 0x4d, 0xe9, 0x5a, 0x73, 0x63, 0xf7, 0x35, 0xc8, 0x96, + 0xed, 0x68, 0x90, 0xc3, 0x1a, 0xf9, 0x1a, 0x54, 0xca, 0xa1, 0x7c, 0x93, 0xcc, 0xe3, 0x24, 0xd8, 0xd7, 0xc8, 0xed, + 0x3b, 0xee, 0x6b, 0xb6, 0xb7, 0x43, 0x52, 0x1d, 0x92, 0xb0, 0xef, 0xb6, 0x69, 0x92, 0xe0, 0x70, 0x83, 0x0c, 0xc2, + 0x05, 0x6c, 0x64, 0xe8, 0xdb, 0xeb, 0x46, 0x21, 0x9a, 0xef, 0x1a, 0x7c, 0xaf, 0xee, 0x8b, 0x37, 0x66, 0x30, 0x49, + 0x92, 0x44, 0x60, 0x36, 0x53, 0x1a, 0x13, 0xe5, 0x1b, 0xc3, 0x73, 0xb5, 0xe7, 0x07, 0xe5, 0x5c, 0x4b, 0xd8, 0x33, + 0x1d, 0xbf, 0x1d, 0x8d, 0x57, 0xa5, 0xdf, 0xe0, 0x55, 0x52, 0x12, 0xdd, 0xf9, 0xfb, 0x00, 0x8e, 0xbc, 0x29, 0xeb, + 0x17, 0xf3, 0x1d, 0xa7, 0xc7, 0xd2, 0xe6, 0xed, 0x26, 0x2e, 0xf0, 0x37, 0x4f, 0xa4, 0x5e, 0xf1, 0xa5, 0xa6, 0x49, + 0xbf, 0x6e, 0xf1, 0x60, 0x17, 0x30, 0x79, 0xcb, 0x0d, 0xb3, 0x06, 0x7d, 0xb3, 0xca, 0x4d, 0xdf, 0x42, 0x79, 0x58, + 0xce, 0x63, 0x9e, 0x3a, 0x84, 0x5f, 0x3c, 0xaa, 0x43, 0x65, 0x34, 0xb7, 0x66, 0x27, 0xf4, 0x37, 0x98, 0xd7, 0xdc, + 0xc1, 0x0c, 0x27, 0xb2, 0x24, 0x0d, 0x6f, 0x7a, 0x7a, 0x3b, 0xca, 0x3c, 0x08, 0x42, 0x92, 0x22, 0xda, 0x06, 0x76, + 0xd0, 0x82, 0x0a, 0xb8, 0x41, 0xd4, 0xec, 0x3d, 0x62, 0xb6, 0x97, 0x76, 0x1f, 0xe7, 0xbd, 0x77, 0x3c, 0x59, 0x13, + 0x21, 0x67, 0x08, 0xa1, 0xf8, 0x7b, 0xda, 0xcf, 0x61, 0xcf, 0x08, 0x57, 0x5a, 0xa1, 0x60, 0xc4, 0x0d, 0xaa, 0x7e, + 0xcc, 0x16, 0x10, 0x2d, 0x12, 0x90, 0xb3, 0x5d, 0x0b, 0x6b, 0x26, 0x33, 0xf9, 0x49, 0x0c, 0x95, 0xd4, 0xb6, 0x7c, + 0xc3, 0x7f, 0xae, 0x0a, 0x49, 0x60, 0x31, 0x27, 0x75, 0xdf, 0x47, 0x12, 0x8b, 0x9b, 0x35, 0x9b, 0x87, 0x72, 0xed, + 0xf3, 0x72, 0xac, 0xbd, 0x83, 0xbe, 0x50, 0x71, 0x59, 0x2e, 0xaf, 0x4a, 0xbb, 0x44, 0x5d, 0xeb, 0x30, 0xb4, 0xa4, + 0xb4, 0x62, 0xd8, 0x87, 0x56, 0xf5, 0xc8, 0x91, 0xc3, 0xdf, 0x03, 0x69, 0xb8, 0xbb, 0xcc, 0xf0, 0xe6, 0xa5, 0xeb, + 0x5d, 0x34, 0x6d, 0xa5, 0x22, 0xe1, 0x4e, 0x6e, 0xbb, 0xa2, 0x33, 0x24, 0x88, 0x58, 0x0f, 0x1f, 0xe5, 0x87, 0x0b, + 0x86, 0x55, 0x8a, 0x36, 0xa4, 0xdb, 0x6c, 0x2e, 0x33, 0x37, 0x92, 0xb2, 0xdd, 0x9f, 0x56, 0xbd, 0x09, 0xaa, 0x75, + 0xa2, 0x36, 0xcf, 0xed, 0xb6, 0xd8, 0xba, 0x67, 0x00, 0xf5, 0x93, 0x33, 0x85, 0x23, 0x26, 0x88, 0x89, 0x56, 0x29, + 0x17, 0x61, 0xe6, 0x11, 0x0c, 0xf7, 0xd6, 0xfc, 0x84, 0xd8, 0xc7, 0x8b, 0x1c, 0x3f, 0xa6, 0x07, 0xb8, 0xe7, 0x13, + 0xb7, 0xcf, 0x69, 0x92, 0x83, 0xec, 0x88, 0xed, 0x46, 0xf1, 0x90, 0x8b, 0xee, 0x86, 0x4d, 0x25, 0x2c, 0x13, 0xe7, + 0xaa, 0xe5, 0xda, 0x18, 0x94, 0x0a, 0x45, 0x45, 0xee, 0x23, 0x65, 0xf1, 0xfb, 0x49, 0x55, 0xbe, 0x07, 0x91, 0xd8, + 0xf6, 0x49, 0x04, 0x52, 0xfd, 0xa3, 0xa0, 0x94, 0x12, 0xe6, 0xa5, 0x91, 0x67, 0xea, 0x4f, 0x28, 0x65, 0xc1, 0x43, + 0xc0, 0x17, 0x07, 0x9c, 0x0b, 0x6d, 0xfd, 0xf7, 0xb9, 0xee, 0x79, 0x3a, 0xf4, 0x92, 0xc2, 0x9d, 0xa3, 0xba, 0x4b, + 0xe4, 0x4e, 0xc9, 0xf8, 0x14, 0xa7, 0xe8, 0x41, 0xae, 0xd5, 0xb7, 0xdd, 0xbe, 0xa1, 0x6b, 0xbc, 0x7c, 0xa2, 0xf8, + 0xd6, 0xa6, 0xf2, 0x47, 0x51, 0xa7, 0xd3, 0x18, 0x9b, 0xec, 0x99, 0x72, 0x26, 0x17, 0x67, 0xb9, 0x9f, 0x1a, 0x0c, + 0x8d, 0x78, 0xc4, 0xd5, 0x12, 0xeb, 0xec, 0x3d, 0x66, 0x15, 0x27, 0xbc, 0x21, 0x0d, 0x04, 0xa8, 0xa4, 0x17, 0x1c, + 0xd1, 0x17, 0x68, 0xcb, 0xfa, 0xd2, 0xdd, 0xed, 0x47, 0x7a, 0xdc, 0xc1, 0xd1, 0x68, 0x55, 0x45, 0xbe, 0x4e, 0x0e, + 0x2a, 0xb9, 0x10, 0xa2, 0xd6, 0xf3, 0x1b, 0xd8, 0x42, 0xf3, 0x8b, 0xc9, 0x82, 0xfe, 0x2e, 0x6b, 0x4e, 0xd9, 0x7f, + 0xd6, 0xca, 0xb5, 0x21, 0x40, 0x1e, 0x17, 0xe4, 0xee, 0x15, 0xb8, 0x4c, 0x88, 0xfa, 0xc3, 0x7d, 0xcf, 0x76, 0x22, + 0xf2, 0xa1, 0x46, 0x8b, 0x45, 0xaf, 0x2a, 0x64, 0xbf, 0x3d, 0x1b, 0x77, 0xce, 0x1c, 0xf8, 0x3d, 0x2f, 0xbc, 0x92, + 0x4f, 0xfc, 0x86, 0x86, 0xf4, 0x1e, 0xd6, 0xb3, 0xa2, 0x6b, 0x16, 0x80, 0x52, 0x43, 0x0a, 0x7d, 0x0d, 0xdb, 0x73, + 0x50, 0x69, 0x9f, 0x79, 0x51, 0x8a, 0x80, 0xf1, 0x8d, 0xdd, 0x33, 0xf9, 0x54, 0x56, 0xc4, 0x25, 0x62, 0x96, 0x0e, + 0x18, 0x60, 0x64, 0x8e, 0x91, 0x51, 0xad, 0x1e, 0xaf, 0x70, 0x07, 0x8e, 0x94, 0x60, 0xab, 0xfd, 0xf3, 0xb8, 0x49, + 0xe6, 0xcf, 0x1c, 0x94, 0xfa, 0x84, 0xbc, 0xe7, 0x12, 0x82, 0xf1, 0xfc, 0xe4, 0x40, 0x75, 0xae, 0xc5, 0x06, 0x7b, + 0x3d, 0x67, 0x39, 0xce, 0xbc, 0x07, 0x46, 0xb0, 0xf5, 0xaf, 0xe2, 0x1f, 0xcb, 0x13, 0x77, 0x8b, 0x07, 0x31, 0xa9, + 0x95, 0xd3, 0xb5, 0x36, 0x5b, 0xe8, 0x6e, 0x20, 0xc3, 0x99, 0xe6, 0xcd, 0x9a, 0xba, 0xdc, 0x54, 0xc3, 0xc0, 0x6a, + 0xe6, 0x84, 0x5c, 0xcc, 0x91, 0xf8, 0x2f, 0x18, 0xe7, 0x66, 0x0d, 0x65, 0x2e, 0xc7, 0x66, 0x72, 0x09, 0xe4, 0xea, + 0x14, 0xfb, 0xcd, 0x3f, 0xfc, 0x06, 0x54, 0xc2, 0xf2, 0xa3, 0x7f, 0x88, 0xf2, 0x03, 0xcb, 0xd4, 0x1c, 0x7e, 0xe4, + 0xa8, 0x87, 0x32, 0x97, 0xc7, 0xff, 0x80, 0xac, 0xcf, 0xfa, 0xca, 0xf7, 0x93, 0xbb, 0xef, 0x9b, 0xe4, 0xcf, 0x6c, + 0x35, 0x27, 0x9b, 0x5d, 0x6b, 0xef, 0xe7, 0x7b, 0xf0, 0xbd, 0xf9, 0x3d, 0x32, 0xab, 0x85, 0xde, 0x28, 0xd4, 0x55, + 0x0f, 0x58, 0xe8, 0xd5, 0x4f, 0x7d, 0x8b, 0xd0, 0xec, 0x43, 0xac, 0xc1, 0x39, 0x44, 0x54, 0xba, 0xa7, 0x5e, 0xc3, + 0xb6, 0xbe, 0x77, 0x27, 0x06, 0xba, 0x66, 0x38, 0xef, 0x69, 0x93, 0xc8, 0x6d, 0xd4, 0xc3, 0xe6, 0xfd, 0xd9, 0x15, + 0xd6, 0x44, 0xb7, 0xba, 0x61, 0xe7, 0x52, 0xb2, 0x7c, 0x6b, 0x0f, 0xe0, 0xb1, 0x7a, 0x10, 0xf6, 0xae, 0x99, 0x3f, + 0x96, 0x03, 0x7f, 0x96, 0xf2, 0x4e, 0xb5, 0xb4, 0xfa, 0x8d, 0x6f, 0x55, 0x1f, 0xfb, 0x80, 0x37, 0xc2, 0x53, 0x41, + 0x75, 0xf6, 0x9c, 0x3d, 0x79, 0x71, 0x21, 0xbe, 0xd1, 0x0d, 0x2e, 0xa1, 0x5b, 0x15, 0x79, 0x03, 0x5f, 0xda, 0xbc, + 0xaa, 0xe0, 0x79, 0x68, 0xc9, 0x28, 0x4f, 0x9a, 0x72, 0x6c, 0xe6, 0x76, 0x31, 0x49, 0xb7, 0x32, 0x3f, 0xba, 0x51, + 0x81, 0x0b, 0x04, 0x92, 0x74, 0x65, 0x08, 0xff, 0x04, 0x27, 0x5e, 0x2b, 0xe1, 0xd3, 0x8d, 0x66, 0xbd, 0xd7, 0x55, + 0x3d, 0xee, 0x1a, 0xf4, 0x22, 0x3e, 0xb5, 0xd3, 0x5e, 0x7b, 0x84, 0xbf, 0xbf, 0x7f, 0x9e, 0x69, 0xe4, 0xbf, 0xce, + 0xec, 0xe4, 0x3f, 0xcf, 0xcd, 0xe4, 0xbf, 0xce, 0x0d, 0x9c, 0x5a, 0x7d, 0xcf, 0xbe, 0x7a, 0x61, 0x5f, 0xbd, 0xb2, + 0xc7, 0x4c, 0xed, 0xa1, 0x75, 0xad, 0x73, 0xd0, 0x8e, 0x5d, 0xcf, 0xf5, 0x96, 0x1c, 0xf0, 0xad, 0xae, 0xb2, 0x64, + 0xfd, 0xdb, 0xc9, 0xee, 0xde, 0x15, 0x53, 0xf9, 0xfe, 0x00, 0xc1, 0x93, 0xef, 0x87, 0x65, 0xad, 0xa2, 0x6c, 0xce, + 0xb4, 0x8c, 0xad, 0x74, 0xb6, 0xf7, 0x50, 0x3c, 0x9d, 0x3e, 0x42, 0xb2, 0xad, 0xe1, 0x0c, 0x55, 0x26, 0xf0, 0x1f, + 0x49, 0x3f, 0x36, 0x2a, 0xbd, 0x68, 0xbc, 0x74, 0xef, 0x48, 0xca, 0xf3, 0x17, 0x43, 0xc4, 0xc8, 0xb4, 0x9c, 0xda, + 0x3b, 0x98, 0xba, 0xc7, 0xac, 0xc5, 0xcb, 0x0e, 0xc8, 0x6c, 0xe9, 0x56, 0x52, 0x81, 0x10, 0xc6, 0xb6, 0x85, 0xff, + 0x2c, 0xc0, 0xaa, 0xfa, 0x96, 0x59, 0x3a, 0xcd, 0x9e, 0xa2, 0xa5, 0xd3, 0x0b, 0xd0, 0x20, 0x0e, 0x43, 0x99, 0xee, + 0x0a, 0x99, 0xc3, 0xf3, 0x2a, 0xae, 0x20, 0xab, 0x5f, 0x28, 0xf9, 0xef, 0x73, 0xf6, 0x70, 0xfd, 0x41, 0x40, 0x83, + 0xff, 0xdb, 0x64, 0x3b, 0xe8, 0x4f, 0x68, 0x6b, 0x9c, 0x72, 0x49, 0xa4, 0xfd, 0x5c, 0xc9, 0xdb, 0x33, 0xdf, 0x67, + 0xd7, 0xb7, 0xcf, 0x18, 0xce, 0xcf, 0x55, 0x08, 0x64, 0xce, 0xda, 0x4f, 0xf7, 0xf5, 0x31, 0x15, 0xb9, 0xeb, 0xbc, + 0xe7, 0x04, 0xab, 0xdc, 0x99, 0x52, 0x6b, 0x66, 0x72, 0x7e, 0xfe, 0xf2, 0x3f, 0xcc, 0xaf, 0x25, 0xe5, 0xa0, 0xef, + 0xf5, 0x92, 0xdd, 0xdc, 0x17, 0xca, 0xd2, 0xf3, 0x4c, 0xf9, 0xe8, 0x83, 0x4a, 0x3e, 0x1f, 0xd0, 0x74, 0x3f, 0xdd, + 0xf9, 0x8f, 0xea, 0x01, 0xdd, 0xa6, 0xf9, 0xac, 0xfb, 0x65, 0x49, 0x39, 0xe0, 0x07, 0xbd, 0x7c, 0x7e, 0x7b, 0x8b, + 0x7f, 0x6c, 0x3a, 0xdf, 0xd3, 0x05, 0x80, 0xf0, 0xfc, 0x28, 0xd9, 0x1c, 0x87, 0x9c, 0xc9, 0x9d, 0xeb, 0x0a, 0xcf, + 0xa8, 0x5a, 0x0e, 0x85, 0x5c, 0x2c, 0xf1, 0x19, 0xf9, 0x98, 0x27, 0xb2, 0xd1, 0x27, 0xb0, 0x4b, 0x99, 0xbd, 0x87, + 0x25, 0x64, 0xb7, 0xcd, 0xa7, 0x70, 0x94, 0xcf, 0x3d, 0xa2, 0x6d, 0x76, 0x1d, 0x16, 0x26, 0x6d, 0x69, 0x2a, 0x2e, + 0x3c, 0x60, 0xdf, 0x09, 0x0a, 0x83, 0xd5, 0x48, 0xed, 0x63, 0x46, 0x4e, 0x6f, 0x21, 0xba, 0xce, 0x38, 0x95, 0xbd, + 0xdf, 0xc1, 0x80, 0xa5, 0xf0, 0xf0, 0xd0, 0x7b, 0x0d, 0x68, 0x87, 0xcf, 0xb9, 0xe8, 0xa3, 0x9b, 0x50, 0xaf, 0x06, + 0xe0, 0xc4, 0x59, 0x36, 0xdd, 0x78, 0xb9, 0x9f, 0xf3, 0x87, 0xce, 0xe5, 0xca, 0xea, 0x63, 0x0d, 0x6d, 0x9b, 0xa3, + 0x33, 0xce, 0x57, 0x09, 0x2a, 0x8c, 0x30, 0x67, 0x78, 0xfe, 0xf5, 0xd4, 0x7d, 0xa0, 0x04, 0x7d, 0xa2, 0xd7, 0x9c, + 0x90, 0xd1, 0x7f, 0x22, 0x50, 0xa7, 0x93, 0xb4, 0x67, 0xf5, 0x47, 0xff, 0x1e, 0x3d, 0xb4, 0x4d, 0x8f, 0x7a, 0xab, + 0xe0, 0x3e, 0x85, 0x06, 0xa5, 0x52, 0x69, 0xac, 0x6d, 0x8e, 0x7f, 0x75, 0x72, 0x9d, 0x46, 0x6d, 0x8f, 0x70, 0x76, + 0xa6, 0xcd, 0x79, 0xdc, 0xde, 0xcc, 0xdc, 0xab, 0x17, 0x0f, 0xfd, 0x17, 0xff, 0x65, 0x18, 0x97, 0x8c, 0xb4, 0x20, + 0x37, 0xa9, 0x3d, 0xab, 0x1e, 0x1b, 0xf3, 0xaa, 0x7f, 0xab, 0x7e, 0x64, 0x54, 0xc0, 0xc6, 0x58, 0xcf, 0xe1, 0x32, + 0x3e, 0xcd, 0xeb, 0xa8, 0x28, 0x0b, 0x36, 0xc4, 0xf9, 0x70, 0xbb, 0xd7, 0xde, 0x23, 0x3b, 0xd0, 0xe4, 0xd7, 0x5f, + 0x66, 0xd3, 0x8f, 0xb6, 0xf3, 0x3b, 0x50, 0xcc, 0xfa, 0xfe, 0x94, 0x62, 0x83, 0xba, 0x02, 0xb7, 0x01, 0x97, 0xef, + 0xd8, 0x34, 0xf3, 0xaa, 0xf1, 0xbe, 0x7f, 0xc0, 0x5a, 0x12, 0x8a, 0x56, 0x0a, 0x0e, 0x8b, 0x75, 0x19, 0x45, 0x69, + 0xb1, 0x26, 0xfa, 0x55, 0xa7, 0xaa, 0xd3, 0xb6, 0x1b, 0x38, 0x37, 0x11, 0xa6, 0xaa, 0xb7, 0xa2, 0x1f, 0x22, 0xf2, + 0x36, 0x9e, 0xea, 0xab, 0x6d, 0x20, 0x86, 0xa7, 0xb8, 0x6e, 0xad, 0x7a, 0x0d, 0x67, 0x30, 0xa0, 0x27, 0x7d, 0x71, + 0x0c, 0xc1, 0xc3, 0x97, 0x01, 0x4b, 0xbd, 0xe9, 0xd2, 0xe1, 0xed, 0x63, 0xad, 0xd6, 0x9b, 0x3a, 0xaf, 0x3e, 0x55, + 0x6a, 0xd3, 0xf2, 0x74, 0x8f, 0x92, 0x21, 0xd1, 0xfe, 0xaa, 0x7c, 0xf6, 0xd3, 0x21, 0x63, 0x7b, 0x26, 0x9e, 0x2a, + 0x5e, 0x28, 0x69, 0x79, 0x57, 0xf1, 0x30, 0x8e, 0x3b, 0x29, 0x6a, 0x08, 0x56, 0xfc, 0x63, 0x18, 0x16, 0xe9, 0x9c, + 0xad, 0x0f, 0x75, 0xf0, 0x4a, 0x28, 0xe9, 0xca, 0xb5, 0xd6, 0x0a, 0x74, 0x6c, 0xe3, 0x99, 0x9f, 0x39, 0x9d, 0x09, + 0x50, 0xf9, 0x55, 0x98, 0x04, 0x14, 0x44, 0x22, 0x3c, 0x51, 0x2d, 0xbc, 0x28, 0xfa, 0x0c, 0xe6, 0xd0, 0x0c, 0xab, + 0xc1, 0x34, 0x15, 0xfd, 0x8d, 0x32, 0x30, 0xd7, 0x21, 0x82, 0x17, 0x99, 0x6b, 0xf3, 0x31, 0x0f, 0x1d, 0x8a, 0x9c, + 0x91, 0x53, 0x7f, 0xb0, 0xa4, 0xbc, 0x81, 0x3c, 0x56, 0xa1, 0xf8, 0x57, 0x30, 0x88, 0x73, 0x36, 0x00, 0x85, 0x8c, + 0x3d, 0x8f, 0x00, 0x60, 0x49, 0x3e, 0x49, 0x02, 0x6f, 0xfa, 0xbb, 0xb3, 0xf1, 0x59, 0x51, 0xb0, 0x5f, 0xed, 0x9b, + 0x49, 0xd3, 0x2c, 0xdc, 0xdd, 0xb3, 0x65, 0xf7, 0x14, 0x41, 0x04, 0x48, 0x32, 0x9b, 0x56, 0xec, 0x3d, 0xc4, 0xaf, + 0x14, 0x30, 0x03, 0x93, 0x0c, 0xe0, 0x84, 0x69, 0x49, 0xeb, 0x8a, 0x9f, 0x5c, 0x1d, 0xb6, 0x72, 0x5b, 0x28, 0xc1, + 0x22, 0x32, 0x8f, 0x6e, 0x89, 0x34, 0x4b, 0xe9, 0x9e, 0x5b, 0xeb, 0x3b, 0x19, 0xc7, 0x0f, 0x23, 0xe7, 0x89, 0xe3, + 0xf8, 0x35, 0x89, 0x68, 0x45, 0x44, 0x71, 0xba, 0x75, 0x0e, 0xd9, 0x15, 0x94, 0x8a, 0x15, 0x80, 0xaa, 0x07, 0x4c, + 0x35, 0xc1, 0x9a, 0x5f, 0xdc, 0x05, 0x7b, 0xf9, 0x40, 0x7b, 0x42, 0x71, 0x92, 0xac, 0x8c, 0xf5, 0xd0, 0x17, 0x7c, + 0x85, 0x5d, 0x2e, 0x46, 0x9b, 0x1d, 0x93, 0x24, 0xb5, 0xa2, 0x09, 0x06, 0xd4, 0x35, 0xc3, 0x69, 0xd7, 0xce, 0x3f, + 0x72, 0x9a, 0xd9, 0x74, 0x40, 0x8e, 0x71, 0x29, 0x74, 0x1b, 0xf7, 0xa4, 0x10, 0x47, 0x43, 0xe8, 0xe3, 0x30, 0x14, + 0x46, 0x3f, 0xc3, 0x66, 0x56, 0x9f, 0xf6, 0x31, 0x17, 0xb4, 0x35, 0xa6, 0xa8, 0xaa, 0xcb, 0xae, 0x29, 0x00, 0x1b, + 0x29, 0x67, 0xb0, 0x02, 0xfe, 0x78, 0xd9, 0x4e, 0x57, 0x0f, 0x37, 0x36, 0xf9, 0x0f, 0x6e, 0xf6, 0x1b, 0xe9, 0x27, + 0xf0, 0x47, 0x48, 0x66, 0xd6, 0x04, 0xd6, 0x10, 0xce, 0x4b, 0x62, 0x81, 0xe8, 0x71, 0xbe, 0x1f, 0x04, 0x7f, 0x5c, + 0x2d, 0x1e, 0x14, 0x5b, 0x98, 0xb4, 0x92, 0x73, 0xa2, 0x5e, 0x53, 0xa7, 0x8e, 0x7c, 0x90, 0x98, 0x44, 0x4c, 0x28, + 0xcf, 0xa3, 0x9f, 0x66, 0xb5, 0x9a, 0x05, 0xb5, 0x4d, 0x54, 0xec, 0x15, 0xba, 0x73, 0x3b, 0x67, 0x48, 0xb2, 0x23, + 0x38, 0xd5, 0x65, 0xd9, 0x70, 0x7b, 0xdb, 0x9a, 0x79, 0xd3, 0xf0, 0x35, 0x9d, 0xc3, 0x32, 0xee, 0x82, 0x8e, 0xb5, + 0xf1, 0x9a, 0xd8, 0x1e, 0x0c, 0x1e, 0x16, 0x4f, 0x94, 0x4e, 0xa3, 0xe9, 0xa6, 0x9e, 0x99, 0x9b, 0x7d, 0x4d, 0x5d, + 0x4d, 0xb4, 0xb3, 0x04, 0x9a, 0xcf, 0x46, 0xf1, 0x1a, 0x5b, 0xe6, 0x1a, 0x39, 0xb6, 0x96, 0xb8, 0x5b, 0xe6, 0x1d, + 0x8b, 0x91, 0xbb, 0x81, 0x51, 0x62, 0xee, 0x22, 0x86, 0x9a, 0x9f, 0xc3, 0xdc, 0x9e, 0x98, 0x40, 0xa8, 0x7f, 0x5d, + 0x4f, 0x66, 0x70, 0x31, 0x4d, 0x23, 0x19, 0xd6, 0x83, 0xd2, 0xf7, 0x44, 0x73, 0x8f, 0x78, 0xce, 0x09, 0xb6, 0x6d, + 0x2b, 0x5f, 0x7c, 0xcd, 0x18, 0xf8, 0xc0, 0x54, 0x77, 0x10, 0x5c, 0xd1, 0x5b, 0xd0, 0x3c, 0x83, 0xeb, 0x01, 0xb3, + 0x6f, 0x84, 0xf9, 0xbc, 0x10, 0x75, 0xfb, 0x44, 0x26, 0xff, 0x05, 0x84, 0x62, 0x7a, 0xab, 0xf3, 0x47, 0xfb, 0x1c, + 0xee, 0x3c, 0x64, 0x81, 0xc7, 0x92, 0x38, 0x64, 0xf8, 0xc7, 0x8d, 0xb6, 0x8c, 0x45, 0xcf, 0x9c, 0xc7, 0x2d, 0x89, + 0x09, 0xa5, 0xda, 0x5d, 0x4b, 0xa2, 0xbc, 0x16, 0x61, 0x51, 0x85, 0xd8, 0x6d, 0x15, 0x52, 0x19, 0x75, 0x45, 0xa4, + 0x8a, 0xc7, 0x59, 0x37, 0x3b, 0x43, 0x69, 0x04, 0x19, 0x0a, 0x26, 0xa8, 0x6a, 0x9f, 0x44, 0xb5, 0x14, 0xf3, 0xa0, + 0x4d, 0x13, 0xf5, 0xf0, 0xba, 0x2a, 0x63, 0xe1, 0x71, 0xd6, 0xbd, 0xed, 0x88, 0x75, 0xeb, 0x3a, 0xce, 0xb3, 0x75, + 0xe4, 0xad, 0x1c, 0x99, 0xd7, 0x15, 0x61, 0x2b, 0xc2, 0xf6, 0x41, 0x2d, 0x22, 0xca, 0x50, 0x22, 0xe1, 0xc0, 0x16, + 0xd4, 0xdb, 0x0b, 0x65, 0x36, 0x10, 0xee, 0x95, 0xf5, 0x51, 0xc9, 0x56, 0xd2, 0xb6, 0x95, 0x52, 0xb0, 0x80, 0x42, + 0x58, 0x68, 0xec, 0x39, 0xeb, 0xfe, 0xf6, 0xb9, 0x8e, 0xad, 0xff, 0xdb, 0x40, 0x6c, 0xf6, 0xef, 0xde, 0xdf, 0x8f, + 0x31, 0xc0, 0xa8, 0x7b, 0xd6, 0x15, 0xe9, 0x5b, 0x5d, 0xdf, 0x22, 0x7d, 0xf3, 0xf5, 0x4d, 0x6d, 0x4e, 0x78, 0x96, + 0xb1, 0x36, 0x6a, 0xe3, 0xce, 0x0d, 0xb4, 0x0e, 0xfb, 0x92, 0x92, 0xda, 0xef, 0xdb, 0xe5, 0xa7, 0xb1, 0x2a, 0xf3, + 0xa5, 0x99, 0x94, 0xb2, 0xe9, 0xc1, 0xa9, 0x5a, 0xd3, 0x65, 0x84, 0xd4, 0xbd, 0x18, 0x6a, 0x2b, 0xd5, 0xa9, 0xab, + 0xdb, 0x7c, 0x7c, 0x31, 0x26, 0xc6, 0x2f, 0xff, 0x0a, 0x17, 0xcf, 0x77, 0x4c, 0x87, 0xb6, 0xbc, 0xf3, 0xbe, 0xad, + 0xc4, 0xb8, 0xdc, 0x94, 0x70, 0x8e, 0x66, 0x16, 0x32, 0x46, 0x5c, 0x56, 0x9d, 0xbb, 0xe0, 0x32, 0x82, 0xc0, 0x17, + 0x74, 0x55, 0x29, 0x99, 0xa5, 0xbe, 0xad, 0xa3, 0xcf, 0xf7, 0x44, 0x95, 0xc3, 0x9f, 0x0b, 0x4c, 0xe8, 0x42, 0x57, + 0x95, 0xeb, 0x7b, 0x45, 0xc4, 0x50, 0x14, 0x71, 0xce, 0xa9, 0xf4, 0x2e, 0x2c, 0x7c, 0x53, 0x8f, 0xa7, 0x44, 0x6d, + 0x1b, 0xa4, 0x98, 0xc5, 0x98, 0x4b, 0x4b, 0x31, 0x97, 0xf2, 0x88, 0xed, 0xf3, 0x18, 0x08, 0x8b, 0x49, 0x20, 0xf2, + 0xe1, 0xca, 0x85, 0x63, 0xf9, 0x22, 0x60, 0xb0, 0x8a, 0x3e, 0x10, 0x9c, 0xdf, 0x99, 0x65, 0x17, 0x7f, 0x9b, 0x0f, + 0x47, 0x26, 0xe3, 0x2a, 0x0c, 0x81, 0x3b, 0xe2, 0xb7, 0x4e, 0x3b, 0x94, 0x01, 0xce, 0x19, 0x4d, 0x0c, 0x98, 0x75, + 0xd3, 0x34, 0x38, 0x55, 0x4d, 0x5b, 0xe5, 0x6e, 0x5e, 0x61, 0x26, 0x24, 0x31, 0x10, 0xe5, 0x66, 0xf8, 0x95, 0x1a, + 0x09, 0xc8, 0xf9, 0xfb, 0x2e, 0xce, 0xc9, 0x29, 0x85, 0x13, 0x95, 0x4c, 0x82, 0xaf, 0x1d, 0x78, 0x87, 0xba, 0x15, + 0x2f, 0xc4, 0x71, 0x9a, 0xf2, 0xc8, 0x04, 0xf4, 0x40, 0xed, 0x40, 0x94, 0x55, 0x4b, 0x8e, 0xc2, 0x44, 0x42, 0x28, + 0x85, 0x8f, 0xf8, 0x4c, 0xe6, 0xa2, 0xaa, 0x35, 0xaf, 0xfa, 0x82, 0x6e, 0x41, 0x62, 0x40, 0x54, 0x11, 0x22, 0xc9, + 0xa4, 0x5a, 0x37, 0x54, 0x58, 0x2c, 0x5d, 0x5a, 0x0c, 0xe2, 0x04, 0xc9, 0x3c, 0x2e, 0x04, 0xff, 0x32, 0xb0, 0xb7, + 0x1c, 0x6f, 0x7a, 0xef, 0x06, 0x75, 0x35, 0x32, 0x93, 0x9d, 0xf7, 0xe6, 0x45, 0xaf, 0xa4, 0x25, 0x97, 0x0f, 0x89, + 0x42, 0x7f, 0x5f, 0xb7, 0x9d, 0x65, 0x35, 0x91, 0x82, 0x79, 0x59, 0x54, 0x17, 0x95, 0xed, 0xa5, 0x95, 0x0b, 0x3c, + 0xee, 0x1e, 0x26, 0x48, 0xf0, 0xdd, 0x66, 0xf2, 0x14, 0xb8, 0x48, 0xd6, 0xd8, 0x72, 0x9f, 0x48, 0xa3, 0xa3, 0xdb, + 0x28, 0x59, 0x1d, 0xd9, 0xda, 0x3f, 0x41, 0x94, 0xe4, 0xcc, 0x5a, 0x89, 0xae, 0xff, 0x59, 0xea, 0x26, 0x17, 0x85, + 0xb5, 0x38, 0xe4, 0x20, 0x6e, 0x3a, 0x0b, 0x61, 0x4a, 0xf6, 0x56, 0x60, 0x23, 0x44, 0x86, 0x8b, 0x49, 0x16, 0xe4, + 0xdc, 0x8b, 0x1f, 0x1c, 0x29, 0xf8, 0x8f, 0x48, 0x0d, 0x2d, 0x99, 0xd2, 0xff, 0x70, 0x1d, 0xe1, 0x5b, 0x19, 0x0e, + 0x92, 0xd9, 0x8b, 0x17, 0xdc, 0x96, 0x9e, 0x77, 0xcc, 0x06, 0x49, 0xf8, 0xfd, 0xec, 0xf2, 0x59, 0x6f, 0x0f, 0xe2, + 0x0f, 0x65, 0x42, 0xf0, 0x45, 0x47, 0xb5, 0x8b, 0xa7, 0x51, 0x71, 0x3a, 0x94, 0x5f, 0x8f, 0x4f, 0xcd, 0xef, 0xed, + 0xf2, 0x02, 0x7e, 0xfa, 0xe5, 0x9c, 0x03, 0x33, 0xf0, 0x85, 0xb6, 0x1a, 0x6b, 0xd8, 0x0b, 0x83, 0x3d, 0x86, 0x92, + 0x45, 0x3a, 0xb4, 0x9f, 0x8d, 0x30, 0x1f, 0xba, 0xde, 0x66, 0xfd, 0x1d, 0xc3, 0xac, 0xce, 0x30, 0xbe, 0xb1, 0xaf, + 0x6a, 0x65, 0x76, 0xdb, 0xb0, 0xa7, 0x92, 0x9d, 0xf6, 0xe5, 0x06, 0x53, 0x37, 0x67, 0x6f, 0x43, 0xcd, 0xe5, 0x9b, + 0x51, 0x5c, 0x79, 0x33, 0x0f, 0x4b, 0x08, 0x18, 0x33, 0xcc, 0xb9, 0x22, 0xe7, 0x5a, 0xd9, 0x0f, 0x96, 0xd8, 0x1f, + 0xb6, 0x42, 0xda, 0x54, 0x45, 0x32, 0xb3, 0x81, 0x8f, 0xb5, 0x5a, 0x7b, 0x5a, 0x0f, 0xcc, 0xd2, 0x89, 0xe9, 0x58, + 0xb3, 0xb4, 0x82, 0xa1, 0x54, 0x68, 0xb5, 0xd4, 0x1d, 0xae, 0xd2, 0x97, 0x5a, 0x5e, 0xf2, 0x84, 0x84, 0xfd, 0x04, + 0xb2, 0x13, 0xdf, 0xc3, 0x3d, 0x69, 0xfb, 0xce, 0xac, 0xb1, 0x31, 0x95, 0x25, 0xca, 0x93, 0x72, 0x05, 0x65, 0xea, + 0x1d, 0x60, 0xa8, 0xa8, 0x31, 0x36, 0x74, 0x87, 0x06, 0x6d, 0x34, 0x0e, 0xf7, 0x85, 0xeb, 0x6d, 0x41, 0xfe, 0xa3, + 0xbe, 0xcf, 0xc9, 0x57, 0x67, 0xb3, 0xa8, 0xa7, 0xf5, 0x56, 0x63, 0xe4, 0xc8, 0x78, 0x80, 0xd7, 0x9b, 0x93, 0x2a, + 0x5b, 0x30, 0x64, 0xaf, 0xa1, 0xfe, 0xa9, 0x99, 0xba, 0x90, 0x76, 0x62, 0x46, 0x94, 0xf1, 0x20, 0x92, 0x04, 0x3d, + 0x59, 0x0f, 0x82, 0x6b, 0x96, 0x85, 0xb5, 0xc9, 0xc8, 0x3d, 0x18, 0xce, 0x91, 0x8a, 0xe8, 0x12, 0x8a, 0xe2, 0x9c, + 0xcd, 0xe3, 0x13, 0x86, 0x1c, 0xe5, 0xb1, 0x58, 0x96, 0x2c, 0xa8, 0xf7, 0x2d, 0x8c, 0xd4, 0x64, 0x9b, 0x8e, 0xa5, + 0xe4, 0xb2, 0x03, 0x38, 0xb1, 0xa3, 0xed, 0x3c, 0x61, 0x4e, 0x6d, 0x5d, 0x82, 0x9d, 0xec, 0xd4, 0xdc, 0xad, 0xc8, + 0x00, 0xc9, 0x03, 0x21, 0x0a, 0x03, 0x3e, 0xdf, 0xaf, 0x08, 0x50, 0xcd, 0x71, 0x8a, 0xc4, 0x1f, 0x84, 0xf2, 0xc7, + 0x13, 0x49, 0xa7, 0xc2, 0x72, 0xd7, 0x33, 0xbc, 0x39, 0x0e, 0xa0, 0x95, 0x7a, 0xb2, 0xf9, 0x41, 0x89, 0xb2, 0x91, + 0xbf, 0x8a, 0xb5, 0x8e, 0x18, 0x22, 0x1c, 0xf8, 0xcd, 0x6a, 0x43, 0xd2, 0x78, 0xb3, 0xba, 0x38, 0x1a, 0x85, 0x42, + 0x57, 0x07, 0xdc, 0x47, 0x2a, 0x00, 0xfb, 0x66, 0xc3, 0x53, 0x37, 0x4e, 0x77, 0x51, 0x96, 0x25, 0x9c, 0x06, 0x13, + 0xf8, 0x67, 0xd3, 0xb5, 0xba, 0x85, 0x8b, 0x35, 0xcd, 0xc4, 0x47, 0x71, 0x3a, 0xdd, 0xd7, 0xbd, 0x0e, 0x01, 0xff, + 0x72, 0x89, 0x1d, 0xd2, 0x27, 0xa4, 0x8a, 0x83, 0x11, 0x73, 0x74, 0x8c, 0x4b, 0x9a, 0xe9, 0xa9, 0x21, 0x77, 0x97, + 0xca, 0x47, 0x28, 0x07, 0xaa, 0x73, 0x3c, 0x3d, 0x64, 0x37, 0xc3, 0x31, 0x42, 0x6d, 0x67, 0x88, 0x2b, 0x03, 0xf5, + 0x04, 0xc8, 0x95, 0x04, 0xc2, 0x32, 0xcf, 0x67, 0x48, 0xdf, 0x33, 0x66, 0x02, 0x1a, 0x3a, 0x50, 0x6e, 0x7a, 0x52, + 0xe6, 0x90, 0x7a, 0xa8, 0x83, 0x10, 0x13, 0x1e, 0xf4, 0xb2, 0xa9, 0x69, 0x65, 0x1d, 0x8d, 0x50, 0x69, 0x42, 0x41, + 0xfc, 0x02, 0xa7, 0xe8, 0xab, 0x21, 0xf2, 0x97, 0x91, 0xf2, 0x3a, 0x2b, 0xf3, 0x86, 0xf4, 0x12, 0x2d, 0xb2, 0xfa, + 0xc6, 0xc8, 0xec, 0x48, 0x5d, 0x56, 0x7a, 0xed, 0x05, 0x60, 0x1e, 0x0e, 0xc1, 0x89, 0x44, 0xc4, 0x3c, 0x89, 0x26, + 0xb2, 0xa9, 0x50, 0xfe, 0xcc, 0xee, 0x49, 0x01, 0x5c, 0xce, 0x23, 0x41, 0x13, 0x81, 0x8f, 0x1d, 0x00, 0x67, 0x66, + 0x10, 0xe0, 0x6c, 0x35, 0x69, 0x04, 0xc6, 0x5c, 0x2b, 0x6f, 0x35, 0xfb, 0x98, 0x11, 0xe5, 0xb8, 0x98, 0x1b, 0xd9, + 0x5d, 0x93, 0xfb, 0x53, 0xcc, 0x13, 0x1b, 0x73, 0xf8, 0xb9, 0xf6, 0x2a, 0x99, 0xfe, 0x65, 0x06, 0x3e, 0x29, 0x51, + 0x7d, 0x69, 0x50, 0xbc, 0x6e, 0xe3, 0x82, 0x36, 0xda, 0x35, 0xe4, 0xb2, 0xe8, 0x30, 0x58, 0xae, 0xfd, 0xbf, 0x7e, + 0x7b, 0x3e, 0xef, 0x2b, 0xe7, 0x63, 0x76, 0xc5, 0x7d, 0x70, 0x58, 0x33, 0xe4, 0xfc, 0xba, 0x2e, 0x9e, 0xe3, 0xfb, + 0xf5, 0xb7, 0xb9, 0xf1, 0x74, 0x77, 0x10, 0x64, 0x2e, 0xa4, 0x3e, 0xb3, 0x84, 0xe8, 0xc3, 0xd0, 0xe2, 0xd9, 0x18, + 0x55, 0xa2, 0xf1, 0xa5, 0x43, 0x8a, 0x65, 0x8b, 0xa7, 0x27, 0x81, 0x78, 0x39, 0xdc, 0x93, 0x2d, 0x10, 0x2b, 0x4a, + 0x84, 0x39, 0x9d, 0x88, 0x34, 0x8e, 0x80, 0xf1, 0x4a, 0xdc, 0x33, 0x04, 0x46, 0x1a, 0x65, 0xd6, 0xb4, 0xff, 0xd8, + 0x88, 0xec, 0x73, 0x48, 0x34, 0x19, 0x36, 0xe5, 0x93, 0xcd, 0xa8, 0xbd, 0x12, 0x09, 0x45, 0xc3, 0xba, 0x9f, 0xa6, + 0x19, 0x95, 0xf7, 0x62, 0x1c, 0x12, 0x87, 0x70, 0xd2, 0xbb, 0xdf, 0xaf, 0xbf, 0x95, 0x3c, 0xfc, 0x1e, 0xf6, 0x1f, + 0xbf, 0xf8, 0x1f, 0xbf, 0x87, 0x7b, 0xf2, 0x8b, 0x9f, 0xfc, 0x1e, 0xf2, 0xc9, 0x2f, 0xe2, 0xa5, 0xd2, 0xf4, 0x95, + 0xdd, 0x79, 0x30, 0x16, 0x0c, 0xe5, 0xb2, 0x8c, 0x6c, 0xa5, 0x0a, 0x7e, 0xf1, 0x21, 0xe1, 0x3e, 0x17, 0x48, 0xc9, + 0xa9, 0x64, 0x82, 0x95, 0xa8, 0x64, 0x65, 0xe8, 0x14, 0xd4, 0xa7, 0x01, 0x3e, 0x4a, 0xbd, 0xfd, 0x9c, 0x7f, 0xba, + 0x35, 0x92, 0xc6, 0x40, 0x3c, 0x19, 0x82, 0xae, 0xdc, 0x99, 0x5b, 0xcf, 0x4d, 0x49, 0x18, 0x65, 0x39, 0x62, 0xb4, + 0xa2, 0xd2, 0x8e, 0xb3, 0x44, 0xef, 0x3c, 0x18, 0x34, 0x13, 0xf4, 0xed, 0x7b, 0xe8, 0xa4, 0xb0, 0x3b, 0x43, 0x01, + 0x72, 0x96, 0x95, 0x02, 0x1e, 0xd8, 0xc7, 0x5e, 0x3c, 0x47, 0x5a, 0x79, 0x35, 0xa9, 0xa2, 0x06, 0xd7, 0xe4, 0x60, + 0x8c, 0x11, 0x12, 0xf7, 0xf4, 0x2f, 0xf9, 0x98, 0x9c, 0xb9, 0x79, 0xab, 0x59, 0xb8, 0xc7, 0xd4, 0x72, 0x40, 0x73, + 0x62, 0x54, 0xcd, 0x0c, 0x5b, 0x44, 0xad, 0x59, 0xcd, 0x99, 0x45, 0x9c, 0x2c, 0xc5, 0xd6, 0x55, 0xd8, 0xf3, 0x1e, + 0x3f, 0xe5, 0x1f, 0xe6, 0x34, 0x57, 0x8f, 0x34, 0xd8, 0x17, 0x19, 0xbb, 0x0f, 0xae, 0x70, 0x5a, 0x6b, 0x30, 0x3d, + 0xe1, 0x6c, 0x2d, 0xae, 0xaf, 0xa6, 0xf0, 0x05, 0x69, 0x75, 0xcf, 0xa5, 0x88, 0x46, 0x37, 0xc9, 0xc4, 0x86, 0xa1, + 0xb5, 0xd9, 0x7d, 0x6d, 0xa1, 0xd1, 0x66, 0x05, 0xad, 0x59, 0xd9, 0xfd, 0xe6, 0x8d, 0x36, 0xb1, 0xc9, 0x9c, 0x05, + 0x99, 0xa8, 0xba, 0x09, 0xd2, 0xa6, 0xc0, 0x27, 0x27, 0x2b, 0x8c, 0x47, 0x20, 0x8b, 0xdc, 0xe6, 0x64, 0x7f, 0xe9, + 0xa8, 0x65, 0x54, 0x95, 0x10, 0x89, 0xcf, 0xca, 0x2d, 0xe4, 0x12, 0x74, 0xbc, 0x38, 0x10, 0xc1, 0xe5, 0x30, 0x2e, + 0x95, 0x9a, 0x46, 0xdb, 0x35, 0xda, 0x5b, 0xc8, 0x73, 0xa8, 0xcb, 0x4f, 0x83, 0x0d, 0x61, 0x88, 0x6a, 0xf4, 0xa1, + 0xcd, 0x3c, 0xbd, 0xa6, 0x4b, 0xfb, 0xf5, 0xf7, 0x01, 0x38, 0x7a, 0xb1, 0xbd, 0x90, 0xcc, 0x5d, 0x9f, 0x92, 0x48, + 0x20, 0x51, 0xf2, 0x05, 0xa0, 0x07, 0x80, 0x5e, 0xf5, 0x12, 0x56, 0x03, 0x06, 0xad, 0x54, 0x81, 0x9e, 0x29, 0x78, + 0x00, 0x32, 0x43, 0xcb, 0x41, 0xe5, 0x8f, 0x48, 0xf0, 0xb5, 0x43, 0xb2, 0x98, 0xf0, 0xd2, 0x50, 0xbc, 0x8e, 0x09, + 0xed, 0x7c, 0x98, 0x9a, 0x5e, 0x22, 0xf7, 0x14, 0x29, 0x1d, 0xb1, 0x45, 0x3f, 0xfd, 0xf4, 0xaa, 0xa7, 0x85, 0x93, + 0x3c, 0xb2, 0x7c, 0xac, 0xfd, 0x5b, 0xd6, 0xb6, 0xab, 0xea, 0x8f, 0x4c, 0x49, 0x1d, 0x68, 0x43, 0x28, 0xd7, 0x33, + 0x65, 0x4f, 0xe9, 0x2b, 0xd8, 0x59, 0x0c, 0x8b, 0x5e, 0xbb, 0xcf, 0x6a, 0x73, 0xf8, 0xd0, 0x45, 0x0f, 0x44, 0x13, + 0x6e, 0x5f, 0x23, 0x81, 0xe6, 0x12, 0xc1, 0x62, 0x78, 0x46, 0x97, 0x76, 0xe3, 0x43, 0x4e, 0x51, 0x10, 0xab, 0xc0, + 0x87, 0x74, 0xfd, 0x84, 0x86, 0x0c, 0x65, 0xbb, 0x8d, 0x02, 0x67, 0x35, 0xd0, 0x7c, 0x5f, 0xe3, 0xb0, 0x57, 0x27, + 0x60, 0x6d, 0xc9, 0x7c, 0xb5, 0x69, 0xa3, 0xd8, 0x6b, 0x2e, 0xaf, 0xf6, 0xda, 0x0a, 0x81, 0x3f, 0x17, 0x9f, 0xfd, + 0xed, 0x79, 0x52, 0x7d, 0x9f, 0x9f, 0x94, 0xde, 0xdb, 0xac, 0xfa, 0xa0, 0x35, 0xd8, 0xfb, 0xe3, 0x94, 0xf7, 0x91, + 0xe5, 0x30, 0x29, 0x3d, 0x1f, 0x8d, 0x6a, 0xb1, 0x7b, 0x4d, 0xe6, 0xf1, 0x61, 0x25, 0x54, 0xb3, 0xa9, 0x91, 0x07, + 0xf7, 0x5a, 0x73, 0xa1, 0xef, 0x51, 0xa0, 0xba, 0xd7, 0xc2, 0xa9, 0xba, 0x2a, 0x25, 0x88, 0xc9, 0xc8, 0x68, 0xa6, + 0xd9, 0x58, 0x6f, 0x03, 0xf3, 0x71, 0xaa, 0x5f, 0xf0, 0x27, 0x52, 0x72, 0xd8, 0xed, 0xac, 0x2c, 0x4a, 0xc5, 0x24, + 0x25, 0xa0, 0xc5, 0xf6, 0x6f, 0x71, 0x70, 0x60, 0x50, 0xb5, 0xea, 0x3c, 0x60, 0x24, 0xf6, 0xc5, 0xe2, 0x23, 0x50, + 0xf1, 0x5b, 0x3b, 0xc8, 0xec, 0x86, 0x8f, 0x65, 0x29, 0x2c, 0xfc, 0x20, 0x4a, 0xa5, 0x9e, 0x80, 0x40, 0x4d, 0x9d, + 0xbc, 0x29, 0x41, 0xb0, 0x7c, 0x33, 0xa7, 0x8d, 0xbd, 0x30, 0x5d, 0x1d, 0xc8, 0xb5, 0x69, 0x24, 0x86, 0x22, 0xfe, + 0xc9, 0xb1, 0xe1, 0x3a, 0x9a, 0xb0, 0xea, 0x89, 0xe5, 0x5e, 0x94, 0x07, 0xa1, 0x41, 0xe8, 0x90, 0xa7, 0xca, 0x6d, + 0x19, 0xd6, 0xe7, 0x2d, 0x2f, 0x4f, 0xfa, 0x17, 0x1e, 0x1f, 0x2c, 0x3a, 0x7f, 0x42, 0x33, 0x17, 0x02, 0x29, 0xa8, + 0x62, 0x93, 0xc2, 0x1d, 0xa1, 0x2a, 0xcb, 0x9d, 0x97, 0x15, 0xcd, 0x6b, 0x33, 0x0f, 0xd2, 0xd5, 0x47, 0x05, 0x99, + 0x4b, 0x28, 0x09, 0xa5, 0x2e, 0x60, 0x0a, 0xa3, 0x2c, 0xde, 0xe8, 0xbb, 0xf5, 0x0f, 0xbb, 0x94, 0x84, 0x03, 0x3e, + 0x86, 0xc1, 0x4c, 0xe0, 0xdf, 0x0f, 0x29, 0x0d, 0xdc, 0xd4, 0xba, 0x16, 0xca, 0x18, 0xd2, 0x0a, 0xc1, 0x7c, 0x24, + 0xd1, 0x60, 0x82, 0xef, 0x3b, 0x83, 0x22, 0x27, 0x05, 0x2b, 0x8d, 0xdf, 0x8c, 0x7b, 0x0c, 0x1d, 0x67, 0xc6, 0x3b, + 0x3b, 0x5d, 0xb1, 0xb7, 0xe6, 0xb8, 0x3a, 0x84, 0x80, 0xcb, 0xb1, 0xdc, 0xca, 0xba, 0x20, 0xeb, 0x18, 0xf2, 0x2c, + 0xdc, 0x22, 0x71, 0xc9, 0x08, 0x3d, 0xa5, 0x43, 0x23, 0x95, 0x61, 0x09, 0x4e, 0x9b, 0xe1, 0x03, 0xdb, 0xb8, 0x82, + 0xba, 0x9d, 0x9d, 0x06, 0xea, 0xf6, 0x0a, 0x78, 0xb0, 0x6b, 0x42, 0x89, 0xd2, 0xc8, 0xaa, 0x80, 0x06, 0x23, 0xa0, + 0x2d, 0x0b, 0x94, 0x6a, 0x22, 0x26, 0x1a, 0x85, 0x51, 0x22, 0xb5, 0x94, 0xb2, 0xa3, 0xe9, 0x77, 0x5d, 0x24, 0x93, + 0x64, 0x1d, 0x8a, 0x83, 0x9e, 0x98, 0x24, 0xb5, 0x5a, 0x97, 0x2d, 0x3e, 0x1c, 0x88, 0xfd, 0x22, 0x95, 0x9e, 0xd8, + 0xdb, 0x69, 0x81, 0xdc, 0xec, 0x7b, 0x1a, 0x52, 0x43, 0xa3, 0xb3, 0xad, 0xd1, 0x79, 0x79, 0x2a, 0x9b, 0x1f, 0x74, + 0xd4, 0x72, 0xeb, 0xc6, 0x98, 0xa2, 0x0a, 0xa8, 0x3f, 0xd6, 0x82, 0xf4, 0xfd, 0x4b, 0xa1, 0x4e, 0x50, 0x34, 0x4c, + 0xed, 0x7b, 0x2c, 0x46, 0xba, 0x4e, 0xf3, 0x48, 0x48, 0x70, 0xef, 0x09, 0x02, 0x3c, 0x22, 0x4f, 0x23, 0x19, 0xd3, + 0x09, 0xc2, 0x10, 0x91, 0x75, 0xb2, 0xe6, 0x7d, 0x6e, 0xfd, 0xfe, 0x92, 0xbc, 0xef, 0xe2, 0x06, 0x93, 0xab, 0xfd, + 0x94, 0xde, 0xfb, 0xed, 0x76, 0x68, 0xed, 0x71, 0x12, 0x37, 0xe3, 0x85, 0xa5, 0xf6, 0x58, 0xd8, 0xff, 0x66, 0xf3, + 0xa9, 0x53, 0xa5, 0xb7, 0x6b, 0x0d, 0x69, 0x3c, 0xb3, 0xc6, 0x66, 0x3f, 0x09, 0xda, 0x91, 0x0b, 0xb4, 0x13, 0x3b, + 0x39, 0xab, 0x20, 0xa1, 0x21, 0x31, 0xa6, 0xb6, 0x73, 0x08, 0xd0, 0x8c, 0x75, 0xe6, 0xf6, 0xad, 0xf6, 0xed, 0x29, + 0x27, 0x65, 0x80, 0xf2, 0x52, 0xf8, 0x67, 0xdb, 0x49, 0x89, 0x7d, 0x1c, 0x63, 0x6c, 0x05, 0xf1, 0x21, 0x81, 0x54, + 0x05, 0x13, 0x5a, 0x4d, 0x1e, 0xd0, 0xc5, 0x29, 0x1d, 0x7f, 0xa6, 0x1f, 0x3e, 0xc0, 0xea, 0x6b, 0x1e, 0xd9, 0x66, + 0x0f, 0x1c, 0x63, 0x4a, 0xbd, 0xce, 0x0e, 0x58, 0x3f, 0xa5, 0xf7, 0xba, 0x58, 0x1b, 0x43, 0xca, 0x96, 0x5c, 0xbb, + 0xb6, 0x08, 0x99, 0x30, 0x64, 0x5d, 0x47, 0x28, 0xac, 0xe0, 0xfc, 0x86, 0x9c, 0xc0, 0xea, 0xfd, 0x9c, 0x2b, 0xf5, + 0x2c, 0x52, 0xb3, 0x4c, 0xd0, 0xce, 0x8e, 0x1c, 0xe9, 0x3c, 0xa9, 0xff, 0x6f, 0x25, 0x84, 0xe0, 0xd2, 0x9a, 0x6e, + 0x4b, 0xa8, 0x93, 0xfc, 0xe4, 0x2a, 0x5a, 0xc0, 0x73, 0x37, 0xca, 0x1f, 0xc9, 0xea, 0x6d, 0x82, 0x67, 0x83, 0x48, + 0x60, 0xc3, 0x72, 0x4a, 0x54, 0xc3, 0x6a, 0xab, 0x5b, 0xf8, 0xee, 0xd1, 0xed, 0x8d, 0x62, 0x0c, 0x15, 0x4e, 0x7e, + 0x0e, 0x94, 0x54, 0xdc, 0xeb, 0x92, 0x5a, 0x47, 0xe5, 0x7f, 0xa3, 0xb8, 0xc2, 0x49, 0x7c, 0x73, 0x93, 0xb3, 0x81, + 0x47, 0xdd, 0x53, 0x43, 0xb2, 0xbf, 0x5f, 0xa8, 0x10, 0x6d, 0xb4, 0x8e, 0x19, 0xa0, 0x0a, 0x1f, 0x41, 0x2e, 0x47, + 0xbe, 0x9f, 0x75, 0xe5, 0x17, 0xf9, 0xa5, 0x6f, 0xcf, 0x0d, 0x62, 0xcd, 0x5c, 0xa8, 0x59, 0xca, 0x28, 0xbf, 0x0c, + 0x6f, 0xe2, 0xb6, 0xc8, 0x20, 0xab, 0xcf, 0x6b, 0xec, 0x1d, 0x62, 0xe5, 0xd8, 0x6d, 0x4f, 0x58, 0x41, 0x4c, 0x90, + 0x2e, 0xc1, 0x53, 0x5d, 0x50, 0xc4, 0x28, 0x35, 0x67, 0x38, 0xd5, 0xa2, 0xba, 0x50, 0xce, 0xd5, 0x7a, 0x49, 0x05, + 0x84, 0xea, 0x7b, 0x2a, 0xe7, 0x25, 0x30, 0xec, 0x9d, 0xc7, 0x7e, 0xb0, 0x3c, 0x6f, 0xea, 0x5a, 0x99, 0x9d, 0xa6, + 0xeb, 0x1e, 0x2a, 0x1c, 0x68, 0x53, 0x7a, 0x4b, 0x57, 0xf3, 0x7c, 0xad, 0x16, 0xf8, 0x6d, 0x68, 0xc1, 0x33, 0xe7, + 0x13, 0xd0, 0x57, 0xc9, 0x23, 0x89, 0x3b, 0x4b, 0xd7, 0xae, 0x80, 0x16, 0x26, 0x93, 0xc0, 0x83, 0xd3, 0x7d, 0xad, + 0x92, 0xb5, 0x91, 0x70, 0x4c, 0x08, 0x03, 0x72, 0xd6, 0x07, 0xdb, 0x6e, 0x8c, 0x5c, 0xa2, 0xf6, 0xfa, 0x91, 0x86, + 0x16, 0x59, 0x3f, 0x68, 0xd2, 0xf3, 0x40, 0x51, 0x39, 0xaa, 0xde, 0xdc, 0x29, 0xa3, 0x87, 0x98, 0x27, 0x8c, 0xda, + 0xc4, 0xa0, 0x91, 0x1e, 0xa8, 0x33, 0x42, 0xce, 0x4f, 0x6c, 0x52, 0x7d, 0x8d, 0x0f, 0x9f, 0x09, 0x61, 0xac, 0x36, + 0x0d, 0xf9, 0x3c, 0x81, 0xf6, 0x6c, 0xe9, 0xb8, 0x53, 0x43, 0x86, 0xd7, 0xa6, 0xcb, 0x21, 0x19, 0x0b, 0x2e, 0x9b, + 0x21, 0x0c, 0x6a, 0x25, 0xe3, 0x34, 0xb1, 0xcf, 0xa9, 0x1b, 0x49, 0x57, 0xe5, 0x1a, 0x02, 0x1c, 0x77, 0x9c, 0x49, + 0xb3, 0xd8, 0x72, 0x8b, 0x92, 0xab, 0x4b, 0x4d, 0x88, 0x2d, 0x9a, 0x88, 0x12, 0x00, 0x7a, 0x39, 0xec, 0x23, 0x20, + 0xe1, 0xdb, 0x0a, 0xe7, 0xe6, 0x89, 0x2d, 0xad, 0x5c, 0x73, 0x41, 0x61, 0xb8, 0xa3, 0xaf, 0xf7, 0x62, 0x53, 0x11, + 0x7b, 0x06, 0xf3, 0xd0, 0x6c, 0x2c, 0xb3, 0xf9, 0x23, 0xdf, 0x9f, 0x87, 0x66, 0x20, 0xfd, 0x03, 0x16, 0xc4, 0x7f, + 0x0d, 0x15, 0xe2, 0x19, 0x17, 0xe4, 0x0f, 0xb4, 0x92, 0x86, 0x2f, 0x58, 0xb7, 0xd3, 0x95, 0x9f, 0x4d, 0x9f, 0xaa, + 0x05, 0x04, 0xe5, 0x81, 0x5c, 0x48, 0x73, 0x03, 0x6b, 0xbc, 0xc1, 0x8a, 0xf5, 0xc6, 0x0e, 0x49, 0x60, 0xeb, 0xe9, + 0x48, 0x26, 0x8d, 0x74, 0x8a, 0x07, 0xbe, 0xd5, 0xb1, 0xfd, 0xad, 0xce, 0x29, 0xbd, 0x29, 0x4f, 0x9b, 0xe6, 0xad, + 0x78, 0xe8, 0x59, 0x5b, 0x45, 0x98, 0x30, 0x78, 0x2a, 0x9c, 0xf0, 0x7a, 0x2f, 0x57, 0xd9, 0x35, 0x7c, 0x06, 0x3f, + 0xf4, 0x6c, 0x30, 0x17, 0x36, 0xd7, 0x22, 0x41, 0x07, 0x61, 0xbc, 0xf1, 0xf9, 0x11, 0x46, 0xa6, 0x4b, 0xe9, 0x15, + 0xfd, 0x68, 0x90, 0x28, 0xde, 0xae, 0xbf, 0xdd, 0x7d, 0x8f, 0xe0, 0xe0, 0xde, 0x82, 0x6c, 0x4c, 0x9b, 0xbd, 0x61, + 0x0f, 0x69, 0x51, 0xd5, 0x18, 0x23, 0xa4, 0x42, 0x1c, 0x43, 0xc4, 0xe5, 0xf6, 0x55, 0x5b, 0x1e, 0xdc, 0xf2, 0x4b, + 0x9e, 0x51, 0xf8, 0x28, 0xfe, 0xce, 0x7c, 0xd7, 0x47, 0xe8, 0x8a, 0xeb, 0x3c, 0x87, 0xf8, 0xda, 0x6f, 0xaf, 0x91, + 0x10, 0x25, 0xe1, 0x7f, 0x06, 0x0f, 0x30, 0x33, 0x5e, 0xac, 0x01, 0x7b, 0x5e, 0xdd, 0xc8, 0x49, 0x70, 0x5f, 0x30, + 0xf4, 0xb6, 0xf9, 0x42, 0x3f, 0x9e, 0x92, 0x78, 0x8b, 0xb6, 0x88, 0x5d, 0xa9, 0x83, 0x19, 0x3b, 0x71, 0xcd, 0x87, + 0xc9, 0xec, 0x3f, 0x46, 0x58, 0x00, 0x84, 0x82, 0x5a, 0x0b, 0x3f, 0x6d, 0x05, 0x70, 0xab, 0xff, 0x60, 0xa4, 0xc0, + 0x4d, 0xf4, 0xc4, 0xcf, 0x76, 0x4f, 0xb0, 0x09, 0x4e, 0xc4, 0x5e, 0x91, 0xb6, 0xe7, 0x40, 0xaf, 0x56, 0x35, 0x84, + 0xea, 0xd6, 0xe9, 0x20, 0x74, 0xb1, 0x28, 0x8c, 0xf5, 0x3a, 0x0a, 0x6c, 0x56, 0x2d, 0xab, 0x0e, 0x43, 0x6d, 0x57, + 0xa1, 0xf6, 0x24, 0x1b, 0x16, 0x25, 0x2a, 0x72, 0xe3, 0x78, 0x53, 0xac, 0x03, 0xea, 0xd7, 0x7e, 0x6d, 0x82, 0x5b, + 0x2f, 0x78, 0x74, 0x2c, 0xc8, 0xd5, 0x14, 0x31, 0x78, 0x81, 0xc8, 0xe0, 0x55, 0x59, 0xa0, 0x93, 0x5e, 0xb8, 0xef, + 0x9b, 0x4f, 0x75, 0x61, 0xe9, 0x6e, 0x1a, 0x3e, 0xfb, 0x79, 0xf4, 0xab, 0xe1, 0xeb, 0x25, 0x63, 0x64, 0x5c, 0x24, + 0x2d, 0x7a, 0xea, 0x1c, 0x97, 0x6b, 0x30, 0x7b, 0x68, 0x75, 0xcc, 0xb0, 0xfb, 0x74, 0xa5, 0xc5, 0x18, 0xbf, 0x13, + 0xc5, 0xb4, 0x07, 0xcb, 0x32, 0x13, 0xf7, 0xf4, 0x82, 0x00, 0x69, 0x2d, 0xf1, 0xa6, 0xd5, 0x5b, 0x6d, 0x7d, 0x36, + 0x2d, 0x83, 0xe8, 0x1b, 0x8b, 0x4c, 0xdd, 0x2c, 0x64, 0xb9, 0x4c, 0xb1, 0x46, 0xab, 0xb0, 0x2f, 0x97, 0x47, 0x37, + 0x7d, 0x5d, 0x1a, 0xff, 0x16, 0x55, 0x4f, 0x86, 0x44, 0xd2, 0x12, 0xa5, 0x52, 0x81, 0x93, 0x2e, 0xec, 0x62, 0x4d, + 0x47, 0x2d, 0xd7, 0x89, 0x33, 0xde, 0x8f, 0x97, 0x0e, 0xcb, 0x1f, 0x9f, 0x0b, 0x42, 0xad, 0xfc, 0x3f, 0x10, 0xfb, + 0xec, 0x70, 0x32, 0xa0, 0x9c, 0xc2, 0x19, 0xd9, 0xfd, 0x0f, 0xba, 0xda, 0x15, 0x40, 0xcd, 0x30, 0x7a, 0xb9, 0x54, + 0x38, 0x54, 0x94, 0x7e, 0x3a, 0xe9, 0xc6, 0x50, 0x58, 0x5f, 0xad, 0x85, 0xd7, 0x5e, 0x52, 0xd1, 0x25, 0xfe, 0x4a, + 0xfa, 0x98, 0x70, 0x2a, 0x65, 0x87, 0xfa, 0xaa, 0x21, 0x01, 0xa0, 0x43, 0xbc, 0x12, 0x01, 0x37, 0xf3, 0x16, 0x34, + 0x99, 0xc8, 0xb8, 0xf8, 0xe0, 0x02, 0xb8, 0x30, 0xde, 0x3e, 0xcd, 0x40, 0xb2, 0xd6, 0x12, 0x3b, 0x09, 0xdd, 0xf4, + 0x31, 0x61, 0x04, 0x48, 0xb0, 0xe3, 0x01, 0x34, 0x79, 0x27, 0xbc, 0xc7, 0x7a, 0x35, 0x31, 0x05, 0x41, 0x44, 0xf7, + 0x9e, 0x83, 0xdd, 0x5c, 0xcb, 0x6a, 0x85, 0x4d, 0x88, 0xcd, 0x8e, 0xaa, 0xef, 0xa7, 0x0a, 0xbc, 0x5e, 0x98, 0x54, + 0x6c, 0x14, 0xba, 0x4e, 0x1e, 0x68, 0x1c, 0x60, 0x3a, 0x4b, 0x0e, 0x35, 0x5c, 0xf9, 0x50, 0x96, 0x93, 0x94, 0xd0, + 0x52, 0x38, 0xe0, 0x0c, 0x24, 0x07, 0xff, 0x63, 0x41, 0x03, 0x59, 0x87, 0x9f, 0x18, 0xd7, 0xe0, 0x5f, 0x48, 0x6b, + 0x9a, 0x16, 0xd1, 0x6a, 0xaf, 0x61, 0x0d, 0x9a, 0x97, 0xc9, 0x97, 0x13, 0x03, 0xd8, 0xac, 0x16, 0xb2, 0xfa, 0xb1, + 0xe7, 0x9a, 0x3f, 0x52, 0x7e, 0xca, 0x42, 0xed, 0xa9, 0x9e, 0xb6, 0x42, 0xb2, 0xd3, 0xb4, 0xa8, 0x88, 0xe2, 0x7a, + 0xb2, 0x5d, 0x17, 0x2f, 0xbe, 0x88, 0x04, 0x7e, 0x31, 0x81, 0x18, 0x12, 0x40, 0x60, 0x70, 0x04, 0x35, 0x24, 0x74, + 0xd4, 0xd7, 0x9b, 0xc7, 0x57, 0x15, 0x04, 0xcd, 0x63, 0xa6, 0x80, 0x98, 0xae, 0x98, 0x9d, 0xbf, 0x04, 0x5a, 0xf1, + 0xfe, 0x0d, 0xd6, 0x55, 0xcd, 0x9f, 0x37, 0x69, 0xe3, 0x17, 0xd6, 0x7f, 0xd4, 0xb1, 0x2a, 0xb0, 0x21, 0x36, 0xa8, + 0x52, 0x24, 0xac, 0x32, 0x06, 0x88, 0x46, 0xcf, 0x5c, 0x45, 0x9a, 0xc2, 0xfe, 0xee, 0x3c, 0x1e, 0xd4, 0x3a, 0xb5, + 0xf9, 0xa6, 0xe7, 0x52, 0x62, 0x09, 0x97, 0x99, 0xe9, 0x73, 0x39, 0x00, 0x32, 0xd3, 0x83, 0xdc, 0x40, 0x83, 0xaf, + 0xc1, 0xab, 0x2b, 0xe6, 0x2c, 0x3d, 0xbb, 0x1f, 0x36, 0x7e, 0x7f, 0x95, 0x5e, 0xd1, 0x3b, 0x18, 0x99, 0x6f, 0xee, + 0xf5, 0xee, 0x5a, 0x5d, 0xbf, 0xb0, 0x98, 0x51, 0x97, 0xaa, 0xe5, 0xe9, 0xe7, 0xed, 0xbe, 0x2f, 0x1e, 0xac, 0xfd, + 0x29, 0x28, 0x63, 0x7b, 0x92, 0x77, 0xad, 0xe4, 0xc6, 0xbf, 0x40, 0xd3, 0xaa, 0xa0, 0x96, 0x91, 0x29, 0x6f, 0x6b, + 0xbf, 0xe5, 0xba, 0xbc, 0x3d, 0x91, 0x71, 0xc4, 0xb9, 0x63, 0xc8, 0xfb, 0xd2, 0x36, 0x3e, 0xf7, 0x1a, 0x02, 0x85, + 0x5f, 0x9e, 0x4e, 0x29, 0x68, 0x6b, 0xc2, 0x25, 0xe2, 0x0c, 0x2d, 0xaf, 0x4b, 0x37, 0xc5, 0x20, 0x72, 0xf4, 0x81, + 0xdd, 0xd2, 0x86, 0xe0, 0xdb, 0x22, 0xfc, 0x6c, 0x26, 0xd4, 0x93, 0xad, 0x40, 0xad, 0x88, 0x2a, 0x7b, 0x88, 0x16, + 0x02, 0xcb, 0x89, 0xe4, 0xa4, 0x37, 0x75, 0x26, 0x90, 0x60, 0xea, 0x15, 0x6f, 0xbb, 0x60, 0xc8, 0x62, 0x97, 0x2b, + 0x0c, 0x2c, 0xa2, 0x64, 0x2a, 0x7e, 0xbd, 0x3c, 0x95, 0x46, 0x0b, 0x0c, 0x01, 0x4c, 0x73, 0x2f, 0x2f, 0x1a, 0x03, + 0xee, 0xfe, 0xee, 0x46, 0x9a, 0x6e, 0x48, 0xe0, 0x9b, 0x67, 0xf3, 0x5e, 0x4a, 0x06, 0x7a, 0x6e, 0xf2, 0xeb, 0x49, + 0xda, 0x89, 0x9c, 0x93, 0xda, 0x9c, 0xe1, 0x10, 0xa0, 0xaa, 0xd9, 0x43, 0x9a, 0x56, 0xa5, 0xec, 0xc4, 0x25, 0x90, + 0xe5, 0x37, 0x11, 0xf8, 0xf2, 0xcb, 0x63, 0xec, 0x9d, 0x8a, 0xcc, 0x14, 0x61, 0x4f, 0x94, 0x4f, 0x1b, 0x56, 0x77, + 0xf3, 0xf0, 0x34, 0x47, 0xb0, 0xf3, 0x87, 0x69, 0xdc, 0xd7, 0x0d, 0xcf, 0x00, 0x30, 0x03, 0xe1, 0x13, 0x82, 0x4f, + 0x30, 0x44, 0x33, 0xdd, 0xdc, 0x76, 0x1f, 0x55, 0xa5, 0xaa, 0x78, 0x0a, 0x70, 0x7c, 0x82, 0xe1, 0x9d, 0xa9, 0xc7, + 0x66, 0x09, 0x36, 0xcf, 0x23, 0x30, 0x84, 0xdc, 0x34, 0xa7, 0x9a, 0x72, 0x03, 0xe4, 0xbb, 0x88, 0x61, 0x8a, 0x67, + 0xb1, 0x47, 0xc3, 0x07, 0xd4, 0x2b, 0x6f, 0xee, 0xbc, 0xc0, 0x6f, 0xb3, 0x88, 0x65, 0xcf, 0x93, 0x51, 0x06, 0x9f, + 0x88, 0x7c, 0x8b, 0x14, 0x32, 0xf7, 0x83, 0xa6, 0xb0, 0xda, 0xa6, 0xf5, 0x33, 0x20, 0x72, 0x73, 0x75, 0x63, 0xa2, + 0x35, 0x70, 0xa1, 0x37, 0x51, 0x5d, 0x40, 0x6b, 0x9b, 0xf5, 0xe1, 0x66, 0x57, 0x22, 0x19, 0x3c, 0x10, 0xe6, 0xdf, + 0x78, 0xf1, 0x60, 0xf2, 0x2d, 0xe4, 0xc9, 0xf0, 0x91, 0x87, 0xd3, 0xbd, 0xb5, 0xe7, 0xad, 0xfb, 0x96, 0xbb, 0x6a, + 0x4d, 0x9e, 0xd3, 0x22, 0x94, 0xd8, 0x49, 0x06, 0x70, 0x04, 0x1f, 0x9b, 0xb1, 0xee, 0x03, 0xd4, 0x89, 0x0c, 0x2e, + 0x54, 0x31, 0xe3, 0xcc, 0x38, 0xca, 0xf2, 0x2b, 0xae, 0x39, 0xb8, 0xfd, 0xbc, 0x72, 0x31, 0x10, 0xb0, 0xd0, 0x81, + 0x32, 0xf5, 0x47, 0x32, 0xb5, 0x35, 0x4d, 0x8e, 0xf9, 0x19, 0x2c, 0x10, 0x19, 0x05, 0x01, 0xc8, 0xc2, 0xd3, 0xb6, + 0x4a, 0xf7, 0xf1, 0xa0, 0x1b, 0x50, 0xde, 0x08, 0xcc, 0xc8, 0xa0, 0x43, 0x30, 0x63, 0x6d, 0x67, 0x22, 0x11, 0x61, + 0x12, 0xae, 0x2c, 0x6a, 0xf8, 0x17, 0x4f, 0x49, 0xf9, 0x98, 0x87, 0xbe, 0x20, 0x8c, 0x8b, 0x79, 0x45, 0xe1, 0x90, + 0x82, 0x74, 0x2e, 0xae, 0xbe, 0x65, 0x99, 0x9c, 0x53, 0x2f, 0x43, 0xa1, 0x8b, 0x84, 0x51, 0x66, 0x93, 0x7a, 0x22, + 0x03, 0x48, 0xc6, 0x2a, 0x33, 0x94, 0x2b, 0xbc, 0x1e, 0x55, 0x72, 0x51, 0xf3, 0x6f, 0xcc, 0xca, 0xb8, 0x1c, 0x5b, + 0xd6, 0x0d, 0xeb, 0x0c, 0x8e, 0x57, 0xaa, 0x65, 0xf2, 0x4d, 0x51, 0x9c, 0x78, 0xf1, 0x19, 0x03, 0xf1, 0x7e, 0x56, + 0x6f, 0xb3, 0x9b, 0x43, 0x5c, 0xee, 0xda, 0xc2, 0x95, 0x49, 0xc5, 0x20, 0x96, 0x30, 0x11, 0xb4, 0x28, 0x8d, 0x3f, + 0x72, 0x30, 0xc5, 0x29, 0x40, 0x1b, 0x0b, 0x3f, 0x19, 0x49, 0x55, 0xe5, 0xb0, 0x5c, 0x46, 0x6f, 0xa5, 0xa8, 0xb1, + 0x59, 0x5e, 0x46, 0x9b, 0x79, 0x12, 0x10, 0xe0, 0xea, 0x4a, 0x59, 0xcd, 0xae, 0x4f, 0x1d, 0xb6, 0x67, 0x5c, 0x59, + 0xca, 0x09, 0x53, 0x34, 0x6b, 0x2c, 0x25, 0xc2, 0xb8, 0xcd, 0xc5, 0xb6, 0x38, 0x7e, 0x57, 0xf3, 0x97, 0xd2, 0x6f, + 0xe0, 0x2e, 0x77, 0x4d, 0x01, 0x6e, 0x91, 0x47, 0xf4, 0x8e, 0x5c, 0x06, 0x7c, 0x67, 0x54, 0x6f, 0xd0, 0x80, 0x2d, + 0x5a, 0x6e, 0xcd, 0xc7, 0xb2, 0x3c, 0xf4, 0x55, 0x74, 0xe1, 0x62, 0x11, 0xd1, 0xea, 0x50, 0xeb, 0xfd, 0xde, 0xfe, + 0xd3, 0x5e, 0xb5, 0xd3, 0x80, 0x0e, 0x28, 0x7d, 0xad, 0xd3, 0xdb, 0x2e, 0xff, 0xab, 0x1f, 0x6e, 0x8b, 0x44, 0x9f, + 0x97, 0xd4, 0x0d, 0x74, 0x08, 0x72, 0x07, 0x82, 0xad, 0x74, 0x3d, 0x67, 0x8e, 0x83, 0x5e, 0x58, 0x12, 0x6a, 0xe1, + 0x75, 0x79, 0x1b, 0x04, 0x0f, 0xa6, 0x94, 0xc4, 0x1a, 0x8f, 0xaa, 0x39, 0x0c, 0xe8, 0xc3, 0x2d, 0xd6, 0x6a, 0x62, + 0xfa, 0x13, 0xa2, 0xca, 0x44, 0x7a, 0x60, 0x7b, 0xd1, 0xc4, 0x84, 0x87, 0xfd, 0xa0, 0x24, 0x25, 0x54, 0x07, 0x82, + 0x36, 0x50, 0x26, 0xd6, 0xf1, 0x65, 0x87, 0x82, 0xe7, 0x42, 0x0b, 0x6c, 0x62, 0xb0, 0xef, 0xb8, 0x18, 0x12, 0x15, + 0x3b, 0xa4, 0xd4, 0x63, 0xa4, 0x76, 0x87, 0x2d, 0x62, 0x7f, 0x52, 0x0d, 0x94, 0xfe, 0x6e, 0xdc, 0xf7, 0xad, 0x15, + 0x40, 0xa9, 0x6b, 0x7e, 0xdc, 0xf7, 0x28, 0xf6, 0x60, 0x11, 0xbf, 0x0e, 0xc1, 0x99, 0x6c, 0xd7, 0x54, 0xc4, 0x9a, + 0xcf, 0x92, 0x3d, 0x37, 0x6c, 0xf8, 0xfb, 0x8a, 0x40, 0xc6, 0x48, 0xd3, 0xa1, 0x8c, 0xcd, 0xf8, 0x59, 0x46, 0x31, + 0x45, 0xd8, 0x17, 0x7e, 0x27, 0x09, 0x11, 0x22, 0x64, 0x0c, 0xd3, 0x1c, 0x41, 0x3b, 0xf3, 0x79, 0x52, 0x0b, 0x54, + 0xd7, 0x24, 0xf4, 0x3d, 0xdd, 0x1d, 0x88, 0x07, 0x39, 0x7a, 0x54, 0x02, 0xa0, 0xff, 0x5b, 0x3c, 0x7b, 0x72, 0xce, + 0x18, 0xc1, 0x5a, 0x71, 0x22, 0x8d, 0x2b, 0x70, 0x9c, 0xe3, 0x93, 0x16, 0x12, 0xc4, 0x4b, 0x75, 0x27, 0xa1, 0x4f, + 0xda, 0x38, 0x35, 0x78, 0x82, 0x5c, 0x14, 0x2b, 0x15, 0x80, 0xda, 0x2d, 0x78, 0xb3, 0x84, 0x19, 0x33, 0xa4, 0x47, + 0xde, 0x83, 0x35, 0x0f, 0x75, 0x29, 0x97, 0xc7, 0x9c, 0x9c, 0x21, 0x6a, 0x2e, 0xf2, 0xa4, 0xc6, 0x5c, 0x41, 0x5f, + 0x83, 0xe2, 0x14, 0xda, 0x18, 0x13, 0xab, 0xcd, 0x53, 0x9f, 0xaa, 0xa1, 0x28, 0x3d, 0x9b, 0xe5, 0xc5, 0x3a, 0xe2, + 0x12, 0xd8, 0x85, 0x66, 0xf4, 0xc1, 0xaf, 0x64, 0x92, 0xc3, 0x41, 0x9a, 0x27, 0x82, 0x8e, 0xf2, 0xc1, 0xd0, 0xc9, + 0x8c, 0xf6, 0x2e, 0x3d, 0x62, 0x47, 0x0f, 0x25, 0xa7, 0x2f, 0x50, 0x7a, 0x08, 0x01, 0xfa, 0xab, 0xe1, 0x4d, 0xdb, + 0x5f, 0xd1, 0x49, 0xf1, 0x62, 0xc2, 0x3b, 0x49, 0x14, 0xe1, 0x21, 0x9c, 0x11, 0x85, 0x8c, 0x44, 0xfb, 0x60, 0x30, + 0xf3, 0xce, 0xb6, 0x35, 0xe5, 0x7d, 0x51, 0xa7, 0x4e, 0x73, 0xf0, 0xf4, 0xbd, 0x78, 0x2d, 0x37, 0x0f, 0x02, 0x7a, + 0xec, 0xcb, 0x96, 0x90, 0x9d, 0x27, 0x03, 0x08, 0x90, 0x2f, 0x76, 0xc8, 0x98, 0x20, 0x0d, 0x6b, 0x5a, 0x92, 0x35, + 0xfd, 0x68, 0x11, 0xfa, 0xa7, 0xea, 0xe3, 0x34, 0xcb, 0x84, 0x50, 0x5b, 0x18, 0x03, 0x22, 0xf4, 0x94, 0x93, 0x82, + 0x15, 0xb9, 0x0f, 0x5e, 0x52, 0x38, 0x1c, 0xac, 0xd7, 0xc5, 0xf0, 0xa4, 0x39, 0x1b, 0x02, 0xdb, 0x31, 0x01, 0x9d, + 0x66, 0x48, 0x14, 0x62, 0xc3, 0x7d, 0x8c, 0x66, 0x92, 0x0a, 0xc6, 0x34, 0x51, 0xf9, 0xd0, 0x3f, 0xa8, 0x8d, 0xb8, + 0x49, 0x3d, 0x8a, 0x87, 0x11, 0xf6, 0x1c, 0x87, 0xae, 0x13, 0xcb, 0x80, 0xa8, 0xb2, 0xa4, 0xb2, 0xe6, 0x7a, 0xd4, + 0x34, 0x23, 0x83, 0x2a, 0x91, 0xfa, 0x45, 0x5b, 0x07, 0x97, 0x06, 0xd4, 0xb3, 0xf8, 0x66, 0xe0, 0xb9, 0x25, 0xb4, + 0xdc, 0x9f, 0x23, 0x89, 0x27, 0x83, 0x51, 0x8f, 0xe6, 0x08, 0x2f, 0xdd, 0x1d, 0x02, 0xe0, 0xad, 0xf2, 0x76, 0xd5, + 0xf3, 0xef, 0x28, 0x63, 0x27, 0x6e, 0xaa, 0xad, 0x52, 0x92, 0x5a, 0x83, 0x12, 0xf3, 0xef, 0xf2, 0xc7, 0x38, 0x77, + 0x15, 0x0b, 0xee, 0xbd, 0xa7, 0x6b, 0x85, 0xfa, 0xd3, 0x27, 0xb2, 0x93, 0xc2, 0x8d, 0xd3, 0x1b, 0x44, 0xe6, 0xe1, + 0x23, 0x6a, 0xc1, 0x5c, 0xe0, 0xee, 0xb8, 0xa8, 0x7b, 0xf3, 0x37, 0x84, 0x9b, 0xa2, 0xa6, 0xd0, 0x85, 0x92, 0x8d, + 0x16, 0x5f, 0xc9, 0xcc, 0x00, 0xcd, 0xe5, 0x4a, 0x2d, 0x3c, 0x67, 0x3d, 0x50, 0xfb, 0x15, 0x89, 0x5b, 0xeb, 0xf5, + 0xb5, 0x5b, 0xdb, 0x43, 0xb8, 0x9a, 0x2c, 0xa8, 0x63, 0x24, 0x79, 0xcc, 0x1c, 0x5a, 0x2b, 0x32, 0x5d, 0x93, 0x84, + 0xe6, 0x92, 0x5a, 0xaf, 0x2e, 0x1a, 0x7e, 0xfe, 0xda, 0x44, 0x10, 0x13, 0x46, 0x56, 0x2b, 0xe8, 0x1d, 0xb6, 0x9b, + 0x5f, 0x2c, 0x5c, 0x6d, 0x52, 0xa6, 0xc2, 0x21, 0x50, 0x9b, 0x2c, 0x3f, 0xc7, 0xd2, 0x53, 0x14, 0x44, 0xea, 0xb4, + 0xd5, 0x55, 0x42, 0x42, 0xb0, 0x52, 0xa9, 0x7f, 0x1d, 0x98, 0x90, 0x23, 0x2a, 0x47, 0x64, 0xf7, 0xba, 0x9c, 0xf3, + 0x53, 0x03, 0xd2, 0xdd, 0x88, 0x48, 0xc8, 0xe9, 0x8d, 0x01, 0x5d, 0x16, 0x1a, 0xfb, 0xdb, 0x80, 0x2b, 0x7c, 0x88, + 0xd0, 0xe9, 0xd8, 0x95, 0x72, 0x5d, 0x84, 0xfb, 0xbe, 0x40, 0x8a, 0xaa, 0x22, 0x82, 0x05, 0xd5, 0x8e, 0x6c, 0xce, + 0x8e, 0xfc, 0xc6, 0x1a, 0x1c, 0xce, 0xcd, 0xf1, 0xae, 0x51, 0x84, 0xd2, 0xc5, 0xce, 0xe3, 0x40, 0x4f, 0x94, 0x24, + 0x7c, 0x77, 0x8c, 0xd0, 0x5a, 0xeb, 0xfc, 0xac, 0xfb, 0x01, 0xcf, 0x92, 0x70, 0xfe, 0x81, 0x4d, 0xde, 0x97, 0xe4, + 0xbc, 0xbc, 0xda, 0xd4, 0x6d, 0xc1, 0x08, 0x40, 0x7d, 0xe3, 0x79, 0x5b, 0x79, 0x70, 0x83, 0x91, 0x41, 0x9e, 0xcc, + 0x09, 0xc6, 0x33, 0x57, 0x83, 0x79, 0x76, 0xec, 0x2c, 0xef, 0xb1, 0x10, 0xc8, 0x53, 0x4d, 0x6d, 0x5a, 0x2b, 0xb1, + 0x45, 0x3b, 0x66, 0xbf, 0x65, 0x03, 0x9c, 0x00, 0xa7, 0xc3, 0xf1, 0xd2, 0x36, 0xf8, 0x40, 0x2e, 0xe9, 0xad, 0x65, + 0x14, 0x64, 0x17, 0xfe, 0x6d, 0xa8, 0x8f, 0x28, 0xaf, 0x40, 0xa8, 0x48, 0xea, 0xd8, 0x28, 0x29, 0x45, 0xa9, 0x11, + 0x5a, 0x66, 0x5b, 0x90, 0x15, 0x67, 0x7b, 0xc4, 0xa3, 0x66, 0x86, 0x87, 0x22, 0xb7, 0x45, 0x3a, 0x6b, 0xb8, 0x2f, + 0x05, 0x2a, 0x36, 0x85, 0x34, 0xd3, 0x1a, 0xd8, 0xc6, 0x3d, 0x59, 0x53, 0x7b, 0xb7, 0x11, 0x35, 0x83, 0x47, 0xf4, + 0x2d, 0x4d, 0x4d, 0xdf, 0xaf, 0x8d, 0xb4, 0x52, 0x0c, 0x94, 0x39, 0xc4, 0x74, 0x4d, 0x8d, 0x99, 0x54, 0xa9, 0xc5, + 0x7e, 0xdd, 0xe6, 0xd3, 0x6f, 0x17, 0xca, 0x21, 0x39, 0x70, 0x42, 0xc9, 0x11, 0x43, 0x76, 0x86, 0x21, 0xb8, 0x95, + 0xb3, 0x89, 0x64, 0xb9, 0x11, 0xb9, 0xcc, 0x3a, 0xa3, 0x3b, 0xfe, 0xc1, 0x04, 0x50, 0xe8, 0x8b, 0x05, 0x0a, 0xfa, + 0xb1, 0xda, 0xfa, 0x44, 0x1d, 0x49, 0x25, 0x29, 0x3e, 0x5d, 0xb8, 0x8a, 0xca, 0xa1, 0xe6, 0xea, 0x55, 0x51, 0x81, + 0x5a, 0x13, 0x3a, 0x70, 0x3d, 0x42, 0x60, 0x03, 0x61, 0xf4, 0x47, 0x53, 0x08, 0xcb, 0x7d, 0x15, 0x37, 0xed, 0x26, + 0xef, 0x9e, 0xce, 0xf6, 0x18, 0xa9, 0x41, 0x16, 0x5a, 0x56, 0x1c, 0xc3, 0xe9, 0x01, 0x4f, 0x06, 0x8f, 0x1d, 0x33, + 0x6c, 0x36, 0x4e, 0x8f, 0x31, 0x06, 0x58, 0xb2, 0xc2, 0x62, 0x9b, 0x4a, 0x6b, 0x45, 0x84, 0xd4, 0x36, 0xab, 0x97, + 0x36, 0x77, 0x8a, 0xfc, 0xf6, 0x67, 0x00, 0x98, 0x57, 0x4d, 0xa6, 0x75, 0x14, 0x53, 0xc4, 0x28, 0x69, 0xb3, 0x38, + 0x5e, 0x88, 0x95, 0x17, 0x1f, 0x0b, 0xdc, 0x1f, 0xa1, 0x72, 0x65, 0xb9, 0xe0, 0xea, 0x4c, 0xee, 0x87, 0x9b, 0xef, + 0x33, 0x27, 0x11, 0x2f, 0x98, 0xe8, 0x33, 0x66, 0xc3, 0xd5, 0x85, 0x77, 0xa4, 0x4e, 0xb3, 0x98, 0xdc, 0xfb, 0xe2, + 0x2d, 0x9f, 0xe7, 0x2e, 0xa0, 0xb2, 0x07, 0xb1, 0xdb, 0xaa, 0x8c, 0xf5, 0x3a, 0x23, 0x83, 0x84, 0x6f, 0x29, 0xd9, + 0x2b, 0x19, 0x3b, 0xf1, 0x19, 0x64, 0x7a, 0xb0, 0x0c, 0x0b, 0x4f, 0x19, 0xc9, 0xed, 0x33, 0x55, 0xd4, 0xae, 0xa7, + 0x54, 0xae, 0x8b, 0xee, 0xbc, 0xe6, 0xde, 0x56, 0xb8, 0x53, 0x33, 0x93, 0x4e, 0xbc, 0x2e, 0x40, 0x9d, 0x0f, 0x2e, + 0x2d, 0xd2, 0x39, 0x2f, 0x60, 0xd1, 0x0c, 0x85, 0xeb, 0xa9, 0x1a, 0x7d, 0xb6, 0xdc, 0x47, 0x16, 0xc3, 0xa6, 0x3b, + 0xbf, 0x2c, 0x7b, 0x34, 0xf9, 0x64, 0x81, 0x40, 0xec, 0x29, 0x3c, 0xbe, 0xa4, 0xc1, 0xad, 0xc5, 0xcf, 0xb4, 0xd5, + 0x56, 0x06, 0xaa, 0x4d, 0x52, 0x0b, 0xfc, 0x64, 0x39, 0xe2, 0xe4, 0x70, 0x6a, 0x79, 0xd7, 0xc0, 0x97, 0xf8, 0x05, + 0xf4, 0x87, 0xb0, 0x2a, 0x52, 0x97, 0x88, 0x6f, 0x09, 0x65, 0xe5, 0x98, 0xfb, 0x0d, 0xc8, 0x7a, 0x98, 0x2d, 0x14, + 0xc7, 0x9b, 0x70, 0x44, 0xa2, 0xb4, 0xfd, 0xdc, 0x1f, 0x1f, 0xf4, 0x2b, 0x7a, 0x0c, 0x86, 0xe3, 0x40, 0x85, 0xc8, + 0x99, 0x12, 0x22, 0x0a, 0xa7, 0x25, 0x5c, 0x86, 0xc6, 0x3c, 0x14, 0x04, 0x64, 0xd4, 0xff, 0x81, 0x70, 0x70, 0x31, + 0x6f, 0x9d, 0xa0, 0x52, 0x55, 0x5a, 0x58, 0x2e, 0x7b, 0xb1, 0x1f, 0x40, 0x95, 0x87, 0x3c, 0x60, 0x7d, 0xde, 0x71, + 0x9d, 0x33, 0x0b, 0x1e, 0x08, 0x46, 0x40, 0x12, 0x33, 0x5b, 0x47, 0xb7, 0x7a, 0xfa, 0x8b, 0xbb, 0x4e, 0x40, 0x3f, + 0x6e, 0x18, 0x7f, 0x84, 0x53, 0x51, 0x5a, 0xc8, 0x5f, 0xb5, 0x24, 0x9b, 0x30, 0xba, 0x0d, 0x8d, 0x75, 0x88, 0xc4, + 0xc5, 0x25, 0x47, 0xcf, 0x79, 0x51, 0xa0, 0x1c, 0xba, 0xee, 0x00, 0x8f, 0x85, 0x77, 0x57, 0x14, 0x68, 0x2e, 0xdc, + 0x35, 0x7d, 0x21, 0x27, 0xd6, 0x3a, 0x3c, 0x62, 0xad, 0x6d, 0x1b, 0xa2, 0x07, 0xcb, 0x29, 0x9e, 0xa1, 0xa1, 0x5c, + 0x2b, 0xd5, 0x92, 0x6c, 0x52, 0xcf, 0x80, 0x8c, 0x95, 0x7a, 0x82, 0x26, 0x65, 0xde, 0x21, 0x9e, 0x3a, 0x18, 0x3b, + 0x74, 0x93, 0x41, 0xf4, 0x5f, 0x47, 0xe6, 0x44, 0xe5, 0x9e, 0xf4, 0x63, 0xdb, 0xa8, 0xe0, 0x00, 0xe8, 0x68, 0x79, + 0xbf, 0xec, 0xbe, 0x77, 0xab, 0xb3, 0x14, 0x6d, 0x78, 0x55, 0x91, 0x84, 0x5a, 0x47, 0xfb, 0xbc, 0x86, 0xe7, 0xdb, + 0x11, 0x61, 0x44, 0xb7, 0x07, 0x66, 0x85, 0xb3, 0x6d, 0x52, 0x8c, 0x5d, 0xb5, 0xe0, 0x84, 0x79, 0x08, 0x88, 0x77, + 0x3d, 0xa9, 0x0e, 0x2b, 0x0d, 0xd1, 0x79, 0x1e, 0x5e, 0x2e, 0xae, 0x58, 0x98, 0xaa, 0x5e, 0x0a, 0x62, 0xbf, 0xf9, + 0xe0, 0x03, 0xf7, 0x79, 0x86, 0x61, 0xe3, 0xcd, 0x28, 0x4f, 0x8f, 0x31, 0x3b, 0x3f, 0xc4, 0x6e, 0xea, 0x48, 0x5f, + 0x71, 0x01, 0x7a, 0xbd, 0x27, 0xa7, 0xef, 0xd0, 0xf7, 0xa2, 0xe3, 0x8c, 0x2f, 0x0c, 0xd7, 0x8e, 0xf3, 0x05, 0x71, + 0x4a, 0x84, 0x28, 0xf5, 0x78, 0x51, 0x8f, 0x59, 0x22, 0x5e, 0x05, 0xc1, 0xb6, 0xe5, 0xcd, 0xf8, 0xef, 0xc2, 0x49, + 0xca, 0x77, 0xc7, 0x90, 0xc0, 0xe3, 0xc1, 0x9f, 0xa3, 0xe3, 0x02, 0x67, 0x22, 0x72, 0x18, 0x87, 0x3b, 0xb7, 0x55, + 0x4a, 0xef, 0xdb, 0xb0, 0x66, 0xea, 0xf5, 0xe7, 0x05, 0x21, 0xe3, 0x86, 0x1c, 0xb8, 0x83, 0x22, 0x9e, 0x96, 0xc0, + 0x5c, 0x9b, 0x42, 0x88, 0x7a, 0xfc, 0x37, 0xdc, 0x3c, 0x45, 0xf8, 0xa8, 0x11, 0x85, 0x89, 0xa6, 0xa6, 0xe4, 0xae, + 0xd8, 0x00, 0xac, 0xc4, 0x09, 0xed, 0x20, 0xf5, 0x43, 0x59, 0x79, 0x85, 0x81, 0xd5, 0xa2, 0xae, 0x04, 0x6a, 0x59, + 0x20, 0x8f, 0x0c, 0x4e, 0xec, 0xbd, 0x08, 0x8b, 0xae, 0x61, 0x14, 0xf4, 0x60, 0xaa, 0xb6, 0x5e, 0xc2, 0xeb, 0x6e, + 0x0b, 0xcb, 0x0f, 0xef, 0x57, 0x53, 0xcb, 0x5d, 0x95, 0x3f, 0x1e, 0x20, 0x67, 0xc9, 0xe9, 0x03, 0x80, 0x15, 0x0f, + 0x53, 0xc0, 0x56, 0xef, 0xcd, 0x61, 0x6b, 0x77, 0x89, 0xc6, 0x6d, 0xe6, 0x4f, 0x77, 0x48, 0x30, 0x4a, 0xfa, 0xd9, + 0xe7, 0x3f, 0xcf, 0x60, 0x71, 0xf4, 0x06, 0xc0, 0x43, 0xac, 0x3b, 0x59, 0xd5, 0xad, 0xec, 0x1e, 0xff, 0xf4, 0xa1, + 0x29, 0x12, 0xe9, 0x89, 0x69, 0xfc, 0xe2, 0xa8, 0x26, 0x7b, 0xab, 0x1d, 0x23, 0x67, 0x77, 0x24, 0xce, 0x4a, 0x09, + 0xc9, 0xe5, 0x88, 0x4a, 0x74, 0xdb, 0x23, 0x8a, 0xe0, 0xb5, 0x77, 0x96, 0x61, 0xa3, 0x5f, 0xc3, 0x08, 0x05, 0xa0, + 0x26, 0x60, 0xf8, 0xa0, 0xb2, 0xde, 0x09, 0x00, 0x8c, 0xd2, 0xaa, 0xa9, 0x53, 0x46, 0x17, 0xbb, 0xe9, 0xf2, 0xe2, + 0x41, 0xa6, 0xb4, 0x13, 0x35, 0x93, 0xdb, 0x13, 0x2a, 0x5b, 0x2d, 0x8c, 0x6d, 0xbf, 0x64, 0xc4, 0xa7, 0x81, 0x44, + 0x2b, 0x2c, 0x30, 0xa3, 0x83, 0x65, 0x29, 0xcb, 0x51, 0x22, 0xb1, 0x4c, 0x90, 0x5d, 0x79, 0x33, 0x8c, 0xbc, 0x0d, + 0xac, 0xc8, 0x8c, 0x48, 0x24, 0x5b, 0xd4, 0x74, 0x44, 0x0c, 0x8f, 0xda, 0x31, 0xab, 0xba, 0xb4, 0xb1, 0x62, 0xe1, + 0xe9, 0xe6, 0xd0, 0x93, 0x2b, 0xa4, 0xe8, 0x72, 0x1f, 0xa4, 0x50, 0x4c, 0x17, 0x6d, 0x5c, 0x9d, 0xdb, 0xec, 0x8b, + 0x28, 0xf3, 0x15, 0x99, 0x17, 0xb1, 0x98, 0xdd, 0x3f, 0xd9, 0xd8, 0x61, 0xb2, 0x3c, 0xce, 0xc9, 0x64, 0xe6, 0x40, + 0x35, 0x6d, 0xc8, 0xb5, 0xe4, 0xb5, 0x64, 0xc5, 0x49, 0x5c, 0xfc, 0xbb, 0xbc, 0x6c, 0xf3, 0x64, 0xaa, 0x10, 0x41, + 0x0f, 0xb3, 0x64, 0x81, 0x59, 0xaa, 0xa5, 0x83, 0x12, 0xce, 0x22, 0xb2, 0xa3, 0x81, 0xe9, 0x4d, 0x49, 0x9b, 0x7c, + 0xd0, 0x49, 0x77, 0x27, 0x6f, 0x0d, 0x09, 0xd7, 0x6b, 0x9c, 0xd8, 0x16, 0x73, 0x31, 0xe2, 0xa9, 0xef, 0xca, 0x24, + 0x5a, 0x91, 0x78, 0x90, 0x25, 0x31, 0x57, 0x9e, 0x8d, 0x45, 0x89, 0x2f, 0x72, 0x7a, 0x5a, 0x2f, 0x66, 0xa3, 0x45, + 0x1a, 0xfb, 0xc3, 0xc8, 0x2f, 0x8b, 0x9f, 0xdd, 0x8e, 0x1c, 0xf5, 0xf6, 0x84, 0xf2, 0xac, 0xa6, 0xb6, 0xae, 0x99, + 0x39, 0x66, 0x94, 0x69, 0xa4, 0x10, 0x4b, 0x48, 0x9f, 0x8c, 0x08, 0x5a, 0x9c, 0x0e, 0x6c, 0xd8, 0xfc, 0x4e, 0x05, + 0x9e, 0xa9, 0xdd, 0x5e, 0x0d, 0x0d, 0xcf, 0x2b, 0x24, 0x82, 0x0b, 0x1a, 0x6f, 0x70, 0xd4, 0x0c, 0xf5, 0x7f, 0x78, + 0x3a, 0x6f, 0xcd, 0x74, 0xf6, 0x44, 0x32, 0xb2, 0xb4, 0xf0, 0x0c, 0x70, 0x3e, 0xa9, 0x4a, 0x73, 0x7b, 0x3f, 0xc8, + 0x23, 0xeb, 0xfe, 0x49, 0x54, 0xbf, 0x22, 0xb0, 0x3b, 0x49, 0x4c, 0x08, 0xd0, 0xf0, 0xba, 0x9e, 0x0d, 0x13, 0x09, + 0xad, 0x04, 0xef, 0xbb, 0x0a, 0xfe, 0x4e, 0xca, 0x24, 0x5d, 0x9a, 0xd0, 0xe4, 0xa2, 0x5c, 0x0d, 0x76, 0xb2, 0x40, + 0xbe, 0x05, 0xd8, 0x40, 0x10, 0x08, 0xac, 0x30, 0xef, 0x98, 0x4a, 0x68, 0x07, 0xd2, 0x40, 0xe6, 0x04, 0x98, 0x64, + 0xe3, 0x5c, 0x19, 0x14, 0xd5, 0x46, 0x3e, 0xad, 0x72, 0x36, 0x24, 0x1a, 0x06, 0x99, 0xf5, 0xc7, 0xd0, 0xd9, 0x2b, + 0x26, 0xc9, 0xbc, 0xbf, 0x73, 0x34, 0x9e, 0x6c, 0xcf, 0x90, 0x28, 0xe4, 0x6a, 0x9f, 0x41, 0x3c, 0xa1, 0x19, 0x2e, + 0x2b, 0x51, 0x5f, 0xd6, 0xb5, 0xfa, 0x4f, 0xaa, 0xf7, 0x1d, 0x3c, 0x39, 0x90, 0x45, 0x6f, 0xe3, 0xc0, 0x72, 0xcb, + 0x16, 0x01, 0xe6, 0xf9, 0x1a, 0x68, 0x46, 0x09, 0x20, 0x43, 0x13, 0x60, 0xae, 0x31, 0x7b, 0x69, 0x68, 0x46, 0x28, + 0xfb, 0x22, 0xd7, 0x26, 0xa1, 0xe8, 0x61, 0xee, 0xcb, 0x2b, 0x71, 0x9b, 0xeb, 0x1d, 0xd2, 0xeb, 0xb6, 0x7a, 0x8f, + 0x5d, 0x8e, 0xc8, 0x72, 0x8a, 0xb8, 0x4d, 0xa8, 0x1e, 0xa0, 0x90, 0x55, 0x13, 0xa6, 0x75, 0xb0, 0x3b, 0xe3, 0x2f, + 0x49, 0x88, 0x30, 0x21, 0x31, 0xaa, 0x8f, 0xd0, 0xb1, 0x1a, 0xfb, 0x44, 0x8f, 0x25, 0x47, 0xa2, 0x37, 0x48, 0x1d, + 0xb9, 0x14, 0x3a, 0x8f, 0x0b, 0x75, 0x6d, 0xad, 0xb6, 0xe0, 0x12, 0x61, 0xc0, 0x89, 0x55, 0x0e, 0x87, 0xcb, 0xa9, + 0x50, 0xd9, 0x12, 0xf7, 0x36, 0x66, 0xd4, 0xcb, 0x9d, 0xb7, 0x59, 0x97, 0x7a, 0x6f, 0x12, 0x16, 0x91, 0xe5, 0xa1, + 0x62, 0x1c, 0x08, 0x05, 0x6b, 0xfb, 0x60, 0x79, 0x8d, 0x33, 0xf2, 0x2c, 0xc3, 0x66, 0x30, 0x7a, 0x1f, 0xa0, 0xac, + 0xa8, 0xc7, 0xe1, 0x02, 0x10, 0xeb, 0x43, 0xf2, 0xa2, 0xc9, 0x0c, 0x01, 0x16, 0xd9, 0xe2, 0x52, 0x93, 0x2c, 0x14, + 0x3a, 0xea, 0xaf, 0x7b, 0x40, 0xbb, 0x16, 0x12, 0x03, 0xe9, 0xf0, 0xa8, 0x93, 0xae, 0x66, 0x89, 0x65, 0x73, 0x0c, + 0x0d, 0x85, 0xc5, 0x69, 0x9e, 0xa7, 0x23, 0xdb, 0xda, 0x3b, 0xc0, 0x89, 0x8e, 0xae, 0x17, 0xe0, 0xb6, 0x83, 0x4b, + 0x21, 0xc7, 0x11, 0xdc, 0x34, 0x47, 0x79, 0x76, 0x2a, 0x6d, 0x0a, 0x46, 0x13, 0x37, 0x2b, 0xcd, 0x85, 0x2e, 0xa7, + 0xf0, 0x3c, 0xdd, 0xfa, 0x13, 0x15, 0xfd, 0xd3, 0x52, 0x3b, 0x83, 0x41, 0x95, 0xd3, 0xae, 0x94, 0xb1, 0xa4, 0x5d, + 0x73, 0xf4, 0x85, 0x40, 0x1e, 0x16, 0xfa, 0x7e, 0xa1, 0x71, 0xe7, 0xd4, 0x41, 0xf1, 0x8e, 0x71, 0x66, 0xa7, 0x07, + 0x0d, 0x7b, 0xa5, 0xf1, 0x68, 0x44, 0x29, 0x2b, 0xf5, 0x03, 0xe3, 0x5a, 0xde, 0x9e, 0x10, 0x6d, 0x32, 0x0a, 0x77, + 0x28, 0xcb, 0xe4, 0xdb, 0x1e, 0x07, 0x9a, 0xb6, 0x67, 0xdc, 0x76, 0x5b, 0xdf, 0xae, 0x93, 0x5b, 0x44, 0xe2, 0xf6, + 0x17, 0x5c, 0xc2, 0x33, 0xf8, 0xc6, 0x90, 0x8a, 0x3d, 0xeb, 0xc4, 0xe5, 0xcb, 0x28, 0xcb, 0xf9, 0x0a, 0x47, 0x57, + 0x4c, 0xc6, 0xc2, 0x0b, 0x2d, 0x22, 0xdc, 0x34, 0x50, 0xc7, 0x95, 0x24, 0xb1, 0x9b, 0x92, 0xf8, 0xb9, 0xe5, 0x9f, + 0xb7, 0xe6, 0x46, 0xc0, 0x54, 0x24, 0xd7, 0x21, 0xfa, 0xcc, 0xa9, 0x5a, 0xdd, 0x6b, 0x95, 0x05, 0xf5, 0x98, 0xa7, + 0x72, 0xc4, 0x9c, 0xba, 0xdd, 0x14, 0x59, 0x26, 0x3d, 0x6c, 0xae, 0x29, 0x4a, 0x14, 0x68, 0xab, 0x0b, 0xbd, 0xcc, + 0x9c, 0xb3, 0xd0, 0xd1, 0x89, 0x94, 0x6d, 0x8d, 0x66, 0x13, 0x73, 0x1c, 0xce, 0x7e, 0x12, 0xd9, 0x13, 0x5c, 0xf5, + 0x9e, 0xb7, 0xf6, 0x61, 0xb3, 0xf1, 0x75, 0xa8, 0xd5, 0x90, 0x1d, 0x10, 0x68, 0xe6, 0xce, 0x14, 0x28, 0xc2, 0xfe, + 0x2b, 0x3b, 0x12, 0xa5, 0x2c, 0xff, 0xd8, 0x69, 0x5d, 0xdf, 0x36, 0xaa, 0x8e, 0xc9, 0x5f, 0xd3, 0xbe, 0x86, 0xab, + 0x0e, 0x8a, 0x9c, 0xc3, 0xf1, 0x49, 0xbb, 0x33, 0xdd, 0x3c, 0x10, 0x9e, 0xb3, 0xc3, 0xa8, 0x2c, 0x67, 0x57, 0xd4, + 0x1b, 0xba, 0x0a, 0x18, 0xa8, 0x51, 0x32, 0x29, 0x7b, 0xa3, 0xb0, 0x8e, 0xfa, 0x9d, 0xb8, 0xd6, 0x57, 0x14, 0xdd, + 0xb2, 0xc6, 0xad, 0x4d, 0x76, 0xe0, 0x8f, 0x18, 0x2b, 0x77, 0x98, 0x21, 0x3f, 0x5c, 0x63, 0xd5, 0x22, 0xf5, 0x46, + 0xe3, 0x62, 0xdb, 0x6a, 0x3a, 0xd3, 0x40, 0xb7, 0xad, 0x99, 0x1b, 0x61, 0x07, 0xd5, 0x70, 0x5b, 0xb7, 0x95, 0xaa, + 0xb6, 0x9d, 0xc7, 0xaf, 0xf6, 0xd5, 0x89, 0x98, 0xd0, 0x86, 0xa1, 0xaf, 0x81, 0xe9, 0x5a, 0x54, 0x73, 0x31, 0xb0, + 0xa9, 0x5e, 0x2d, 0xf6, 0x5d, 0xc8, 0xee, 0xdd, 0x5f, 0x43, 0x12, 0xaa, 0xe2, 0xca, 0x2d, 0x2f, 0xb7, 0x9f, 0x74, + 0xb2, 0x4a, 0x65, 0x6a, 0x1f, 0xf9, 0x1d, 0x66, 0xca, 0x87, 0x99, 0xe2, 0x71, 0xa5, 0x63, 0x2d, 0x20, 0x0a, 0x43, + 0xe1, 0x55, 0x0a, 0x74, 0x6b, 0x16, 0xf1, 0x0f, 0x74, 0xec, 0xca, 0x98, 0x11, 0x32, 0x1a, 0x95, 0x33, 0x74, 0x43, + 0x42, 0x35, 0x34, 0xb1, 0x9c, 0xa4, 0x4b, 0x0d, 0xba, 0xda, 0xe1, 0x3a, 0xb2, 0x3c, 0x10, 0x02, 0x71, 0x22, 0x87, + 0x39, 0x53, 0x23, 0xda, 0xfd, 0x24, 0x30, 0x91, 0x66, 0x5d, 0xb5, 0x5f, 0x74, 0x38, 0xdd, 0x50, 0x7b, 0x4f, 0xbf, + 0x7c, 0x68, 0xb4, 0xa7, 0x5f, 0xae, 0xb4, 0x3e, 0x39, 0x31, 0xe5, 0xd4, 0x4a, 0xc7, 0x0d, 0x8c, 0xc3, 0x45, 0xe9, + 0xc0, 0xf7, 0x48, 0x35, 0xb8, 0x31, 0xdc, 0x8d, 0x4e, 0xe0, 0x8c, 0xdc, 0x36, 0x22, 0x2b, 0x37, 0x81, 0x99, 0x81, + 0x94, 0xd2, 0x8b, 0x63, 0xe0, 0xbe, 0xed, 0xfd, 0x28, 0xc9, 0x78, 0xd3, 0x64, 0xfc, 0x7a, 0x99, 0x15, 0x4a, 0xdf, + 0x33, 0xb3, 0xd0, 0x55, 0xfc, 0xce, 0x24, 0x77, 0xb5, 0xc6, 0x4e, 0xaa, 0xe5, 0x0c, 0x18, 0xe5, 0x6a, 0x85, 0xe5, + 0x8e, 0xf7, 0xe4, 0xb0, 0xb9, 0x9f, 0x65, 0x09, 0x69, 0xb2, 0x15, 0x55, 0x89, 0x31, 0x22, 0x85, 0xf6, 0x17, 0x67, + 0xe7, 0xfe, 0x68, 0xf1, 0x01, 0x1d, 0xf5, 0x1d, 0x33, 0xae, 0xc6, 0xad, 0xd8, 0x2e, 0x56, 0xec, 0x60, 0x1a, 0xae, + 0x0d, 0xa6, 0x79, 0x80, 0xd0, 0x3d, 0x73, 0x07, 0xf5, 0x0b, 0xfc, 0x8f, 0x7c, 0x5c, 0x55, 0x48, 0x87, 0x2e, 0x9b, + 0xa9, 0x28, 0x5f, 0xa2, 0x06, 0x05, 0x2c, 0x5a, 0xb7, 0x4b, 0x13, 0x30, 0x45, 0x16, 0xd2, 0x2d, 0xa4, 0x20, 0x4a, + 0x16, 0x82, 0x19, 0x54, 0x7c, 0xe5, 0x2f, 0x13, 0x5f, 0xeb, 0xab, 0x85, 0x5e, 0xd2, 0x13, 0xb6, 0x0a, 0xb9, 0xba, + 0x61, 0xb4, 0x98, 0x55, 0xa7, 0x1d, 0xa7, 0x89, 0x43, 0x83, 0x1a, 0x75, 0x44, 0xe8, 0x3a, 0x3e, 0xf8, 0x6c, 0x13, + 0x79, 0x83, 0xc9, 0x4f, 0x4e, 0x02, 0xfe, 0x5e, 0x9f, 0xbc, 0xc5, 0xd9, 0x43, 0xac, 0x4a, 0x33, 0x1e, 0x2f, 0x94, + 0x3d, 0x2a, 0x7b, 0x41, 0xad, 0xb1, 0x9f, 0x5d, 0x98, 0xd6, 0x46, 0x25, 0x85, 0xdc, 0x79, 0xb8, 0x90, 0xef, 0x9c, + 0xc2, 0xb9, 0x1b, 0x95, 0x88, 0xf2, 0x00, 0x66, 0xc2, 0xe6, 0xc4, 0x8d, 0x8a, 0x5b, 0x40, 0xe5, 0x4c, 0x4f, 0x9a, + 0xc4, 0x74, 0x56, 0x22, 0xc6, 0x8c, 0x4e, 0xe1, 0x7a, 0x1c, 0xa2, 0x31, 0x34, 0xc3, 0x9c, 0xde, 0xc7, 0xe8, 0x09, + 0x72, 0x80, 0xb3, 0x76, 0xad, 0x21, 0xc4, 0x4c, 0x2a, 0x7c, 0xef, 0x56, 0xc4, 0x96, 0xd9, 0x17, 0x82, 0xda, 0x36, + 0xef, 0xbb, 0x11, 0x51, 0x5e, 0x29, 0x7c, 0x9f, 0xfb, 0xcb, 0x2f, 0x18, 0xaf, 0x64, 0x68, 0x0d, 0xcf, 0x92, 0x9f, + 0xc3, 0xfc, 0xec, 0x37, 0x76, 0x60, 0x02, 0x12, 0xa7, 0x15, 0x8d, 0x7a, 0x4a, 0x96, 0xe6, 0x3a, 0xeb, 0x7d, 0x13, + 0xce, 0x28, 0x99, 0x06, 0x4c, 0xac, 0x65, 0x16, 0x40, 0x27, 0x52, 0x09, 0x9c, 0x25, 0x95, 0x75, 0x34, 0x93, 0x47, + 0x0b, 0xbd, 0x37, 0xf1, 0xf4, 0x45, 0x49, 0x7a, 0x05, 0xfe, 0xd8, 0x52, 0x63, 0x51, 0xa6, 0x6d, 0x5e, 0x04, 0xaa, + 0x66, 0x2d, 0x8f, 0x83, 0x5c, 0x7a, 0xbd, 0xac, 0x7a, 0xe5, 0x69, 0x2d, 0xd5, 0x05, 0xda, 0x4e, 0xc8, 0x31, 0x6a, + 0x51, 0x5e, 0x41, 0x1a, 0x8a, 0xf6, 0x40, 0xe9, 0x6b, 0x98, 0xd0, 0x03, 0x7e, 0xa9, 0x06, 0x65, 0x34, 0x78, 0x67, + 0xcd, 0x16, 0x17, 0x93, 0x23, 0x67, 0xcd, 0x00, 0x02, 0x6e, 0xd7, 0xdb, 0x52, 0x13, 0x21, 0x15, 0x6e, 0x30, 0x4c, + 0x8b, 0x44, 0xfd, 0x44, 0x73, 0x58, 0xbb, 0x42, 0x52, 0x87, 0x58, 0x87, 0x16, 0x26, 0xa0, 0x35, 0xe3, 0x62, 0x43, + 0x8b, 0xb2, 0x13, 0x39, 0xb0, 0x36, 0x8b, 0x24, 0xe3, 0xb0, 0x47, 0x33, 0x6d, 0x06, 0x72, 0x2d, 0xc1, 0x65, 0x89, + 0xe8, 0x2d, 0x8a, 0xee, 0x9e, 0xc8, 0xb0, 0xb9, 0xc9, 0x4a, 0xa6, 0xcc, 0xf4, 0x68, 0x08, 0xb4, 0x6b, 0x0f, 0x06, + 0xdb, 0xa1, 0x82, 0xbf, 0x84, 0x77, 0x49, 0xd2, 0xfd, 0x3e, 0x7b, 0xdc, 0x81, 0x0f, 0xe1, 0xd4, 0x69, 0xbf, 0x09, + 0xb0, 0xce, 0x81, 0x53, 0xac, 0x13, 0x63, 0x9c, 0x71, 0x54, 0xef, 0x66, 0xb4, 0xb1, 0x9f, 0x10, 0x43, 0xa0, 0x70, + 0xf8, 0xb6, 0x47, 0x2b, 0xaf, 0xda, 0xb1, 0x36, 0xd3, 0x4b, 0xda, 0x91, 0x8f, 0xc8, 0x11, 0x4c, 0x82, 0x48, 0x5a, + 0x26, 0x10, 0x9a, 0x31, 0x78, 0x0b, 0x57, 0xb0, 0x36, 0x67, 0x40, 0x4b, 0x5d, 0x2f, 0x14, 0x5a, 0xe0, 0xe9, 0x19, + 0x03, 0x93, 0xc2, 0xbc, 0x83, 0x4b, 0xda, 0x7f, 0x34, 0xc2, 0xac, 0xa1, 0x5a, 0xad, 0xed, 0x36, 0x2d, 0x1f, 0x12, + 0x05, 0xc2, 0xf6, 0x53, 0xbd, 0xe9, 0x7e, 0xe4, 0x67, 0xd7, 0x02, 0xd4, 0x55, 0x6c, 0xbb, 0xc6, 0x8b, 0x7a, 0xef, + 0x6d, 0x6b, 0xf4, 0xb1, 0xbf, 0xd2, 0xf0, 0x2d, 0xc4, 0xb0, 0x2c, 0x99, 0x30, 0x5d, 0x99, 0x0f, 0x7e, 0xce, 0x14, + 0xf7, 0x79, 0x1a, 0x93, 0xee, 0x0e, 0x25, 0x26, 0xf1, 0x75, 0x67, 0x77, 0xd8, 0xb6, 0x8c, 0xe8, 0x65, 0xfd, 0x56, + 0xaf, 0xb0, 0xd3, 0xe7, 0xdf, 0x41, 0x4c, 0xbd, 0xa2, 0x64, 0x3c, 0x4c, 0xb4, 0xc5, 0x43, 0x50, 0x18, 0xbf, 0xca, + 0x9c, 0x0c, 0x3e, 0xb9, 0xb7, 0x2d, 0x24, 0xc2, 0x6f, 0xe3, 0x55, 0x9c, 0xcc, 0x5a, 0x34, 0x9c, 0x76, 0x3d, 0x29, + 0x0e, 0x8c, 0x84, 0xd6, 0xcc, 0xb7, 0x49, 0x5a, 0x73, 0x29, 0x0c, 0xbf, 0x58, 0x88, 0x8d, 0x66, 0xe3, 0x28, 0x5a, + 0x0a, 0xa0, 0xa5, 0x3d, 0x72, 0xc9, 0x62, 0xe0, 0x61, 0xc1, 0x43, 0xf9, 0xd2, 0x12, 0x96, 0x3d, 0x7f, 0x9d, 0x4e, + 0xe4, 0x9b, 0x9b, 0x9c, 0x6e, 0xb7, 0x73, 0x75, 0xf9, 0xfc, 0x4b, 0x1a, 0x51, 0x56, 0xbf, 0xe8, 0x91, 0x44, 0x35, + 0xd6, 0xc7, 0xd6, 0xf3, 0x2f, 0xb9, 0x57, 0x27, 0x92, 0xd3, 0xce, 0x76, 0xc0, 0x70, 0x4d, 0x01, 0x5b, 0xa6, 0xed, + 0x61, 0x53, 0xf6, 0xf7, 0x5b, 0x17, 0x07, 0x75, 0x41, 0xe2, 0x13, 0xe6, 0x14, 0x49, 0x8a, 0xc7, 0x06, 0x1d, 0x08, + 0xb5, 0x0c, 0xa8, 0x47, 0xb0, 0x2f, 0x27, 0x76, 0xe4, 0x9b, 0xa7, 0xd1, 0x2f, 0xca, 0x74, 0xe8, 0x90, 0xa6, 0x43, + 0x1e, 0x02, 0x1b, 0xb7, 0xb9, 0xcb, 0x81, 0x22, 0x71, 0xa0, 0x22, 0x66, 0xda, 0x2f, 0x52, 0x7b, 0x39, 0x2f, 0xc2, + 0x9c, 0xa3, 0xea, 0xca, 0xe9, 0x53, 0x62, 0xdf, 0x85, 0x18, 0x7d, 0x88, 0x5b, 0x79, 0x67, 0x87, 0xbd, 0x91, 0x7e, + 0x88, 0x73, 0xf3, 0x25, 0x0e, 0x8c, 0xa8, 0xd2, 0x1c, 0xcd, 0x42, 0xa4, 0x14, 0xb9, 0xa6, 0x95, 0x7d, 0x47, 0x91, + 0xe9, 0x7a, 0x16, 0x7d, 0x79, 0x96, 0xc8, 0xec, 0x89, 0x30, 0x99, 0x43, 0xbd, 0x83, 0x97, 0x94, 0x68, 0xd6, 0xb6, + 0x5b, 0x07, 0x04, 0x76, 0x02, 0xe6, 0x69, 0x89, 0xbc, 0x4e, 0xc9, 0xc9, 0x7f, 0x7c, 0xfb, 0x2f, 0x2a, 0x79, 0x04, + 0x0f, 0x35, 0x75, 0x61, 0x19, 0x2d, 0x44, 0x1c, 0xc7, 0xf9, 0xdd, 0xba, 0x4e, 0x40, 0x8c, 0xf5, 0xe7, 0x67, 0x6b, + 0xcc, 0xd6, 0x41, 0xad, 0xa4, 0xa1, 0x48, 0xcc, 0xcd, 0x8e, 0x99, 0x95, 0xc9, 0x95, 0x71, 0xc5, 0x6e, 0x83, 0x7e, + 0x12, 0x59, 0x28, 0xd1, 0x8c, 0xe2, 0xe1, 0x14, 0x8b, 0xa4, 0xa4, 0x15, 0x16, 0xb5, 0xe4, 0x33, 0x43, 0x39, 0x4c, + 0x96, 0xa5, 0x6d, 0x67, 0x2e, 0x85, 0x64, 0x2d, 0x4b, 0x80, 0xec, 0x62, 0x89, 0x9a, 0xf3, 0x8a, 0x5c, 0x86, 0x15, + 0x91, 0x13, 0xc0, 0x38, 0x30, 0x85, 0x9f, 0xfc, 0x49, 0x68, 0x7f, 0x27, 0x0f, 0x3e, 0x85, 0xf0, 0x32, 0x4e, 0xd0, + 0x83, 0x71, 0x2b, 0x98, 0xc1, 0xc1, 0x10, 0xbd, 0x50, 0xc2, 0xba, 0xdc, 0x89, 0x17, 0x24, 0xcb, 0x52, 0x37, 0x40, + 0x68, 0xd6, 0xcd, 0x5a, 0xdd, 0xb7, 0xb0, 0x2a, 0x59, 0x42, 0x68, 0xc4, 0x4a, 0x2b, 0xb6, 0x62, 0x9b, 0x82, 0x8e, + 0x28, 0xc9, 0x09, 0x60, 0x66, 0x00, 0xce, 0x4e, 0x22, 0x2a, 0x35, 0xb0, 0x8e, 0x61, 0xc5, 0x62, 0xa6, 0x31, 0x29, + 0x80, 0xd5, 0xae, 0xf1, 0x51, 0x36, 0x4d, 0x17, 0x28, 0x54, 0x5f, 0x3b, 0x27, 0xe8, 0xa3, 0x4b, 0x2b, 0xf5, 0xd8, + 0x27, 0x60, 0xff, 0xe3, 0x0e, 0xea, 0x60, 0xd1, 0xa8, 0xfb, 0xd6, 0xbf, 0xc4, 0x90, 0xe7, 0x35, 0x62, 0xdc, 0xdc, + 0x1f, 0x38, 0xd5, 0x01, 0x9b, 0x64, 0x35, 0x1b, 0x49, 0x9c, 0x04, 0x3d, 0x87, 0xea, 0x4d, 0x28, 0xc1, 0x50, 0x5d, + 0xba, 0xca, 0x9e, 0x47, 0x46, 0xbc, 0x35, 0x96, 0x95, 0x2c, 0xf9, 0x19, 0xd0, 0x05, 0xe5, 0x29, 0x21, 0x38, 0xdb, + 0xce, 0x4a, 0xa2, 0x30, 0xd6, 0xa2, 0x38, 0xc6, 0x09, 0xbf, 0x23, 0x59, 0x19, 0x97, 0x4c, 0x51, 0x98, 0xf2, 0x39, + 0x38, 0x57, 0xe6, 0xc3, 0xdf, 0x9e, 0xfc, 0xf2, 0x9c, 0xae, 0x2e, 0x45, 0xec, 0xf3, 0xe3, 0x9c, 0x5e, 0x7f, 0x9b, + 0xfe, 0x25, 0xf3, 0x59, 0xf8, 0x27, 0xbc, 0xb3, 0x84, 0x9c, 0x77, 0x3f, 0x3e, 0x15, 0x2d, 0x0e, 0x8a, 0x85, 0xae, + 0x62, 0x8b, 0x5a, 0x70, 0xfe, 0xfc, 0xca, 0x66, 0xaa, 0x3c, 0x26, 0x68, 0xa6, 0x92, 0xb2, 0xfa, 0x4d, 0x91, 0x02, + 0x69, 0x1b, 0x95, 0x84, 0x8d, 0xff, 0x31, 0x05, 0xc5, 0xff, 0x47, 0x19, 0x0a, 0x0d, 0x59, 0xfb, 0xeb, 0x2d, 0x93, + 0xfc, 0x0a, 0x9e, 0xff, 0x31, 0x29, 0x50, 0xab, 0x9f, 0x08, 0x50, 0x49, 0x5b, 0x49, 0xa5, 0x0f, 0x0e, 0x3c, 0xd6, + 0xd1, 0xe4, 0x8c, 0x69, 0x18, 0xcf, 0x3c, 0x61, 0x3f, 0x03, 0x86, 0xb6, 0x59, 0x97, 0xbc, 0xdb, 0x36, 0xf1, 0x1f, + 0x28, 0xbc, 0x29, 0x53, 0x1b, 0x8d, 0x41, 0x72, 0xaa, 0x00, 0x69, 0x8e, 0xb3, 0x55, 0xe8, 0x8a, 0x36, 0x9c, 0x73, + 0xb3, 0xa5, 0x05, 0x67, 0xc3, 0xd8, 0x6a, 0xf8, 0xf2, 0x17, 0xc4, 0x56, 0xd8, 0x35, 0xa9, 0x83, 0xaa, 0xac, 0x79, + 0x71, 0x13, 0xfe, 0x09, 0xdb, 0x4b, 0x0c, 0x66, 0xf2, 0x92, 0xe6, 0x93, 0xe9, 0x08, 0x69, 0x9e, 0x21, 0x67, 0x36, + 0xff, 0xa3, 0x98, 0xc9, 0xf2, 0x52, 0x46, 0x33, 0x5f, 0x26, 0xc6, 0xbf, 0xf9, 0x33, 0x09, 0xec, 0x57, 0xce, 0x87, + 0x51, 0x64, 0x62, 0x79, 0x6c, 0x1b, 0x2f, 0xc8, 0x7d, 0x0c, 0xdd, 0x68, 0xb1, 0xca, 0xb2, 0x8c, 0x7d, 0xa5, 0xcc, + 0xd2, 0x18, 0x83, 0xc3, 0xd3, 0xf5, 0x88, 0x2a, 0x74, 0xd6, 0x87, 0x3c, 0x97, 0xfe, 0x65, 0x95, 0x0a, 0xd3, 0x87, + 0x32, 0x53, 0x5a, 0x6f, 0x81, 0xd8, 0xeb, 0x89, 0xe2, 0xc3, 0x57, 0x12, 0x6d, 0x72, 0x24, 0xe7, 0x83, 0x53, 0x58, + 0x4d, 0xf2, 0xda, 0x23, 0x13, 0xf1, 0x0c, 0x3f, 0xd9, 0xf6, 0xf3, 0x5c, 0x49, 0xcf, 0x2f, 0x3e, 0xc3, 0x6e, 0x97, + 0xc6, 0xde, 0x4b, 0x7e, 0x27, 0x3f, 0x47, 0x1f, 0x06, 0x77, 0xe4, 0xa4, 0xa4, 0xb6, 0xbf, 0xf4, 0x39, 0xae, 0x03, + 0x65, 0xf7, 0x3f, 0xa8, 0xbe, 0x86, 0x2c, 0x2a, 0x1e, 0x4d, 0xd2, 0x15, 0xe6, 0x60, 0xa9, 0x1f, 0x66, 0x2e, 0xfc, + 0x45, 0x9a, 0xe0, 0x2c, 0xba, 0xd1, 0xcb, 0x83, 0x69, 0x3d, 0xf9, 0x47, 0x64, 0xe9, 0x4f, 0xb3, 0x6c, 0x72, 0x38, + 0x0d, 0x17, 0xfc, 0x48, 0x46, 0x3f, 0xde, 0xab, 0xdb, 0x93, 0x7a, 0xad, 0x97, 0x7b, 0x08, 0x98, 0x7e, 0xa4, 0x21, + 0x92, 0x37, 0xcb, 0x54, 0x61, 0x40, 0xf2, 0x06, 0x17, 0xb4, 0x06, 0x5d, 0x6a, 0x9a, 0xa5, 0x55, 0xe0, 0x8c, 0xee, + 0x09, 0x3a, 0xa8, 0xe0, 0x68, 0xb9, 0xf2, 0xf5, 0x59, 0xc4, 0xe2, 0xa4, 0x62, 0xbb, 0x2d, 0x8a, 0x68, 0xcf, 0xe0, + 0x38, 0x5a, 0x44, 0x45, 0x66, 0xf4, 0xbb, 0xd4, 0x56, 0x28, 0xfb, 0x82, 0x15, 0xdc, 0xd1, 0x17, 0xb2, 0x52, 0xae, + 0xa5, 0x21, 0xdf, 0x4a, 0xc9, 0x16, 0x1a, 0x50, 0x29, 0xc5, 0x96, 0xaa, 0x71, 0x19, 0x07, 0x57, 0xc6, 0xe6, 0x58, + 0xc2, 0x92, 0x56, 0xc5, 0xab, 0xc8, 0x90, 0x8e, 0xaf, 0x13, 0x41, 0xca, 0x65, 0x19, 0x38, 0x3c, 0x9c, 0xa3, 0x0c, + 0x79, 0xb2, 0x0d, 0x25, 0x79, 0x26, 0x60, 0x0e, 0x66, 0x5c, 0xab, 0x27, 0xd5, 0xaa, 0x01, 0x8d, 0x14, 0xd5, 0x55, + 0x4c, 0x67, 0xab, 0x03, 0xea, 0xf8, 0x15, 0x81, 0x59, 0x58, 0xc6, 0xf3, 0x28, 0xc4, 0x5d, 0x29, 0xc3, 0x2e, 0xdc, + 0x4e, 0x12, 0xac, 0xc7, 0xc9, 0x70, 0xb8, 0xa3, 0x8d, 0x9d, 0x8b, 0x5e, 0xe3, 0x47, 0x21, 0x5c, 0x4a, 0xf7, 0x18, + 0x19, 0x81, 0xc9, 0xc5, 0xce, 0xa5, 0xf3, 0x49, 0x13, 0xee, 0x64, 0x41, 0x00, 0x44, 0x1e, 0xf6, 0x7d, 0xb0, 0xb8, + 0x3c, 0xea, 0x2c, 0x60, 0x62, 0x9e, 0x2b, 0x3b, 0x2a, 0x6f, 0xe0, 0xab, 0x75, 0x28, 0x2b, 0x7b, 0x47, 0x5f, 0x26, + 0x31, 0x56, 0xda, 0x8c, 0xdf, 0x96, 0xe5, 0x51, 0x7a, 0x63, 0x59, 0x4d, 0x5b, 0x54, 0x0f, 0x1e, 0xdd, 0xe1, 0xda, + 0x11, 0x63, 0x63, 0x99, 0x75, 0x62, 0x11, 0x98, 0xff, 0x3e, 0xb3, 0x08, 0x1b, 0x55, 0x2d, 0xdf, 0x04, 0xd2, 0x11, + 0xa3, 0x59, 0xd4, 0xf0, 0x80, 0x4f, 0x47, 0xcb, 0x18, 0x16, 0x33, 0x82, 0x59, 0xf6, 0xa0, 0xe5, 0x6a, 0x08, 0xd2, + 0x8c, 0x47, 0x89, 0x20, 0xdd, 0x88, 0xa1, 0x19, 0xc9, 0x19, 0x01, 0x9b, 0xa4, 0x10, 0x83, 0x67, 0xc0, 0xfe, 0xd8, + 0x39, 0x22, 0x15, 0x1c, 0xd1, 0x03, 0xc2, 0xaa, 0x8a, 0xcb, 0x0f, 0x0b, 0x1b, 0x06, 0x62, 0x48, 0xc5, 0x8b, 0x59, + 0xf9, 0xb4, 0x00, 0x18, 0x59, 0xa3, 0x8a, 0x87, 0x64, 0x88, 0x8c, 0xbc, 0x69, 0x91, 0x51, 0x87, 0x64, 0x0c, 0xbf, + 0x11, 0x31, 0x90, 0x94, 0x9c, 0x41, 0x1e, 0x73, 0xb2, 0x55, 0x2e, 0x5f, 0xe6, 0x2e, 0xfd, 0xd3, 0xfe, 0x54, 0x8e, + 0xf7, 0xa9, 0xd4, 0xd0, 0xa6, 0x97, 0x71, 0x39, 0x17, 0x15, 0x07, 0xd7, 0xcb, 0x76, 0xd3, 0xd3, 0x8e, 0xe6, 0x0b, + 0xd7, 0xe6, 0x66, 0xbb, 0x30, 0xde, 0x1d, 0xab, 0xec, 0xc3, 0x27, 0x94, 0x71, 0x41, 0x33, 0x3c, 0xec, 0xd4, 0x6d, + 0x23, 0x63, 0x18, 0x41, 0xff, 0x36, 0xbe, 0x9e, 0xc8, 0x2e, 0x5d, 0xe6, 0x82, 0xe4, 0x30, 0x6f, 0xf0, 0x6d, 0x61, + 0xfc, 0x25, 0xd9, 0x8d, 0xd6, 0xc9, 0xba, 0xa7, 0x35, 0xba, 0x7b, 0x69, 0xc3, 0x17, 0x1c, 0xa0, 0xf3, 0x4b, 0x1c, + 0xea, 0xd1, 0x14, 0x58, 0xee, 0xf3, 0xa6, 0x3e, 0x41, 0xa6, 0xf1, 0xb0, 0xb6, 0x03, 0x72, 0x8d, 0xe7, 0xba, 0x8d, + 0x1a, 0xf5, 0x1d, 0x5b, 0xa6, 0xb7, 0xc4, 0x56, 0xde, 0xdb, 0x6c, 0x83, 0x39, 0x50, 0xf5, 0xdf, 0x3e, 0x44, 0x22, + 0x18, 0x49, 0xd3, 0x3e, 0x47, 0xeb, 0x77, 0x2e, 0xcf, 0xfc, 0xeb, 0xcc, 0xd1, 0x86, 0x95, 0x61, 0x46, 0x83, 0x19, + 0x5f, 0xe9, 0xce, 0xd0, 0xcc, 0x6b, 0xe6, 0x1e, 0xb8, 0xdd, 0x4b, 0xef, 0xc6, 0x9a, 0x35, 0xfa, 0x61, 0xba, 0x53, + 0x92, 0x59, 0xe0, 0x74, 0xfc, 0x9b, 0xa0, 0xa7, 0x82, 0xf4, 0xa3, 0x3a, 0xb0, 0xf8, 0x8e, 0x93, 0x98, 0x90, 0x0c, + 0x39, 0x58, 0x90, 0xab, 0xe6, 0xbd, 0xa7, 0xdb, 0x5e, 0x9b, 0xb2, 0x46, 0x5c, 0x3a, 0x5d, 0x7d, 0x79, 0xbd, 0xf0, + 0x02, 0xed, 0xf1, 0xde, 0x8f, 0x36, 0xde, 0xd0, 0xc9, 0xe3, 0x0d, 0x54, 0x44, 0xfc, 0x86, 0xdc, 0xd0, 0x18, 0x5f, + 0x85, 0x29, 0x03, 0xc7, 0x7c, 0xef, 0xae, 0xbd, 0x69, 0xee, 0xf1, 0x8b, 0xb9, 0x56, 0x67, 0x4e, 0xb4, 0x57, 0x66, + 0xbd, 0x32, 0x71, 0xb1, 0xa0, 0x24, 0x1f, 0x1e, 0x10, 0x5c, 0xc7, 0x3f, 0xad, 0x56, 0xe1, 0xae, 0xc7, 0x0f, 0x72, + 0xb0, 0x14, 0x03, 0xd3, 0x0d, 0x5c, 0x07, 0x62, 0x1d, 0xc6, 0x16, 0x69, 0x60, 0xa9, 0x1f, 0xca, 0x88, 0x51, 0x30, + 0x7e, 0x7e, 0xbc, 0x8c, 0x7a, 0xc7, 0x7f, 0x58, 0x02, 0x58, 0xb7, 0x11, 0x8e, 0x40, 0x33, 0x2b, 0x4e, 0x39, 0x1f, + 0x17, 0xfa, 0x08, 0xae, 0x6c, 0xca, 0xbe, 0x61, 0xe0, 0x90, 0x15, 0x98, 0xf6, 0x47, 0x43, 0xe5, 0xf7, 0x4f, 0xe4, + 0xc7, 0xb5, 0xbb, 0xdf, 0x6b, 0xd3, 0xc6, 0x0c, 0x47, 0x8f, 0x90, 0x89, 0x0e, 0xe6, 0x40, 0x87, 0x47, 0xc3, 0x62, + 0xca, 0x8e, 0x9b, 0xda, 0xb3, 0x1a, 0x6f, 0xc9, 0xf1, 0x18, 0x7e, 0xad, 0xa2, 0xd9, 0x78, 0x90, 0x6e, 0xab, 0x5c, + 0xcf, 0x76, 0x94, 0x6f, 0x7e, 0xe8, 0x34, 0xd9, 0xc2, 0x37, 0xfa, 0xd7, 0x39, 0xb4, 0x68, 0xbe, 0x46, 0xb4, 0xc8, + 0x1a, 0xea, 0x03, 0xf0, 0xe3, 0x42, 0x63, 0xcd, 0x63, 0x28, 0x08, 0x9b, 0x9b, 0xd6, 0xb5, 0x0d, 0x0d, 0x9a, 0x39, + 0x79, 0x27, 0x48, 0x51, 0x00, 0x89, 0x3b, 0x56, 0xa1, 0xa7, 0x73, 0x10, 0x18, 0x3c, 0xf6, 0x3e, 0xb5, 0x6e, 0x4c, + 0x51, 0x97, 0x7b, 0x4c, 0x34, 0x76, 0xb3, 0x6f, 0x8b, 0xf6, 0xe9, 0x57, 0xfa, 0x8f, 0xc8, 0x85, 0x08, 0x0c, 0x9e, + 0x1f, 0x00, 0xfb, 0x38, 0xb0, 0x15, 0xcd, 0x26, 0x95, 0x37, 0x7c, 0x6e, 0x5f, 0x7f, 0xee, 0xcb, 0xa7, 0xd9, 0x5c, + 0x20, 0xd1, 0xf7, 0xe7, 0xa6, 0x4e, 0xa6, 0x2a, 0xd7, 0x72, 0x07, 0xbb, 0x38, 0x9a, 0x86, 0x18, 0x2d, 0x00, 0x1a, + 0x65, 0x20, 0xf8, 0x09, 0x3e, 0x52, 0x67, 0xfc, 0xf3, 0x79, 0x97, 0xe7, 0x74, 0xff, 0xe1, 0x2d, 0x99, 0xde, 0xd2, + 0x1c, 0xf0, 0x6d, 0xc8, 0xff, 0xed, 0xbf, 0xd1, 0xad, 0x63, 0xac, 0x08, 0xcc, 0x0e, 0xae, 0xcd, 0xa2, 0x5c, 0x7a, + 0x5b, 0x9b, 0xb8, 0xf2, 0x71, 0xf6, 0x03, 0xdc, 0xe6, 0xbe, 0x11, 0x18, 0x4d, 0xe1, 0x63, 0x16, 0x93, 0xb6, 0xca, + 0x75, 0xd3, 0x13, 0x66, 0xdb, 0xe8, 0x12, 0xa9, 0x21, 0xb8, 0xde, 0xc7, 0xb2, 0xd8, 0x78, 0x32, 0x92, 0xd5, 0xf6, + 0xc5, 0x53, 0x01, 0x2e, 0x34, 0x96, 0x7f, 0xa2, 0xce, 0xdb, 0x3d, 0x6a, 0x93, 0xd3, 0xfe, 0x87, 0xd6, 0xee, 0xb9, + 0x54, 0x74, 0x6d, 0x8f, 0x4d, 0x9f, 0x5a, 0x0b, 0x86, 0x60, 0xdf, 0x92, 0x15, 0x7b, 0x01, 0xd0, 0x0e, 0xf0, 0x42, + 0xb5, 0x89, 0x6e, 0xab, 0xfe, 0xb1, 0x07, 0xa4, 0x31, 0xbe, 0xc7, 0x24, 0x55, 0x6e, 0x64, 0x42, 0xcd, 0x22, 0x41, + 0xd1, 0x71, 0x7c, 0x7c, 0x47, 0x5b, 0xad, 0x87, 0x17, 0x62, 0x55, 0x0a, 0x63, 0xcb, 0xdc, 0x9b, 0x32, 0xc8, 0x69, + 0xaa, 0x0f, 0x49, 0x0b, 0xb7, 0x0d, 0x5d, 0x0a, 0x1f, 0x8b, 0x47, 0xad, 0x76, 0x20, 0x27, 0x1b, 0x08, 0xe1, 0x88, + 0xce, 0x5f, 0x4a, 0x9d, 0x02, 0xbc, 0x0e, 0xdc, 0x15, 0xc7, 0xb0, 0x6c, 0xc7, 0xdd, 0xa8, 0xd5, 0x16, 0xfe, 0xec, + 0x00, 0xd4, 0xb0, 0xae, 0xda, 0xed, 0x1d, 0xf5, 0xba, 0x4c, 0x61, 0x94, 0x0a, 0x09, 0x08, 0x87, 0xcb, 0xd9, 0xa4, + 0x20, 0x94, 0x04, 0x8c, 0x55, 0x51, 0xfd, 0xa1, 0xcc, 0x6d, 0xb7, 0x1b, 0x35, 0xe7, 0x91, 0x78, 0x18, 0xa8, 0x58, + 0x8f, 0x69, 0x6d, 0xe6, 0xe0, 0x80, 0x42, 0xd4, 0x6c, 0x7a, 0x2c, 0x7f, 0x58, 0x8f, 0xe4, 0x52, 0xf0, 0x48, 0xc4, + 0xe2, 0x6d, 0x8f, 0xd1, 0xe4, 0x8f, 0x67, 0xc8, 0xec, 0x2d, 0x17, 0x3f, 0xcc, 0xe1, 0x76, 0x62, 0x97, 0x01, 0x4f, + 0x30, 0x31, 0x35, 0xea, 0xc9, 0x56, 0xf4, 0x14, 0x90, 0x0e, 0xb3, 0x82, 0x01, 0xc2, 0x29, 0xf5, 0xcb, 0x68, 0xcc, + 0x9b, 0xcb, 0x95, 0x5b, 0x89, 0x46, 0xb4, 0x94, 0x85, 0xb6, 0xdc, 0x96, 0x1f, 0x26, 0x94, 0xac, 0xb8, 0xa6, 0xb6, + 0x99, 0xad, 0xa2, 0x45, 0x2b, 0x08, 0x7f, 0x5c, 0xcd, 0x8c, 0xa8, 0xbf, 0x90, 0x6e, 0xd6, 0x74, 0x77, 0x06, 0x69, + 0x35, 0xa7, 0x76, 0x76, 0x8e, 0xe6, 0x82, 0x06, 0xea, 0x35, 0x82, 0x8c, 0xc5, 0xa5, 0x26, 0xe5, 0xac, 0x73, 0xa1, + 0xc6, 0x1b, 0x86, 0xaf, 0x9b, 0xa4, 0x5e, 0x94, 0x36, 0xae, 0x6e, 0x74, 0xea, 0x4b, 0xd0, 0xc1, 0xa0, 0x83, 0x84, + 0x94, 0x5a, 0x85, 0x8a, 0xec, 0xd3, 0xc5, 0xba, 0x70, 0x9a, 0x90, 0x74, 0xba, 0xe2, 0xe5, 0xa4, 0x78, 0xcf, 0x08, + 0x71, 0xf4, 0x03, 0x52, 0x26, 0x8f, 0x50, 0x93, 0xbc, 0xf6, 0x01, 0x65, 0xf2, 0x34, 0x6a, 0x71, 0xd8, 0xd0, 0x06, + 0x11, 0x0f, 0x06, 0xc7, 0xe3, 0x08, 0x52, 0xc1, 0x7a, 0x4a, 0x46, 0x97, 0x00, 0x49, 0x2f, 0xc9, 0xd3, 0x03, 0x0b, + 0xa6, 0xe6, 0x4e, 0x29, 0x28, 0x9e, 0x0c, 0x30, 0xb4, 0x95, 0x46, 0x65, 0xc9, 0x0c, 0x45, 0x0f, 0x74, 0xeb, 0xf7, + 0x14, 0x0a, 0x18, 0x23, 0xce, 0x1e, 0xfb, 0xdc, 0x04, 0x10, 0x14, 0x87, 0x35, 0x08, 0xdd, 0x67, 0x04, 0x1b, 0x79, + 0x46, 0xc1, 0x22, 0xcf, 0x07, 0xe4, 0xa8, 0xec, 0x65, 0x35, 0xf7, 0x5f, 0xce, 0x90, 0x0d, 0x0c, 0x1e, 0xd5, 0x93, + 0x4e, 0xae, 0xf5, 0xeb, 0x70, 0x82, 0x9c, 0xd1, 0xa7, 0xac, 0x9e, 0xb4, 0x73, 0x53, 0x4f, 0xd1, 0xac, 0x50, 0x7f, + 0xe6, 0x1e, 0x5e, 0xe1, 0x5b, 0x39, 0x33, 0xca, 0x22, 0x15, 0xf1, 0xc2, 0x0f, 0x60, 0xe3, 0xe7, 0x59, 0xc7, 0xe0, + 0xf0, 0xc4, 0xd9, 0xea, 0x84, 0x38, 0xc4, 0x35, 0x39, 0xf8, 0xb8, 0x45, 0x8c, 0x1a, 0x34, 0x26, 0xb7, 0xa8, 0xd6, + 0x94, 0x78, 0x0b, 0xf5, 0xa9, 0xc1, 0x50, 0x1b, 0x27, 0x5d, 0x59, 0x09, 0x26, 0x34, 0xbc, 0xe4, 0x53, 0x25, 0xeb, + 0x28, 0x56, 0xf8, 0xe5, 0x0a, 0x30, 0x1b, 0x98, 0xe6, 0xae, 0x13, 0x0c, 0x56, 0x9a, 0x53, 0x33, 0xf2, 0xea, 0xdc, + 0x21, 0x94, 0xba, 0xd1, 0x0b, 0x98, 0x00, 0x86, 0x43, 0x46, 0x1b, 0xf4, 0xf2, 0xc2, 0x97, 0x0b, 0x52, 0xb5, 0x23, + 0x87, 0x0c, 0x16, 0x39, 0x91, 0x06, 0x87, 0xf8, 0x9f, 0x09, 0x41, 0xd2, 0x66, 0x07, 0xe2, 0xcd, 0xb1, 0x9b, 0x3a, + 0x56, 0x3d, 0x07, 0xf9, 0xdd, 0x0d, 0xf6, 0x5a, 0xf1, 0xda, 0x34, 0xa9, 0xa1, 0x57, 0xa3, 0x71, 0x28, 0x48, 0xcb, + 0x8b, 0xd9, 0x95, 0x27, 0x4d, 0xa2, 0xdb, 0xd2, 0x55, 0x83, 0x1e, 0xc2, 0x3b, 0xf3, 0x90, 0xdf, 0xf0, 0xbe, 0x9e, + 0xcc, 0x05, 0x45, 0x87, 0x70, 0x0d, 0xb9, 0x89, 0x44, 0xfd, 0x44, 0x57, 0x6c, 0x41, 0x59, 0xec, 0x67, 0xa8, 0x03, + 0xbc, 0xb4, 0x38, 0x41, 0x61, 0x8f, 0xd4, 0xb8, 0xe0, 0xb6, 0x27, 0x0c, 0x53, 0xeb, 0xb2, 0x70, 0xd9, 0xe9, 0xb6, + 0x68, 0x72, 0x2d, 0x50, 0x0c, 0x02, 0xcd, 0x79, 0xfe, 0x7a, 0x7b, 0xea, 0x1a, 0xcf, 0xe0, 0x74, 0xec, 0x60, 0x74, + 0x32, 0xe3, 0x2a, 0x61, 0x83, 0xa8, 0xc3, 0x5d, 0xba, 0x69, 0x20, 0x97, 0x3d, 0xa8, 0x6e, 0x9e, 0xf7, 0xa7, 0xb3, + 0x6b, 0xe3, 0xad, 0x06, 0xd0, 0x1e, 0x00, 0xca, 0x8b, 0x5d, 0xfa, 0xc0, 0x89, 0x9b, 0x76, 0xf7, 0x25, 0xd6, 0x1b, + 0xa8, 0x91, 0x88, 0x20, 0x0a, 0x48, 0x98, 0xfa, 0xe7, 0x4e, 0xd9, 0xf4, 0xf1, 0x1d, 0xaf, 0x3a, 0x51, 0xa8, 0x90, + 0x34, 0x70, 0x8d, 0xa3, 0x87, 0x43, 0x1b, 0x73, 0xc0, 0x1a, 0xe3, 0x44, 0xb8, 0xdf, 0x62, 0xdf, 0xb5, 0x56, 0x1c, + 0xd7, 0x65, 0xb8, 0xe8, 0x3b, 0x45, 0x35, 0x07, 0xc3, 0xab, 0xc3, 0xe3, 0x3c, 0xf8, 0x15, 0xaa, 0xa8, 0xe4, 0xdb, + 0x2e, 0x47, 0x1e, 0x57, 0xa0, 0xcb, 0xf9, 0xb6, 0xbd, 0xbf, 0xc1, 0x30, 0x80, 0x28, 0xf0, 0x41, 0x15, 0xbb, 0x54, + 0x39, 0xb1, 0x3e, 0x70, 0xd6, 0x08, 0x32, 0xaf, 0x22, 0xc4, 0x2b, 0x2e, 0xf9, 0x7d, 0x07, 0x80, 0x5d, 0xb9, 0xca, + 0xb2, 0xae, 0x2b, 0xff, 0x6f, 0x86, 0x11, 0x42, 0xc6, 0xd0, 0xb1, 0x6f, 0xb7, 0xe4, 0x34, 0x06, 0xf5, 0x74, 0xdc, + 0xec, 0x4d, 0x3c, 0x37, 0x0e, 0x5c, 0x00, 0x14, 0xb1, 0x7c, 0xcd, 0x13, 0xde, 0x45, 0x9c, 0x05, 0x88, 0x0d, 0x92, + 0xcf, 0x60, 0xca, 0x71, 0xbf, 0xbe, 0x96, 0x2c, 0xab, 0x38, 0x73, 0x50, 0x1f, 0x9c, 0xfb, 0xa7, 0xe6, 0xf0, 0xb2, + 0x4d, 0x31, 0x0e, 0xc7, 0x8f, 0x3f, 0xd0, 0x55, 0x0c, 0xac, 0x54, 0x7b, 0x20, 0x2d, 0x98, 0xf7, 0x5a, 0xa1, 0x61, + 0xa1, 0xf5, 0xa1, 0x4f, 0x4d, 0xe6, 0x7d, 0xfc, 0x78, 0x55, 0x3d, 0xd0, 0x01, 0x3a, 0xb9, 0x43, 0x69, 0x7f, 0x68, + 0xa9, 0x6f, 0x56, 0xbf, 0x44, 0x05, 0x76, 0x99, 0x83, 0xdd, 0x1e, 0xc7, 0x39, 0x9b, 0x15, 0xd9, 0xd1, 0x2f, 0x44, + 0x97, 0x09, 0x3b, 0x7c, 0x9c, 0x9a, 0xe6, 0x0f, 0xb0, 0x2b, 0x5f, 0x6e, 0xfe, 0x44, 0x09, 0x4c, 0xd4, 0xd9, 0x60, + 0x1f, 0x01, 0xd0, 0x7d, 0xf0, 0x79, 0x82, 0xe4, 0xe3, 0xfa, 0x71, 0xf7, 0x5f, 0xfb, 0x03, 0xd4, 0x79, 0x57, 0x62, + 0xd9, 0x40, 0x9c, 0xb8, 0x42, 0x02, 0xda, 0x14, 0x42, 0x7f, 0x2a, 0xe5, 0x65, 0x1c, 0x8a, 0x67, 0x4d, 0x07, 0x95, + 0xbb, 0xb9, 0x4a, 0x26, 0xa0, 0xc1, 0x9b, 0x64, 0x96, 0xfd, 0x98, 0x0e, 0x7b, 0xa9, 0x69, 0xea, 0x27, 0x73, 0x5d, + 0x59, 0xad, 0xa6, 0x7c, 0xbb, 0x7d, 0x57, 0x7e, 0xba, 0xe9, 0x09, 0xd2, 0x78, 0xcf, 0x03, 0xb7, 0x75, 0xdf, 0xc8, + 0x1a, 0x0c, 0xf0, 0xcd, 0xc2, 0xa8, 0xca, 0xe9, 0x08, 0x85, 0xa8, 0x98, 0x07, 0x7f, 0x01, 0x62, 0x3c, 0xac, 0xc6, + 0xf1, 0x93, 0x4e, 0x27, 0xc0, 0x32, 0xfb, 0xf2, 0x66, 0x63, 0x1d, 0xb1, 0x27, 0x30, 0xbc, 0xa8, 0xcc, 0x15, 0x2f, + 0xd1, 0x31, 0x70, 0xdb, 0xbb, 0xb2, 0x4a, 0xa6, 0xcb, 0xe7, 0xbe, 0x0d, 0x0a, 0x5f, 0x1f, 0x90, 0x20, 0x05, 0x2a, + 0x05, 0xf6, 0xc1, 0xe6, 0xfb, 0x08, 0x68, 0x1e, 0xe7, 0xaa, 0x9e, 0xae, 0xdb, 0xab, 0x2d, 0xda, 0x6f, 0xe1, 0x88, + 0xad, 0xad, 0x82, 0x3d, 0xec, 0xe5, 0xbc, 0x77, 0x7a, 0xf3, 0xe0, 0x17, 0xa6, 0x61, 0x16, 0x12, 0xef, 0x36, 0xea, + 0x1b, 0xd6, 0x6b, 0xb6, 0xf4, 0x99, 0xcc, 0x9a, 0x78, 0x98, 0xac, 0xa7, 0x91, 0x87, 0x93, 0x53, 0x79, 0x8e, 0xcd, + 0x63, 0x61, 0x81, 0x37, 0x74, 0xf5, 0xf4, 0x9a, 0x29, 0x3e, 0x9a, 0x8a, 0xe4, 0x25, 0x3e, 0xb9, 0x8a, 0x16, 0x80, + 0x63, 0xa2, 0x72, 0x7a, 0xed, 0x02, 0x27, 0xd8, 0xeb, 0x45, 0x09, 0x0d, 0x8e, 0x91, 0x63, 0x5b, 0x82, 0xa7, 0xa3, + 0x33, 0x31, 0x6b, 0x5c, 0x40, 0xfa, 0x9a, 0xac, 0xbf, 0xae, 0x42, 0x9a, 0x91, 0x49, 0x06, 0x1f, 0x3d, 0x4b, 0x53, + 0x37, 0x2f, 0x37, 0x80, 0xc0, 0x51, 0xf1, 0xbe, 0x0b, 0x64, 0x79, 0xc3, 0x90, 0x3c, 0xc9, 0xc1, 0x4a, 0xb7, 0x27, + 0xb8, 0x09, 0xc1, 0xff, 0xf9, 0xdd, 0xc2, 0x4a, 0xa6, 0x22, 0x97, 0x63, 0x14, 0xa2, 0xd8, 0x3d, 0xe7, 0x06, 0x73, + 0x53, 0xc9, 0x55, 0x02, 0xb5, 0xfc, 0x83, 0xed, 0xcf, 0x6a, 0x48, 0x72, 0xe6, 0x0b, 0xc8, 0x8b, 0xd9, 0x45, 0x28, + 0x70, 0x56, 0x6f, 0x51, 0xc4, 0x06, 0x82, 0x3d, 0xe6, 0x5a, 0xd3, 0xc3, 0x1c, 0x48, 0x66, 0x35, 0xc0, 0x68, 0x4b, + 0x04, 0xa9, 0x17, 0xec, 0xec, 0x52, 0xd1, 0x7d, 0x5d, 0x50, 0xa4, 0xbb, 0x2c, 0x11, 0x53, 0x69, 0x25, 0xc7, 0xe7, + 0x2d, 0xf6, 0xd7, 0x9a, 0xaa, 0xa5, 0xbe, 0xca, 0xce, 0x31, 0xa6, 0xa7, 0xe3, 0x4f, 0x1b, 0x3f, 0x12, 0x7e, 0x9f, + 0x2b, 0x66, 0x30, 0x1b, 0x86, 0xd1, 0x2e, 0x61, 0xd2, 0x50, 0x7d, 0xa6, 0x38, 0x6e, 0x2c, 0x37, 0x5e, 0x6e, 0x5f, + 0x74, 0xc5, 0x56, 0xe9, 0x9f, 0xbb, 0x05, 0xbe, 0x26, 0xdd, 0x6b, 0x32, 0x2f, 0x48, 0x6c, 0xf0, 0x44, 0xf7, 0x60, + 0x9d, 0xa8, 0xae, 0xfd, 0xcb, 0xf3, 0xd3, 0x84, 0x10, 0xb3, 0x6d, 0x2b, 0xf2, 0xca, 0x0a, 0x50, 0x0e, 0x69, 0x37, + 0x01, 0xf5, 0xa5, 0x1b, 0xce, 0x83, 0xba, 0xb1, 0x81, 0x97, 0x90, 0x5a, 0x03, 0xc5, 0x2e, 0x8c, 0x7d, 0x75, 0x3a, + 0x0a, 0x69, 0x72, 0x26, 0x7b, 0x48, 0x28, 0x26, 0x0c, 0xd0, 0x3f, 0x2d, 0x8e, 0x66, 0x54, 0xd0, 0x7a, 0x77, 0x45, + 0x75, 0x2c, 0x3b, 0xd7, 0x40, 0x94, 0x99, 0x8d, 0x66, 0xda, 0x41, 0x86, 0x37, 0x0e, 0x91, 0xef, 0x32, 0xd3, 0xd1, + 0x81, 0x1d, 0x53, 0xee, 0xa4, 0x0e, 0x1b, 0x57, 0xd9, 0x91, 0x04, 0xf6, 0xbd, 0xcc, 0x89, 0x50, 0xf8, 0x66, 0xb6, + 0x3c, 0x90, 0xaf, 0x75, 0xe5, 0x7f, 0xcd, 0xa8, 0xcf, 0x0a, 0x77, 0xb4, 0x2d, 0x57, 0x33, 0x0e, 0x63, 0xc3, 0x81, + 0xcc, 0xc7, 0x07, 0x26, 0x78, 0xe5, 0xa9, 0x2a, 0xfb, 0x4d, 0xd8, 0x65, 0x0f, 0xec, 0xd9, 0xe4, 0x28, 0x2d, 0x1d, + 0xb5, 0xff, 0xb5, 0xcb, 0xa2, 0x43, 0xd1, 0xb0, 0x68, 0x5d, 0x24, 0x88, 0x5a, 0x6d, 0xf1, 0xc3, 0x3c, 0x22, 0x41, + 0xed, 0x8b, 0xc5, 0x4b, 0x7b, 0xe0, 0xa3, 0x29, 0x06, 0xbe, 0xcf, 0x58, 0x3c, 0x89, 0xbe, 0x3f, 0xc2, 0x49, 0x19, + 0x28, 0x1d, 0x3a, 0x03, 0xd2, 0xc4, 0x2a, 0x1e, 0x93, 0x3c, 0x67, 0xb1, 0xc2, 0x5e, 0xf2, 0x3a, 0x2a, 0x83, 0x16, + 0xc9, 0x3f, 0x47, 0x7c, 0xd0, 0xe0, 0x18, 0x3c, 0x8a, 0xbc, 0xf4, 0x4b, 0x70, 0xcb, 0x7d, 0x7f, 0xc0, 0x08, 0x26, + 0x54, 0x6f, 0xd2, 0x62, 0xf4, 0x42, 0x44, 0xe6, 0x23, 0x34, 0x1e, 0xbf, 0x6f, 0x0d, 0x5e, 0x50, 0xfa, 0xd2, 0xce, + 0x40, 0x72, 0x13, 0xe8, 0xd2, 0x6e, 0x6a, 0x9c, 0x06, 0x72, 0x22, 0x53, 0xd7, 0x76, 0xdc, 0x77, 0xc3, 0x63, 0x41, + 0x5b, 0x82, 0x8c, 0xe9, 0x2e, 0x34, 0x73, 0x14, 0x18, 0xfe, 0xbd, 0xd5, 0x38, 0x02, 0x06, 0xec, 0x1a, 0xeb, 0xe1, + 0x97, 0x62, 0xdc, 0xa4, 0x4a, 0x3f, 0x5c, 0xe1, 0x9c, 0x5d, 0xd2, 0xe9, 0xcd, 0xef, 0x07, 0x4a, 0x20, 0x2e, 0xde, + 0x88, 0x55, 0xdf, 0x06, 0xf3, 0xcb, 0xa0, 0x00, 0x8c, 0xa9, 0x34, 0x64, 0xfa, 0xbf, 0x58, 0x17, 0xf4, 0x4e, 0x0c, + 0xd6, 0x0c, 0x0e, 0x0c, 0x22, 0x3e, 0xee, 0xe0, 0x1e, 0x7f, 0x1d, 0xfe, 0x37, 0x25, 0xa8, 0x2b, 0x77, 0x3f, 0x51, + 0xd6, 0x7c, 0x9f, 0x94, 0x22, 0xd3, 0x97, 0xef, 0x5e, 0xb6, 0x42, 0x1d, 0xd4, 0xd8, 0xe6, 0x16, 0x35, 0xaf, 0x2d, + 0x7e, 0x3d, 0x8d, 0xc5, 0xdc, 0xe4, 0x37, 0xbd, 0x5d, 0x75, 0xf5, 0xd4, 0xa8, 0x51, 0x4f, 0x08, 0x46, 0x6f, 0x6e, + 0x86, 0xdd, 0x1a, 0x3f, 0xcf, 0x4a, 0x40, 0x23, 0x9b, 0xbd, 0x7a, 0x03, 0x05, 0xb9, 0xae, 0xd6, 0xcf, 0x63, 0x59, + 0x65, 0x5c, 0x7c, 0x47, 0x00, 0x5e, 0x1a, 0x1f, 0x12, 0x55, 0xaa, 0x65, 0x65, 0x88, 0x9a, 0x04, 0x10, 0x1c, 0xfe, + 0xa0, 0x7b, 0x73, 0x69, 0x3f, 0xc5, 0x6d, 0x56, 0xe4, 0xb5, 0x15, 0x41, 0x07, 0x19, 0x6a, 0xba, 0x32, 0xb8, 0x81, + 0x0e, 0x0f, 0xa7, 0xe8, 0x7f, 0x15, 0x7f, 0x58, 0xb1, 0x7f, 0xd2, 0x4d, 0x09, 0xe5, 0x53, 0x33, 0x3b, 0xf1, 0x64, + 0xcf, 0x14, 0xa9, 0x59, 0x84, 0x9a, 0x55, 0x6b, 0x06, 0xcb, 0x86, 0xda, 0x7d, 0x0d, 0x09, 0x5b, 0x04, 0x29, 0xa6, + 0x60, 0xdc, 0xd8, 0x9d, 0x11, 0x70, 0xc4, 0x39, 0x83, 0x72, 0xe8, 0x14, 0x65, 0x7e, 0x33, 0x5c, 0x36, 0x4e, 0xdd, + 0xf4, 0x06, 0x05, 0x7e, 0x18, 0xf0, 0xb9, 0xbc, 0xb5, 0x20, 0xcf, 0x1e, 0x65, 0xc5, 0x74, 0x16, 0xfb, 0x56, 0x02, + 0x31, 0x51, 0xb4, 0x03, 0x5b, 0x5e, 0xf1, 0xf2, 0x74, 0x66, 0xb5, 0x4f, 0x3a, 0xd7, 0x1d, 0xc2, 0xfd, 0x21, 0x71, + 0x1d, 0x84, 0x5e, 0xa7, 0x1c, 0x36, 0x79, 0x3d, 0x29, 0x61, 0xb7, 0x28, 0xbb, 0x2e, 0x16, 0xd3, 0x19, 0x0a, 0xbd, + 0x05, 0xf6, 0xbb, 0xdf, 0x7a, 0xfe, 0xa4, 0x72, 0x8c, 0xeb, 0xc3, 0xe5, 0x24, 0x86, 0xf1, 0xb5, 0xd4, 0x10, 0x2d, + 0x5b, 0x4a, 0xf7, 0x58, 0xdb, 0xb0, 0x80, 0xad, 0xd9, 0xfb, 0x47, 0x22, 0xa5, 0x89, 0x32, 0x15, 0xa7, 0x7d, 0xa1, + 0x32, 0x6e, 0xac, 0x3b, 0xab, 0x77, 0xb5, 0x16, 0x1f, 0xad, 0xce, 0x46, 0x1b, 0xa7, 0x12, 0xec, 0xbd, 0xa1, 0xbb, + 0xe8, 0x0b, 0xa6, 0x6c, 0xa1, 0xef, 0xe0, 0xdd, 0x06, 0x6d, 0x31, 0x3e, 0x63, 0x68, 0x9a, 0xdd, 0x79, 0xe0, 0xc5, + 0x67, 0x59, 0x74, 0xb9, 0x68, 0x3e, 0xcd, 0x1c, 0x69, 0xd4, 0xfd, 0x7f, 0x79, 0x6b, 0xa5, 0x0c, 0x77, 0x79, 0x42, + 0x86, 0x9d, 0xdc, 0xaf, 0x4b, 0x56, 0x01, 0xf9, 0x18, 0x5b, 0xe9, 0x79, 0x65, 0x97, 0x44, 0xa1, 0xa3, 0x38, 0xd3, + 0x7f, 0xf8, 0xca, 0x5d, 0xed, 0x3b, 0x6d, 0xfa, 0xd1, 0x65, 0xc9, 0x5f, 0x59, 0x4e, 0x8a, 0x36, 0x4f, 0x88, 0x4c, + 0xfe, 0x4f, 0x24, 0x25, 0x47, 0x06, 0xe2, 0xd1, 0x01, 0x14, 0x30, 0x53, 0x27, 0x93, 0xd3, 0x62, 0x70, 0x02, 0x22, + 0x4b, 0x34, 0x87, 0x33, 0x80, 0x49, 0x5a, 0x80, 0x09, 0xcf, 0x6b, 0xb5, 0xef, 0x31, 0x35, 0x8f, 0xbf, 0xcc, 0xa3, + 0x19, 0x8a, 0x33, 0x87, 0x16, 0x4d, 0x40, 0x32, 0x92, 0x30, 0xac, 0xb5, 0xed, 0x9c, 0x9f, 0x6c, 0x27, 0x78, 0x42, + 0xbd, 0x3f, 0xe0, 0x96, 0x43, 0x70, 0xb9, 0x13, 0xa5, 0xa8, 0xee, 0x93, 0x2f, 0x5b, 0xbd, 0x39, 0xe4, 0x3a, 0xeb, + 0xa1, 0x1e, 0x19, 0x28, 0x6e, 0xdb, 0xd9, 0x24, 0xfd, 0xf5, 0x8a, 0x7f, 0xfc, 0x65, 0xa2, 0x8b, 0x8a, 0x66, 0x0d, + 0x1a, 0x28, 0x00, 0xb7, 0x31, 0xe7, 0x7b, 0x1d, 0xb7, 0xb6, 0x83, 0xb9, 0x0d, 0x70, 0xb7, 0x51, 0x28, 0x06, 0x73, + 0x3f, 0x4f, 0x18, 0x10, 0xcc, 0x6b, 0x4f, 0x14, 0x20, 0xd2, 0x83, 0xfb, 0xe4, 0x54, 0x72, 0x99, 0x8d, 0x20, 0x58, + 0xc3, 0x2c, 0xe8, 0x76, 0xd7, 0xac, 0xcb, 0x8c, 0x3f, 0xf9, 0x21, 0xc3, 0x35, 0xd0, 0x3f, 0x99, 0x28, 0xe9, 0xdc, + 0x90, 0x50, 0xd1, 0x83, 0x78, 0x99, 0x43, 0xe5, 0x79, 0xcf, 0x50, 0x4f, 0xaf, 0x3f, 0xfa, 0xfb, 0xd6, 0xcc, 0xa1, + 0xbc, 0x64, 0x4d, 0xfe, 0xee, 0x31, 0xaf, 0x67, 0x79, 0x45, 0x67, 0xbe, 0x9a, 0x75, 0x56, 0x5c, 0x64, 0x9c, 0x1d, + 0x91, 0x0a, 0x4e, 0xad, 0x68, 0x7d, 0xe2, 0x29, 0x36, 0x8d, 0xdf, 0x1b, 0xa4, 0xce, 0x1e, 0x99, 0x7b, 0x76, 0x50, + 0x51, 0x5a, 0x42, 0x81, 0xf5, 0x22, 0x6a, 0xe0, 0xdb, 0x23, 0x9b, 0x31, 0xd3, 0xe7, 0xa4, 0xc0, 0x8b, 0x96, 0x60, + 0xb3, 0xbc, 0xd4, 0x41, 0x13, 0x2f, 0x4b, 0xe6, 0x8a, 0x13, 0xfe, 0x74, 0x99, 0x29, 0xf6, 0x43, 0x46, 0xea, 0x60, + 0xcf, 0x8b, 0x15, 0x7b, 0x96, 0xcb, 0xa7, 0xcb, 0x87, 0x68, 0x93, 0x7b, 0x8f, 0x88, 0x19, 0xaf, 0x1f, 0x2f, 0xda, + 0xa4, 0x04, 0x94, 0xc8, 0xc8, 0x86, 0x71, 0x1b, 0x09, 0x35, 0x8a, 0xf2, 0xd1, 0x15, 0x28, 0x39, 0xd6, 0xa9, 0x08, + 0x00, 0xf8, 0x63, 0x3a, 0x14, 0x36, 0xf0, 0x60, 0x3e, 0x91, 0x80, 0x32, 0xf2, 0xf4, 0x9d, 0xc9, 0x90, 0x10, 0x1d, + 0x35, 0x33, 0x7c, 0x4f, 0x18, 0xab, 0x67, 0x1e, 0x1d, 0x1f, 0x45, 0x1d, 0x6e, 0x84, 0x81, 0xc4, 0xb2, 0x6c, 0xb2, + 0x9b, 0xb7, 0x6e, 0x2b, 0x7c, 0x57, 0xac, 0x40, 0x9a, 0x02, 0x34, 0x2f, 0xe3, 0x46, 0xc0, 0x69, 0x18, 0xb3, 0x2f, + 0x03, 0xd4, 0x58, 0xc1, 0x58, 0x7e, 0xb5, 0xb2, 0xe1, 0xd9, 0x24, 0xef, 0x7e, 0x74, 0x99, 0x0b, 0x84, 0xbc, 0x58, + 0x60, 0x5b, 0x12, 0x75, 0xe2, 0x37, 0x83, 0xdf, 0xd3, 0xef, 0xd5, 0xf4, 0xd1, 0xc6, 0x88, 0x36, 0x3a, 0xcb, 0x4d, + 0x0f, 0x7a, 0xb4, 0x5b, 0xb0, 0x6a, 0x21, 0x52, 0xcd, 0xf1, 0x30, 0x03, 0x1b, 0xd1, 0x97, 0xd8, 0x60, 0xf5, 0x83, + 0x8d, 0x02, 0xc9, 0xc2, 0x90, 0x6d, 0x9b, 0x3d, 0x36, 0x30, 0x04, 0xe5, 0x59, 0x35, 0x05, 0x58, 0x23, 0xb6, 0xab, + 0x14, 0x46, 0x93, 0x7f, 0xd5, 0x16, 0xfd, 0x27, 0xff, 0x53, 0xac, 0xf7, 0x4c, 0x80, 0x64, 0x7b, 0x38, 0x9f, 0x9d, + 0xa6, 0x05, 0x33, 0x78, 0x14, 0x84, 0xf6, 0x60, 0x4a, 0xcd, 0x49, 0x24, 0x06, 0x25, 0x17, 0x22, 0xfb, 0x93, 0xea, + 0x2d, 0xc7, 0x67, 0x1e, 0x2a, 0xbf, 0xb9, 0x93, 0xe2, 0xa4, 0xd3, 0xea, 0x52, 0x19, 0xc1, 0x5d, 0x81, 0x13, 0x94, + 0x60, 0x36, 0xa0, 0x7f, 0xf2, 0xdb, 0x4d, 0x48, 0xa2, 0x4f, 0x5d, 0x60, 0x28, 0x63, 0xf6, 0x8c, 0xc8, 0xcc, 0xc2, + 0x23, 0x5a, 0x85, 0x28, 0xc6, 0x05, 0x72, 0xc0, 0x6c, 0x3f, 0x1b, 0x59, 0xb0, 0xd5, 0xb0, 0x9f, 0xfb, 0x46, 0xb4, + 0x0f, 0x61, 0x32, 0x62, 0x73, 0xe2, 0x2d, 0xc9, 0x03, 0x68, 0x88, 0x1e, 0xe6, 0x42, 0xe3, 0x82, 0x97, 0xae, 0x52, + 0xa3, 0x14, 0xe8, 0x26, 0x1e, 0xf5, 0x76, 0x68, 0xd4, 0x6a, 0x79, 0x33, 0x46, 0x17, 0xc0, 0x21, 0xaf, 0xf7, 0x4f, + 0xf0, 0xd4, 0x63, 0x86, 0xd8, 0x8b, 0x37, 0x1c, 0x58, 0xad, 0x71, 0xb1, 0x9d, 0x13, 0x37, 0x45, 0xc1, 0xc5, 0x99, + 0x4a, 0x7f, 0xb7, 0x85, 0xff, 0xad, 0xbc, 0xbb, 0x2a, 0xb2, 0x26, 0x28, 0x3f, 0x08, 0xce, 0xdc, 0xf3, 0x02, 0x3e, + 0x59, 0xe9, 0x74, 0xf8, 0x8d, 0xd2, 0x7c, 0x70, 0xf3, 0x84, 0xd1, 0x16, 0x6e, 0xaf, 0x30, 0x57, 0xe1, 0x0a, 0x96, + 0x11, 0xda, 0x67, 0xdc, 0x7a, 0xfc, 0xb9, 0x68, 0x8c, 0x29, 0x47, 0xe7, 0x1c, 0xe4, 0x67, 0x84, 0x04, 0xd3, 0xc0, + 0x26, 0x3d, 0xda, 0x61, 0x99, 0x16, 0x48, 0x09, 0x42, 0x4e, 0x2a, 0xba, 0x1f, 0xc3, 0x50, 0x89, 0xcd, 0x24, 0x24, + 0xad, 0x2a, 0x76, 0xe8, 0xc4, 0x29, 0x37, 0x1b, 0xa6, 0x58, 0x23, 0x7c, 0xba, 0xe9, 0x67, 0x88, 0x92, 0xc8, 0x7b, + 0x2e, 0x6e, 0x46, 0x1d, 0xbc, 0x22, 0x53, 0xc5, 0xd2, 0x57, 0x9e, 0x70, 0xeb, 0xaf, 0xb5, 0x0f, 0x90, 0xef, 0x10, + 0x0a, 0x7a, 0x5c, 0xe5, 0x5f, 0xce, 0x61, 0x56, 0xf2, 0x12, 0xae, 0xf0, 0x53, 0x1c, 0xca, 0x5c, 0x54, 0xd0, 0xe3, + 0xb9, 0x08, 0xf1, 0x96, 0xc3, 0x5b, 0x05, 0x9f, 0x44, 0x5f, 0x24, 0xc2, 0x7d, 0xcb, 0xce, 0xa6, 0xcf, 0x4a, 0x78, + 0xfd, 0xb9, 0x39, 0x29, 0x05, 0xd7, 0x81, 0x46, 0xcf, 0x61, 0xe0, 0x65, 0xd0, 0x62, 0xec, 0xd4, 0xc0, 0x3d, 0x91, + 0xec, 0x5b, 0x7f, 0x60, 0x49, 0xf5, 0xd3, 0x0f, 0x1a, 0x10, 0xcf, 0xd4, 0x7f, 0x3b, 0x30, 0xf1, 0x58, 0xfe, 0x91, + 0xe7, 0x3f, 0x93, 0x44, 0xd5, 0xc5, 0x03, 0x6c, 0x9d, 0x64, 0x0b, 0x05, 0x14, 0x1d, 0x1e, 0x10, 0xb0, 0x68, 0x6f, + 0x57, 0x69, 0x99, 0x9d, 0x30, 0x87, 0x3c, 0xdd, 0x55, 0xaf, 0xb3, 0x04, 0xa7, 0xaf, 0xd6, 0xb3, 0x15, 0xe8, 0xb4, + 0xb0, 0x00, 0x94, 0x38, 0xb3, 0x44, 0x75, 0xc6, 0xc1, 0xa9, 0xc5, 0x67, 0xfc, 0xaf, 0x57, 0x2a, 0x61, 0xec, 0xc1, + 0xc3, 0x41, 0x75, 0xa1, 0x82, 0xfc, 0xec, 0x85, 0xa6, 0x34, 0x0c, 0x20, 0xe1, 0x9c, 0xc6, 0x21, 0x59, 0xc6, 0x16, + 0x8f, 0xbc, 0x32, 0x15, 0x3a, 0x81, 0x75, 0xa7, 0x4f, 0xa7, 0x83, 0x60, 0x5c, 0x62, 0x85, 0xc1, 0x6b, 0x2e, 0x0c, + 0x47, 0x5a, 0x2e, 0xa7, 0xf8, 0x2b, 0x4d, 0xd4, 0xb5, 0xc8, 0x26, 0xf3, 0x1a, 0x57, 0x0d, 0xc4, 0x99, 0x76, 0x41, + 0x86, 0xe5, 0x53, 0xe4, 0xd6, 0x62, 0xb9, 0xf6, 0x5b, 0x9f, 0x57, 0x18, 0x86, 0xca, 0xcd, 0xaf, 0xf7, 0xf4, 0xcd, + 0x1d, 0x89, 0x53, 0x2f, 0xde, 0xa2, 0x40, 0xd3, 0x89, 0x5e, 0x0c, 0x35, 0xc2, 0xd3, 0x71, 0x17, 0x91, 0x61, 0x34, + 0xe0, 0xf4, 0x6d, 0x55, 0x33, 0x66, 0xd2, 0x0e, 0xa0, 0x9f, 0x0b, 0xea, 0x1c, 0x00, 0x9a, 0x22, 0x94, 0x1d, 0x08, + 0x57, 0xa1, 0x5a, 0xaf, 0x97, 0x95, 0x36, 0x36, 0x96, 0x07, 0x0a, 0x21, 0x30, 0x2b, 0x5e, 0x52, 0x28, 0xb9, 0x42, + 0x20, 0x2f, 0xb6, 0xa9, 0x4a, 0x65, 0xa6, 0x65, 0xb3, 0x76, 0xd7, 0x15, 0xed, 0x00, 0xa2, 0x26, 0x6d, 0x64, 0x32, + 0x81, 0x0d, 0x15, 0xd2, 0x14, 0x17, 0x49, 0xad, 0x04, 0x5c, 0xf3, 0x61, 0x0a, 0xa6, 0x11, 0x38, 0x3b, 0x80, 0x16, + 0xcc, 0xe1, 0x5e, 0x33, 0x64, 0x9a, 0x3c, 0xa7, 0x7d, 0x46, 0x8f, 0xb6, 0x5a, 0x63, 0xab, 0x5a, 0xb5, 0x8b, 0xfb, + 0xc9, 0x3a, 0x60, 0x62, 0xc0, 0x6a, 0x7b, 0xfc, 0x6f, 0x85, 0x74, 0xe6, 0x63, 0x21, 0x95, 0xfe, 0x6f, 0x46, 0xe7, + 0x62, 0xde, 0x3c, 0x3f, 0x8c, 0x5c, 0x61, 0x4c, 0x85, 0x3c, 0xc6, 0x49, 0x78, 0xb1, 0x1d, 0x5e, 0x34, 0x06, 0xb5, + 0x1f, 0x30, 0x18, 0x72, 0xaa, 0x63, 0xef, 0x7d, 0x10, 0x92, 0x7d, 0x31, 0xb7, 0x68, 0xac, 0x4e, 0x69, 0x51, 0xac, + 0xfb, 0x00, 0x32, 0x28, 0x8a, 0xfd, 0xff, 0xb8, 0x75, 0x91, 0x85, 0xe6, 0x0f, 0xe4, 0x25, 0x2e, 0x79, 0x98, 0xfe, + 0xf8, 0x5d, 0x50, 0xac, 0x4f, 0x1b, 0xf1, 0x12, 0xcd, 0x95, 0x83, 0x7f, 0xd3, 0x65, 0x8b, 0xea, 0x2e, 0xe5, 0xe1, + 0xde, 0x81, 0x31, 0x8d, 0x6f, 0x6e, 0xbe, 0x8c, 0x0b, 0x6b, 0x9c, 0xbb, 0x19, 0xef, 0x70, 0x13, 0xbb, 0xde, 0x56, + 0x56, 0x6c, 0x17, 0x99, 0xa2, 0xa2, 0xa9, 0xd1, 0x47, 0x33, 0x30, 0x76, 0x68, 0x40, 0xfb, 0xb7, 0x18, 0x32, 0x58, + 0x3c, 0xac, 0xcd, 0x85, 0x68, 0x79, 0x9d, 0xcb, 0x1d, 0x05, 0xe7, 0x64, 0xc4, 0x91, 0x04, 0x69, 0xd2, 0x7d, 0xc7, + 0xc9, 0x83, 0x3a, 0xa8, 0x1a, 0x71, 0xa7, 0x9a, 0xec, 0x57, 0xc2, 0xff, 0x21, 0x1f, 0xd7, 0x9d, 0xb6, 0x72, 0x0e, + 0x08, 0xf1, 0x59, 0xe7, 0xcd, 0x09, 0x91, 0x51, 0xdb, 0x46, 0x6d, 0x25, 0xcd, 0xc8, 0xaf, 0x10, 0x89, 0xfa, 0x57, + 0x8c, 0x02, 0x53, 0x7c, 0x06, 0x30, 0xb0, 0x4d, 0x82, 0xd5, 0x6f, 0xd6, 0x0d, 0xd9, 0x52, 0x40, 0xe3, 0x97, 0xb3, + 0x6d, 0x3e, 0xb1, 0x71, 0x3b, 0xfa, 0x05, 0x51, 0xdb, 0x5a, 0xd1, 0x04, 0xd7, 0xdd, 0x0b, 0xab, 0x37, 0xe2, 0xf7, + 0xd4, 0xdb, 0x23, 0xc8, 0x0d, 0xe4, 0x93, 0x74, 0xbf, 0x73, 0xa6, 0x0f, 0xd8, 0x83, 0x31, 0x8e, 0x31, 0xd8, 0x15, + 0xf3, 0xcc, 0xe8, 0x4d, 0x55, 0xd9, 0x04, 0x7a, 0x77, 0xcb, 0x51, 0x71, 0x8f, 0xdf, 0xd2, 0x2f, 0xde, 0x30, 0xc3, + 0xe8, 0x3e, 0x5f, 0x40, 0xd9, 0xa2, 0x1d, 0x57, 0x1a, 0xc9, 0x65, 0xb4, 0x4d, 0xe5, 0x88, 0x12, 0x58, 0x50, 0x92, + 0x1a, 0x5d, 0xde, 0xdc, 0xb2, 0x79, 0x71, 0x1d, 0x4d, 0x28, 0xb7, 0xfe, 0x74, 0xe4, 0x73, 0x3d, 0x38, 0x2a, 0x6f, + 0x43, 0x04, 0xa6, 0x89, 0x36, 0x2c, 0xe0, 0x30, 0xd3, 0xe6, 0xa5, 0x08, 0x02, 0xf0, 0x6e, 0xf0, 0x67, 0x9b, 0x81, + 0x22, 0x17, 0x10, 0x79, 0xe7, 0x2d, 0x58, 0xa0, 0x1b, 0x3c, 0x05, 0xfa, 0x38, 0x36, 0xfc, 0x77, 0xc1, 0xca, 0xd8, + 0x90, 0x2c, 0x61, 0x7c, 0xaf, 0x73, 0x22, 0x39, 0x49, 0x5d, 0x24, 0xad, 0x9f, 0xc2, 0x33, 0xb5, 0x8d, 0x5b, 0xf3, + 0x17, 0xe9, 0x27, 0xd1, 0x50, 0x79, 0x01, 0xf3, 0x35, 0xaa, 0xb3, 0xcb, 0xfc, 0x85, 0x79, 0x4e, 0x7a, 0x66, 0x5e, + 0xa3, 0xd5, 0x1a, 0xf0, 0xc0, 0xd2, 0x8a, 0xb0, 0x94, 0x59, 0x32, 0xe7, 0x32, 0x00, 0xf0, 0xb5, 0xf1, 0x79, 0x6d, + 0x08, 0xf1, 0x89, 0x5d, 0xdf, 0x15, 0x84, 0xca, 0x54, 0xd3, 0xae, 0x33, 0xf7, 0xc9, 0x2a, 0x84, 0xa5, 0xda, 0x76, + 0xc5, 0x6d, 0xa6, 0xb9, 0xad, 0x0d, 0xcf, 0x3d, 0x5f, 0x37, 0x05, 0xa6, 0xe8, 0x0c, 0xfa, 0x3b, 0xdb, 0x88, 0x53, + 0x04, 0x21, 0x62, 0x06, 0x1f, 0xb0, 0x36, 0x82, 0x6c, 0xca, 0x89, 0xfe, 0x6c, 0x17, 0xd4, 0x34, 0xbd, 0x4c, 0x55, + 0x85, 0xcb, 0x39, 0x26, 0x13, 0x9b, 0xb3, 0x01, 0x8b, 0x39, 0x78, 0xf0, 0xf0, 0x36, 0xb7, 0x65, 0xd9, 0x1b, 0x11, + 0xac, 0x06, 0x2d, 0x9c, 0x3b, 0x58, 0x2a, 0xf4, 0x9d, 0xcc, 0x7a, 0x57, 0x07, 0x37, 0xb3, 0xdf, 0xa4, 0xdd, 0x1f, + 0x39, 0xfa, 0xaa, 0xd2, 0xb8, 0x03, 0xdb, 0x58, 0x02, 0x1b, 0x1e, 0x23, 0x52, 0x0e, 0x89, 0xea, 0x53, 0x1f, 0x54, + 0x8f, 0x6a, 0x4c, 0x72, 0x1c, 0x48, 0x87, 0x89, 0x2b, 0x12, 0x7b, 0x93, 0x16, 0x62, 0x57, 0x2a, 0xa4, 0xa7, 0xb3, + 0x90, 0xaf, 0x25, 0x37, 0x5d, 0x27, 0x89, 0x6c, 0x51, 0xfb, 0x90, 0x57, 0x2d, 0xa9, 0x53, 0x83, 0xf2, 0x78, 0xcc, + 0xd1, 0x8f, 0x77, 0x5b, 0xf9, 0x4a, 0x6d, 0x1d, 0xe7, 0x24, 0xf8, 0x1c, 0xc7, 0x8b, 0x86, 0x7f, 0x2e, 0xca, 0x1b, + 0x2d, 0x3c, 0x8f, 0x2b, 0x3f, 0xec, 0xe4, 0xf5, 0x2b, 0x34, 0x4c, 0xc3, 0x51, 0xeb, 0xb6, 0xbc, 0xe2, 0x70, 0xef, + 0x76, 0x62, 0xb1, 0x84, 0xf5, 0x31, 0x2e, 0x97, 0x3c, 0x8d, 0xaa, 0xa5, 0xa3, 0x3f, 0xdd, 0x01, 0xb7, 0xe4, 0x9d, + 0x00, 0x98, 0xe8, 0xd0, 0x47, 0x58, 0xd0, 0x5e, 0x46, 0x8c, 0x10, 0x7b, 0xc1, 0xe4, 0x30, 0x64, 0xef, 0xfe, 0x0f, + 0xbb, 0x1e, 0x86, 0x6c, 0x49, 0xb2, 0xbb, 0x37, 0x23, 0x7c, 0xa1, 0x9e, 0x1e, 0x58, 0x8d, 0xc3, 0x35, 0x79, 0xb1, + 0x0d, 0x51, 0xec, 0x25, 0xdc, 0x30, 0x6a, 0x4b, 0x31, 0xb7, 0x60, 0x8d, 0x71, 0x48, 0xb1, 0x35, 0xca, 0xa8, 0x61, + 0x73, 0x68, 0x73, 0x28, 0xed, 0xbd, 0xe2, 0xbb, 0xfc, 0x1d, 0xe2, 0x83, 0x6f, 0x6d, 0x8f, 0xa2, 0xee, 0x9d, 0x7b, + 0xcb, 0xbc, 0x48, 0x57, 0xb2, 0xfe, 0xb9, 0x9d, 0xd8, 0x50, 0xdc, 0x4d, 0x37, 0x63, 0x3d, 0x71, 0x90, 0x5d, 0x9a, + 0x7c, 0x20, 0xa8, 0xa2, 0x64, 0xa5, 0xd5, 0xff, 0xec, 0xf6, 0xdf, 0x72, 0x1e, 0x9a, 0x68, 0x74, 0x6c, 0x3b, 0xb4, + 0x46, 0xef, 0xe1, 0xd7, 0xf8, 0x18, 0xab, 0x05, 0x24, 0x87, 0xb9, 0x4e, 0x94, 0xba, 0x19, 0x11, 0x3a, 0x71, 0xe3, + 0x05, 0xa2, 0xde, 0x76, 0x3d, 0xd3, 0xb9, 0xf4, 0xfe, 0x2e, 0x03, 0x34, 0x35, 0x84, 0xe0, 0x21, 0x24, 0xe7, 0x37, + 0xe1, 0xcd, 0xe8, 0x44, 0x7c, 0xc3, 0x74, 0x39, 0x43, 0xee, 0xe1, 0x0b, 0xb4, 0xee, 0x24, 0x58, 0x38, 0xdc, 0x10, + 0x52, 0xa4, 0x82, 0x00, 0xd9, 0x3e, 0x06, 0xb0, 0x30, 0xc9, 0x5e, 0x34, 0x19, 0x0d, 0x88, 0x6c, 0xd6, 0xb6, 0x84, + 0x39, 0x36, 0x53, 0x80, 0x16, 0x6c, 0xcd, 0x2f, 0x81, 0xb3, 0xa1, 0x2d, 0xde, 0xd2, 0xff, 0xe4, 0x35, 0x11, 0x60, + 0x4c, 0x53, 0x9b, 0x66, 0xd6, 0x2b, 0xab, 0x85, 0xa3, 0x28, 0x59, 0x2c, 0x90, 0x03, 0xd7, 0x0d, 0xa5, 0xb1, 0x35, + 0x56, 0x97, 0x34, 0xa0, 0xe5, 0xa2, 0xba, 0x20, 0x10, 0x12, 0x43, 0xcc, 0xab, 0x86, 0x42, 0x4a, 0x12, 0xaa, 0xb9, + 0x75, 0x27, 0xb6, 0x09, 0x0a, 0xb3, 0xe3, 0xce, 0xe4, 0xa1, 0x9f, 0xe1, 0xf8, 0xe3, 0x8d, 0xd9, 0x41, 0xa0, 0x70, + 0xc5, 0x4b, 0x19, 0x0d, 0x2a, 0xcb, 0x66, 0x3d, 0xf4, 0xca, 0xcd, 0x02, 0xda, 0x9d, 0xca, 0x32, 0xa3, 0xda, 0xa9, + 0x9e, 0x09, 0x4e, 0x6f, 0x0d, 0xd0, 0x88, 0x48, 0x80, 0x09, 0xfc, 0xa8, 0xbf, 0x34, 0x2a, 0x16, 0x18, 0x6b, 0x2b, + 0x8f, 0x7a, 0x7d, 0x8f, 0x33, 0x99, 0xce, 0x03, 0x6c, 0x9c, 0xb3, 0x68, 0x55, 0x23, 0x9e, 0x90, 0xa0, 0x4f, 0x72, + 0xb2, 0x73, 0x56, 0x2d, 0xe3, 0xeb, 0xe4, 0x82, 0x2f, 0xd8, 0x1d, 0x7f, 0xad, 0x11, 0x94, 0xe3, 0x5f, 0x5c, 0xbc, + 0xc5, 0x6b, 0xe1, 0x14, 0xd7, 0x23, 0xe6, 0x8b, 0x32, 0x2f, 0x7f, 0x78, 0x61, 0xe6, 0xf4, 0xef, 0xaf, 0x30, 0x01, + 0x55, 0xfe, 0x62, 0x89, 0x04, 0x52, 0x79, 0x78, 0xeb, 0x8d, 0xe0, 0x4a, 0x66, 0x14, 0x8d, 0x59, 0x3b, 0x6e, 0x09, + 0x3b, 0x58, 0x14, 0x47, 0x10, 0x2a, 0xfe, 0xf9, 0x0c, 0x20, 0x71, 0x16, 0xb4, 0xcc, 0x68, 0xd0, 0x88, 0xf6, 0xc0, + 0x9d, 0x15, 0x36, 0xe6, 0x85, 0x5c, 0x97, 0x6f, 0x1f, 0x56, 0x70, 0x90, 0x25, 0x24, 0xc1, 0xc3, 0x7a, 0xfb, 0xa6, + 0xca, 0x74, 0xe9, 0x61, 0xea, 0x75, 0xc7, 0xef, 0x99, 0x09, 0x08, 0x69, 0xf6, 0x10, 0xd9, 0xdc, 0x8d, 0xc4, 0xf4, + 0xc6, 0x53, 0xdb, 0x8e, 0x98, 0x8f, 0xed, 0x44, 0xe4, 0x4a, 0x1d, 0xdb, 0xe6, 0x21, 0x32, 0xc2, 0x0a, 0x23, 0x09, + 0x2e, 0xbf, 0x8c, 0xc8, 0x4d, 0x16, 0x34, 0xf6, 0x31, 0xba, 0x94, 0xc5, 0x24, 0xfb, 0x08, 0xfe, 0x52, 0xd6, 0xfa, + 0x97, 0xa8, 0x75, 0xf6, 0x04, 0x7e, 0xc5, 0xd0, 0xde, 0x43, 0x68, 0xac, 0xb3, 0xe0, 0x5d, 0x0b, 0x1e, 0x29, 0xa0, + 0xdc, 0x87, 0x89, 0x84, 0x50, 0x5c, 0x1f, 0x87, 0x5d, 0xb9, 0x6b, 0x89, 0x11, 0xe1, 0xa3, 0xa4, 0x57, 0x6a, 0x93, + 0x31, 0x5c, 0x81, 0x00, 0x2e, 0xcf, 0xf5, 0x78, 0x3e, 0xc3, 0x6c, 0xaf, 0x34, 0x92, 0xd0, 0x77, 0xc3, 0x8c, 0x97, + 0x9b, 0x6e, 0x51, 0x59, 0xb4, 0x79, 0x2b, 0x85, 0xbd, 0x2e, 0x10, 0x99, 0x11, 0x22, 0xe6, 0x96, 0xdf, 0x14, 0xa4, + 0x93, 0xed, 0x7c, 0x83, 0x3e, 0x36, 0x30, 0x9c, 0xc1, 0x4a, 0x57, 0xb5, 0xb5, 0x73, 0x2b, 0xb1, 0xfe, 0x9d, 0x15, + 0x13, 0xf8, 0xf9, 0x62, 0x41, 0x42, 0x40, 0xc2, 0x42, 0xcf, 0x3c, 0x98, 0xf5, 0x70, 0x92, 0x4e, 0x79, 0xf6, 0x12, + 0x13, 0x2e, 0x64, 0xe8, 0x70, 0xfc, 0xa0, 0xa5, 0xb9, 0xa0, 0x39, 0x7e, 0x3e, 0xd3, 0x52, 0xf9, 0x5a, 0x49, 0x93, + 0x2c, 0x58, 0xe5, 0x85, 0xd3, 0xe5, 0x23, 0x43, 0x14, 0x9f, 0x6a, 0xd7, 0x7d, 0x87, 0x9b, 0xcf, 0xa4, 0x68, 0x24, + 0x95, 0x76, 0x22, 0x50, 0x69, 0xc8, 0xe4, 0xed, 0x5e, 0x00, 0x62, 0x1b, 0xa2, 0x2f, 0x9a, 0x8d, 0xcc, 0x54, 0xa6, + 0xa3, 0xab, 0xe5, 0x21, 0x1c, 0xdb, 0xc3, 0x9b, 0xa1, 0x61, 0x08, 0x78, 0x7d, 0x5a, 0xb3, 0x7f, 0x1d, 0x75, 0xa8, + 0x68, 0x62, 0x54, 0xc4, 0xcd, 0x05, 0x93, 0x25, 0x2b, 0xa6, 0x21, 0x41, 0x38, 0x69, 0xc0, 0xe9, 0x6c, 0xc6, 0xd8, + 0x20, 0x79, 0x81, 0x49, 0x26, 0xf6, 0x04, 0x5a, 0x9a, 0x80, 0x79, 0x45, 0xd9, 0x79, 0xb4, 0x19, 0xdb, 0x19, 0xa1, + 0x9c, 0x39, 0x89, 0x8a, 0xf8, 0x67, 0xee, 0x49, 0x2b, 0xe0, 0x3e, 0x63, 0xba, 0xeb, 0x35, 0x9e, 0x71, 0x04, 0x45, + 0xbf, 0x6d, 0x9b, 0xff, 0x65, 0x18, 0x84, 0xa7, 0xcb, 0x76, 0x0e, 0x50, 0x41, 0x96, 0x10, 0xf0, 0x27, 0x2f, 0xe8, + 0x4b, 0xc0, 0x43, 0x0c, 0xf8, 0x81, 0xbd, 0x7a, 0x6d, 0x05, 0x3a, 0xb8, 0xfa, 0xea, 0xec, 0xf7, 0xdf, 0x32, 0x38, + 0xfc, 0x07, 0x57, 0xda, 0xf7, 0x8f, 0x4f, 0x09, 0x9b, 0x93, 0xa7, 0x78, 0x3a, 0x3a, 0xc7, 0xe1, 0xb6, 0x1e, 0xe5, + 0x74, 0x3b, 0x25, 0x14, 0x5e, 0xf8, 0x40, 0xfc, 0x31, 0x8b, 0xbb, 0x81, 0xa6, 0xf2, 0x71, 0xce, 0x1f, 0xe4, 0x27, + 0xc7, 0x27, 0xe9, 0x3e, 0xcd, 0x73, 0x38, 0xe6, 0x82, 0x6d, 0x0c, 0x83, 0xab, 0xce, 0xce, 0x2e, 0xe5, 0x66, 0x58, + 0x4a, 0x7a, 0xab, 0xdb, 0xbd, 0x8e, 0x51, 0xe9, 0xff, 0x2b, 0x7b, 0x4b, 0x47, 0x38, 0x8c, 0x7f, 0xf8, 0x0c, 0x05, + 0x41, 0xee, 0x14, 0xeb, 0xf4, 0xa2, 0x70, 0x8d, 0x3b, 0x94, 0x6f, 0xad, 0xb6, 0xbe, 0xaa, 0x52, 0x8f, 0xcc, 0x45, + 0x8c, 0xf3, 0x15, 0xf1, 0xb2, 0x9a, 0xbc, 0x6e, 0xd0, 0x6f, 0x4f, 0x94, 0xf9, 0xcf, 0xaf, 0x21, 0xc1, 0x76, 0x74, + 0xbf, 0x86, 0xfb, 0x1d, 0x71, 0x0d, 0x6b, 0xce, 0x91, 0x17, 0x9c, 0x71, 0x5d, 0x3d, 0x6d, 0x93, 0x75, 0x2d, 0x1c, + 0xdb, 0x2e, 0x07, 0x5e, 0xeb, 0x52, 0xe7, 0x10, 0xa5, 0x95, 0x71, 0xcf, 0xe9, 0x5d, 0x97, 0xdf, 0x99, 0xea, 0x18, + 0x76, 0x03, 0x9c, 0x8a, 0x60, 0x40, 0x81, 0x79, 0x1f, 0xd4, 0x9d, 0x0c, 0x21, 0x27, 0xf6, 0xac, 0x81, 0x5c, 0x82, + 0x28, 0x9a, 0x2f, 0x41, 0x00, 0x5a, 0xda, 0x81, 0x97, 0xb5, 0x8a, 0x46, 0x96, 0xac, 0x81, 0xb3, 0xd7, 0xff, 0x23, + 0x06, 0x43, 0x9c, 0x7c, 0x93, 0x80, 0x38, 0xc9, 0x14, 0x89, 0x39, 0x8d, 0x45, 0x9f, 0xb3, 0x8f, 0x72, 0x09, 0xd2, + 0xec, 0x67, 0x60, 0x80, 0x60, 0x1a, 0x8e, 0x63, 0x41, 0xa1, 0x64, 0xbe, 0x2a, 0xfa, 0x69, 0xb3, 0xf8, 0xfc, 0x09, + 0xc6, 0xf6, 0x6f, 0x74, 0xdb, 0xa8, 0xfc, 0x5e, 0x53, 0xc9, 0xed, 0xaf, 0x3c, 0x9f, 0xfe, 0xb6, 0x3a, 0x3c, 0xfd, + 0x44, 0xfd, 0xf8, 0x75, 0xd3, 0x02, 0xef, 0xe4, 0xee, 0xa5, 0x0c, 0x35, 0x3f, 0x5f, 0x67, 0x40, 0x58, 0x18, 0x80, + 0xfa, 0xd1, 0xf1, 0xa1, 0xa4, 0xdd, 0xd6, 0xb3, 0x41, 0x34, 0xb1, 0x8f, 0x71, 0x8b, 0xea, 0xe5, 0xbc, 0xc0, 0x66, + 0x35, 0xae, 0xa1, 0x7b, 0x5e, 0x68, 0xcd, 0x33, 0x61, 0x96, 0x0a, 0x4a, 0xe1, 0x64, 0x0a, 0xb8, 0x01, 0x5c, 0x57, + 0x4e, 0x9b, 0x85, 0x17, 0xbd, 0x09, 0x4f, 0x12, 0xcc, 0xe8, 0xc0, 0x45, 0xd3, 0x57, 0x4f, 0xed, 0x8b, 0x8e, 0xe1, + 0xcf, 0x44, 0x5d, 0x8d, 0x21, 0xa9, 0x51, 0x8e, 0x49, 0x8b, 0x95, 0x56, 0x68, 0x2d, 0xaf, 0x96, 0xba, 0xdb, 0x39, + 0x42, 0xaf, 0xbc, 0xa0, 0x0c, 0xc0, 0x03, 0x98, 0xf5, 0x92, 0xde, 0xd2, 0x2a, 0xb2, 0x29, 0xfb, 0x84, 0x5c, 0x9b, + 0xc7, 0x13, 0x9c, 0x96, 0x3e, 0xaa, 0x5b, 0xa4, 0x49, 0x6c, 0x85, 0x6b, 0x38, 0x37, 0x59, 0x55, 0xf5, 0xa2, 0xf9, + 0xda, 0x0f, 0x30, 0xa7, 0x05, 0xfb, 0x37, 0xf6, 0x45, 0xd3, 0x72, 0x12, 0x68, 0xbb, 0x68, 0x64, 0x0b, 0xca, 0x00, + 0x88, 0xd2, 0x3d, 0xbd, 0x01, 0x07, 0xa2, 0x5d, 0xd3, 0x89, 0xf8, 0x36, 0xb1, 0x1d, 0xce, 0x4d, 0x56, 0xa8, 0x85, + 0x0b, 0x73, 0x34, 0x9b, 0x2e, 0x9c, 0xa8, 0xbd, 0x4b, 0x7b, 0x9e, 0x0d, 0x34, 0x6e, 0xf3, 0x40, 0x21, 0x7d, 0x2f, + 0xf0, 0xa8, 0x41, 0xdc, 0x50, 0xa1, 0x17, 0x21, 0x53, 0x81, 0x6b, 0x0a, 0xb6, 0x21, 0x33, 0xd3, 0x38, 0x00, 0xc8, + 0xde, 0x45, 0xdc, 0x80, 0x83, 0x2b, 0x35, 0x86, 0x8e, 0xad, 0xd7, 0xe4, 0x95, 0x64, 0x82, 0xa0, 0xf2, 0x66, 0x89, + 0xcd, 0x58, 0x72, 0x10, 0x95, 0x6f, 0x70, 0xb3, 0x73, 0x27, 0x64, 0xf6, 0x3b, 0x9d, 0x21, 0x4c, 0x59, 0x59, 0xed, + 0x90, 0x9b, 0x11, 0x2f, 0x14, 0x98, 0x5a, 0xb4, 0x20, 0x22, 0x19, 0xb1, 0xaa, 0x1b, 0xbf, 0xf3, 0x76, 0x94, 0x9b, + 0x89, 0x6d, 0xb1, 0x5e, 0xf1, 0x8c, 0x60, 0xbd, 0x83, 0xb5, 0x73, 0xf4, 0x6a, 0x67, 0x64, 0xae, 0xf0, 0x62, 0x98, + 0xdc, 0xae, 0xe7, 0x83, 0x61, 0x44, 0x7d, 0xf9, 0x3f, 0xdb, 0x98, 0x55, 0xe5, 0x34, 0x1a, 0x43, 0x42, 0x24, 0xc3, + 0x9b, 0x00, 0xc4, 0xf3, 0xac, 0xc9, 0x18, 0xcd, 0xc4, 0x6a, 0xdb, 0x3a, 0x4d, 0xb3, 0x9f, 0x4f, 0x39, 0xfd, 0xde, + 0x48, 0x38, 0xc0, 0xf3, 0xaa, 0x73, 0x23, 0xbb, 0x7e, 0xa0, 0x8b, 0x39, 0xf4, 0x65, 0x26, 0x57, 0xf5, 0x8d, 0xec, + 0x54, 0x23, 0xcc, 0xcc, 0xa0, 0xef, 0x06, 0x25, 0x0f, 0x00, 0xd0, 0x1f, 0xe7, 0xe5, 0xd5, 0xff, 0x35, 0x9a, 0x3b, + 0x61, 0x04, 0x1b, 0x2b, 0x96, 0xe6, 0x38, 0x5e, 0x0e, 0xed, 0x40, 0x45, 0xcf, 0x89, 0xda, 0xd3, 0x88, 0xa4, 0x4b, + 0x6a, 0x0c, 0xe3, 0x89, 0x59, 0x1a, 0x1c, 0xd6, 0x50, 0x82, 0xfd, 0x32, 0xfa, 0xed, 0xda, 0xfb, 0x06, 0x52, 0xfc, + 0x1b, 0xd7, 0xd5, 0xf1, 0xec, 0xa8, 0x32, 0x93, 0x5a, 0xe6, 0x89, 0xdb, 0xe2, 0xaa, 0xae, 0x9a, 0xf9, 0xb4, 0x5d, + 0x32, 0x4d, 0x3b, 0x8f, 0xd9, 0x65, 0xfc, 0x19, 0x4d, 0x24, 0x23, 0x3f, 0xac, 0xc3, 0x00, 0x0d, 0x0c, 0xb4, 0x97, + 0xf8, 0xe9, 0x49, 0xa6, 0xab, 0xb7, 0xba, 0x49, 0xd0, 0xba, 0x5c, 0xa7, 0x1f, 0x48, 0xbd, 0xa0, 0x65, 0xd8, 0x59, + 0x33, 0x78, 0xe6, 0x84, 0xe8, 0x02, 0xe7, 0x27, 0xe6, 0x21, 0x67, 0xd4, 0x34, 0xa0, 0x5f, 0xe7, 0xe5, 0x55, 0x97, + 0xbb, 0xc8, 0xc0, 0xcd, 0x04, 0x76, 0xc8, 0x6e, 0x68, 0x7d, 0xac, 0x89, 0xa1, 0x07, 0xe9, 0xc2, 0xb4, 0x35, 0x8f, + 0x83, 0xd0, 0x14, 0xca, 0xc2, 0x95, 0x29, 0xd9, 0x28, 0x7c, 0x4f, 0x8e, 0xae, 0xe1, 0x82, 0x96, 0xd0, 0xde, 0xfd, + 0xdb, 0x05, 0x74, 0xf7, 0x98, 0x40, 0x95, 0x78, 0x92, 0x16, 0xca, 0xcd, 0x42, 0x79, 0x4e, 0x81, 0x15, 0x2c, 0x32, + 0xcf, 0xaa, 0xe9, 0x48, 0xb3, 0xd6, 0x8f, 0x4e, 0xe7, 0xba, 0xd5, 0x1a, 0xf6, 0x31, 0x65, 0x41, 0xf1, 0x8e, 0x16, + 0xe6, 0x5f, 0x89, 0x92, 0x23, 0x0d, 0xfe, 0x2f, 0x12, 0xab, 0xa6, 0x19, 0x7c, 0x85, 0xf9, 0x7f, 0x54, 0xb7, 0x26, + 0xde, 0x27, 0x70, 0x05, 0xc2, 0x5d, 0xa9, 0xb6, 0x33, 0xee, 0x18, 0x75, 0xb4, 0x0e, 0x3c, 0x75, 0x62, 0xc6, 0xc3, + 0xe3, 0x62, 0x8b, 0xe1, 0xb7, 0xa7, 0x37, 0xe0, 0xee, 0xb3, 0x63, 0xdd, 0xdd, 0xeb, 0x20, 0xa4, 0x57, 0x66, 0x91, + 0xee, 0xaf, 0x5a, 0x4d, 0x35, 0x21, 0xd6, 0xb5, 0x32, 0xf7, 0xc4, 0x98, 0x0d, 0x86, 0x33, 0x62, 0x7c, 0x76, 0x70, + 0xb3, 0x35, 0x72, 0x77, 0xa4, 0x24, 0x8a, 0x1d, 0x5d, 0x4a, 0x78, 0x02, 0x43, 0x36, 0xac, 0xca, 0xcd, 0xaf, 0xb5, + 0x7a, 0xb5, 0xaf, 0x3e, 0xfb, 0x12, 0x93, 0xf4, 0x8b, 0x1f, 0x52, 0xd8, 0xf1, 0x44, 0x64, 0xab, 0xc3, 0x58, 0x07, + 0x74, 0x1f, 0x6a, 0xfd, 0xf2, 0xba, 0xa1, 0xda, 0x0f, 0xf8, 0x6e, 0x9d, 0x95, 0xe5, 0x57, 0x8b, 0xdf, 0xd6, 0xb7, + 0x07, 0xee, 0x25, 0x83, 0xe2, 0x17, 0xf8, 0x2a, 0x22, 0x03, 0xee, 0x97, 0xd5, 0x9a, 0x4c, 0x8a, 0xe3, 0x27, 0x74, + 0x8c, 0x65, 0x8a, 0xf2, 0x48, 0xd3, 0x76, 0xb7, 0xde, 0xa8, 0xd1, 0xb1, 0xe1, 0x93, 0x9d, 0xa9, 0xcb, 0x07, 0x64, + 0x64, 0x08, 0xdb, 0xff, 0x54, 0x5e, 0x9c, 0x0e, 0x88, 0x36, 0xd8, 0xbf, 0x65, 0x86, 0xd0, 0x3a, 0x6c, 0x26, 0xb5, + 0x1a, 0xc2, 0xb1, 0x1f, 0x56, 0x57, 0xff, 0xbf, 0xf8, 0x52, 0x1a, 0x0d, 0x44, 0x6f, 0xd5, 0x5b, 0x02, 0x25, 0xb0, + 0x5e, 0xed, 0x52, 0xea, 0xaf, 0x4e, 0x61, 0x13, 0xe3, 0xb2, 0xe4, 0x75, 0xed, 0xce, 0xd0, 0xfa, 0x49, 0xab, 0x0d, + 0xb9, 0x7f, 0xda, 0xd0, 0xe3, 0x10, 0x23, 0x29, 0x6b, 0x13, 0x63, 0x86, 0x86, 0x10, 0xb3, 0x45, 0x19, 0x83, 0xbb, + 0xfe, 0x89, 0x44, 0x6d, 0x9c, 0x44, 0x68, 0x38, 0xcf, 0xdb, 0x60, 0xed, 0xd5, 0xdd, 0xd2, 0x14, 0x37, 0xc3, 0x95, + 0xa9, 0x4b, 0xc0, 0x7c, 0x62, 0xf0, 0xc5, 0x0e, 0x16, 0x14, 0xf0, 0x12, 0x74, 0x93, 0x71, 0xd3, 0x10, 0x7d, 0xb0, + 0xf1, 0xe6, 0xcf, 0x3d, 0xc7, 0xfc, 0xcc, 0xb7, 0x83, 0x35, 0xa8, 0x9d, 0x00, 0x27, 0x3a, 0xd0, 0xf5, 0x59, 0xb5, + 0xa4, 0xfa, 0xe6, 0xf0, 0xaf, 0x4d, 0xe5, 0x77, 0xc7, 0x86, 0x6f, 0xb5, 0xb9, 0x00, 0xbc, 0x9e, 0x19, 0x76, 0x08, + 0xb4, 0x06, 0x75, 0x4e, 0x61, 0xdc, 0x5d, 0x40, 0xad, 0x7b, 0x8d, 0xeb, 0x9b, 0x22, 0x42, 0x18, 0xb8, 0x2c, 0xa8, + 0xec, 0xf6, 0x1b, 0xcc, 0x5b, 0xdf, 0x17, 0xa8, 0x01, 0xc2, 0x43, 0x19, 0xda, 0x16, 0x19, 0x77, 0xee, 0x0d, 0x36, + 0x4b, 0x58, 0xe7, 0x52, 0x4e, 0xb9, 0xa6, 0x74, 0x1d, 0xaa, 0x8f, 0x9b, 0xa2, 0x97, 0x18, 0x90, 0x23, 0x88, 0xa5, + 0x9e, 0x01, 0xab, 0x86, 0x8b, 0xf4, 0x32, 0x4d, 0xd2, 0x29, 0x5f, 0x06, 0x88, 0xad, 0xeb, 0x44, 0xa3, 0xfb, 0x6c, + 0x29, 0x0f, 0x3d, 0x88, 0x21, 0x24, 0x24, 0x92, 0x52, 0x50, 0x3f, 0x90, 0x49, 0xb9, 0xfc, 0x0f, 0x2b, 0xf1, 0x2a, + 0x4f, 0xc7, 0x5f, 0x9e, 0x4e, 0x56, 0xd5, 0x83, 0x0f, 0x84, 0x1f, 0xe8, 0xbe, 0x75, 0xbc, 0x56, 0x6b, 0xcf, 0x57, + 0x75, 0x93, 0x1c, 0xfd, 0xc4, 0xbe, 0xe4, 0x1f, 0xb4, 0xa5, 0xce, 0x4d, 0x78, 0x16, 0x57, 0xc2, 0x9a, 0xe9, 0xf2, + 0xe5, 0x3d, 0x54, 0x79, 0x24, 0x69, 0x3c, 0x4d, 0x59, 0x6d, 0x1a, 0xef, 0x66, 0x8a, 0x40, 0x1b, 0x75, 0xf4, 0x0a, + 0x4e, 0x39, 0x70, 0x51, 0x87, 0x45, 0x27, 0xcb, 0x3f, 0x0b, 0x96, 0x85, 0x6e, 0x7f, 0x4b, 0x66, 0x1f, 0x27, 0x5f, + 0x6f, 0xa8, 0x5c, 0x38, 0x91, 0x43, 0x13, 0x4b, 0x5b, 0x6d, 0xc7, 0xe0, 0x4c, 0xdd, 0x79, 0x5c, 0x92, 0xe8, 0x3a, + 0x96, 0xe5, 0x79, 0x45, 0xac, 0xe3, 0xd4, 0x7b, 0x3d, 0x88, 0x90, 0x35, 0x2b, 0x7c, 0xd9, 0x7b, 0xfd, 0xd5, 0xad, + 0xd0, 0x99, 0x82, 0xac, 0x65, 0xcf, 0xa2, 0x18, 0xde, 0x85, 0xbc, 0x8a, 0xe8, 0xcb, 0xa5, 0x90, 0x15, 0x42, 0x59, + 0xc0, 0x56, 0xe9, 0x8f, 0xa3, 0x90, 0x3c, 0x3c, 0x4e, 0xf1, 0x62, 0xe6, 0x1c, 0x29, 0x77, 0x09, 0x61, 0x77, 0xc8, + 0xf2, 0x24, 0x92, 0x7a, 0xed, 0x46, 0xb0, 0x29, 0x31, 0xc5, 0xa6, 0x28, 0x72, 0x83, 0x5d, 0x10, 0x1c, 0x75, 0xab, + 0x6f, 0x34, 0x6d, 0x24, 0x0c, 0x12, 0xf9, 0xce, 0x08, 0xe9, 0x53, 0xdf, 0xdc, 0xbd, 0xe9, 0x07, 0x53, 0xc6, 0x20, + 0x02, 0x1e, 0x45, 0xcb, 0x00, 0xda, 0x9e, 0xaf, 0xd2, 0x2e, 0x19, 0x0f, 0x33, 0x18, 0x71, 0x5b, 0x01, 0xb9, 0x2e, + 0x1a, 0xb7, 0xe1, 0x97, 0xf0, 0x24, 0x51, 0x3c, 0x4d, 0x0b, 0x45, 0x23, 0x52, 0x79, 0x36, 0x24, 0x6b, 0x9e, 0x04, + 0x0b, 0x52, 0x4f, 0x1a, 0xcc, 0x86, 0xc1, 0x62, 0x34, 0x92, 0xb0, 0x4f, 0x4d, 0x86, 0xb1, 0x32, 0xec, 0x1c, 0xfd, + 0x4b, 0x9b, 0xd3, 0x16, 0x6b, 0x53, 0x0b, 0xb5, 0x99, 0xd1, 0x83, 0x19, 0x6f, 0x8c, 0xd4, 0xb0, 0x6a, 0x86, 0xf1, + 0x45, 0xa6, 0x76, 0x3a, 0x65, 0x14, 0x25, 0xc6, 0x69, 0x30, 0x77, 0x0c, 0x39, 0x54, 0x3f, 0x60, 0xb3, 0x82, 0xdc, + 0x55, 0x9d, 0xcd, 0xbd, 0x66, 0xdc, 0x5e, 0xd7, 0x8c, 0x3e, 0xf5, 0x4f, 0xb7, 0xfe, 0x73, 0x99, 0xae, 0xdb, 0xb1, + 0xca, 0x5f, 0xfa, 0x79, 0x37, 0x7d, 0x68, 0x31, 0x6f, 0xca, 0xce, 0x30, 0xc3, 0xeb, 0xcf, 0xa7, 0xc5, 0x83, 0xa2, + 0x81, 0xcd, 0x97, 0x6a, 0xe3, 0x70, 0xfd, 0xfb, 0x81, 0xad, 0xb7, 0xbb, 0xb9, 0x93, 0xa4, 0x21, 0xb6, 0x1c, 0x21, + 0x37, 0x82, 0x63, 0x02, 0xfe, 0xe3, 0x04, 0xf9, 0xdf, 0x3b, 0xf4, 0x6d, 0x7b, 0x10, 0x3e, 0xc6, 0xeb, 0x1e, 0x46, + 0x01, 0x73, 0xd6, 0xb2, 0x5e, 0x7d, 0x1a, 0x57, 0x45, 0xfa, 0x2b, 0x82, 0xfa, 0x8d, 0x23, 0xf8, 0x47, 0x57, 0x25, + 0xbf, 0xd3, 0x65, 0xd4, 0xbe, 0xfb, 0xdc, 0x0f, 0xd6, 0xa8, 0x32, 0x8e, 0xee, 0xcd, 0x69, 0x4b, 0x4a, 0x7b, 0x52, + 0xbe, 0xd5, 0x1e, 0x9e, 0xb6, 0x42, 0x9a, 0xb3, 0x79, 0x4f, 0x2e, 0xe7, 0x51, 0x82, 0x6d, 0x39, 0x8e, 0x70, 0x07, + 0xf9, 0xfa, 0x94, 0x51, 0x3a, 0x7a, 0x97, 0xe5, 0xed, 0xde, 0x04, 0x36, 0xf3, 0xf4, 0x04, 0xcc, 0x68, 0xda, 0x95, + 0x7e, 0xbf, 0x15, 0x27, 0xe6, 0xc3, 0xf6, 0x2e, 0xfb, 0x35, 0xae, 0xb4, 0x00, 0x8f, 0x7b, 0x5f, 0xb5, 0xfd, 0x6b, + 0xdb, 0x43, 0xdc, 0x8c, 0x14, 0x83, 0xb7, 0xf9, 0x2a, 0x4b, 0xa2, 0x02, 0x59, 0xf0, 0x1a, 0xf9, 0x20, 0xb6, 0x05, + 0x20, 0x67, 0xb4, 0x46, 0x2d, 0xfd, 0x8e, 0x25, 0xf1, 0x7c, 0x5b, 0x81, 0x9a, 0xf3, 0xec, 0xac, 0xa2, 0x55, 0x77, + 0xc2, 0x57, 0xa7, 0x9c, 0xa5, 0xd9, 0x85, 0xe8, 0x7a, 0xf8, 0xcc, 0x52, 0x54, 0xb2, 0x6c, 0x78, 0x37, 0xc6, 0xaf, + 0xd8, 0x2b, 0xcf, 0x50, 0xf2, 0xae, 0x94, 0x86, 0x42, 0x41, 0xb6, 0x06, 0xf5, 0xad, 0xb3, 0x97, 0x58, 0xdc, 0x68, + 0x79, 0x94, 0xab, 0xf0, 0xc5, 0xdc, 0xc7, 0xed, 0x71, 0x54, 0x15, 0x73, 0x0e, 0x61, 0x4f, 0x02, 0x3a, 0x69, 0x90, + 0x03, 0xa4, 0xd5, 0x65, 0x11, 0x36, 0x48, 0xa1, 0x5e, 0x8e, 0x7b, 0x94, 0x2b, 0xda, 0x8e, 0x05, 0x64, 0x2c, 0xba, + 0xcb, 0x8c, 0x4c, 0xe7, 0xb1, 0x13, 0xdd, 0x87, 0x2e, 0x17, 0x28, 0x30, 0x58, 0x9f, 0xb5, 0xe4, 0x92, 0xc7, 0x8a, + 0xa3, 0xec, 0x4a, 0x0c, 0x94, 0x67, 0x43, 0xd6, 0x6b, 0x7c, 0xc5, 0x02, 0xac, 0xe9, 0x76, 0x8e, 0x85, 0x0a, 0x96, + 0x7d, 0xff, 0x0b, 0x9f, 0x96, 0x8c, 0x9c, 0xca, 0x24, 0x96, 0xa5, 0x0f, 0x73, 0xe3, 0x86, 0xe0, 0x09, 0x41, 0x33, + 0x49, 0xe6, 0x29, 0xa7, 0x14, 0x4a, 0xeb, 0x7f, 0xae, 0x3c, 0x42, 0xd5, 0x6c, 0xdd, 0xf4, 0x96, 0x71, 0x77, 0x09, + 0x8d, 0xff, 0x21, 0x3a, 0x56, 0x71, 0xc1, 0xfb, 0xf3, 0x44, 0x92, 0x9c, 0x0a, 0x65, 0x2d, 0x9b, 0x17, 0x5b, 0xc8, + 0xa0, 0xe3, 0x96, 0x72, 0x08, 0xe4, 0x00, 0x60, 0x7a, 0xd5, 0x86, 0xba, 0xc6, 0x3e, 0x77, 0xbd, 0x21, 0x21, 0x56, + 0x04, 0xbb, 0xa1, 0x13, 0x24, 0xd4, 0x54, 0x21, 0xf1, 0x59, 0xaf, 0xf2, 0x6e, 0x14, 0x85, 0x1e, 0xf0, 0x8f, 0x7f, + 0x93, 0x88, 0xf3, 0x37, 0x58, 0xaa, 0xdf, 0xb0, 0x4a, 0x1b, 0xfa, 0xe4, 0x5f, 0x24, 0x5e, 0x75, 0xfe, 0x29, 0x66, + 0x9a, 0x6d, 0x87, 0xee, 0x67, 0x7e, 0x3e, 0xe1, 0x51, 0xf6, 0xc2, 0x21, 0x63, 0x0d, 0x19, 0x3a, 0x86, 0x2e, 0x12, + 0x6c, 0xf2, 0x97, 0x14, 0xfa, 0x64, 0x5a, 0xfa, 0x8a, 0xdf, 0x69, 0xdd, 0x9d, 0xad, 0x42, 0x21, 0x16, 0xcc, 0x50, + 0x4a, 0xa3, 0xee, 0x98, 0xea, 0x98, 0x59, 0x98, 0xe3, 0x90, 0x24, 0xa2, 0x45, 0x0e, 0x67, 0xb8, 0xbf, 0x01, 0x08, + 0x81, 0x06, 0x2b, 0x11, 0x2a, 0xca, 0xc5, 0x1e, 0xc1, 0x13, 0x6e, 0xb6, 0xb9, 0xdf, 0xc9, 0x3c, 0x9c, 0x48, 0xa3, + 0x5c, 0xc1, 0x02, 0x30, 0xd5, 0xb3, 0x1b, 0x49, 0xc9, 0xe1, 0x5e, 0xb4, 0xc6, 0xf9, 0x0c, 0x25, 0x94, 0xc5, 0xce, + 0x83, 0x60, 0x5d, 0x65, 0x53, 0xd9, 0x19, 0xcc, 0xaa, 0xee, 0x1c, 0xa8, 0xe2, 0x02, 0x89, 0xba, 0x31, 0x26, 0x53, + 0xcc, 0xb2, 0x19, 0x7e, 0x02, 0x31, 0x6f, 0xc8, 0x54, 0x70, 0xf7, 0x5a, 0x9d, 0x2d, 0xef, 0x1a, 0x26, 0x94, 0xa1, + 0x81, 0xd5, 0x49, 0x8c, 0x1a, 0x96, 0x70, 0x71, 0xc1, 0x67, 0xd0, 0x9f, 0x06, 0x42, 0x33, 0x3a, 0xbd, 0x19, 0xa3, + 0x7e, 0xcb, 0xc6, 0x93, 0xef, 0x15, 0xe7, 0xbd, 0xee, 0xf0, 0x4a, 0x25, 0x54, 0x25, 0x5f, 0x46, 0x88, 0xe8, 0x56, + 0x5f, 0x2a, 0x9e, 0x53, 0xf7, 0xde, 0x2f, 0x24, 0x9e, 0xf4, 0x99, 0x91, 0xc7, 0xfb, 0x5d, 0x28, 0x28, 0x80, 0xde, + 0xb2, 0x08, 0x99, 0x7e, 0x50, 0x56, 0xd5, 0x1d, 0x9e, 0x5c, 0xda, 0x95, 0x50, 0xf1, 0xba, 0x7e, 0xb3, 0x3c, 0x81, + 0x2a, 0x4c, 0x66, 0x28, 0xe6, 0xd8, 0x54, 0x8e, 0xc6, 0x1b, 0x4c, 0x23, 0x18, 0xe7, 0x39, 0xa1, 0x02, 0xfd, 0x50, + 0x25, 0x9a, 0x3a, 0x33, 0x73, 0xc6, 0xf2, 0xba, 0x0f, 0x7b, 0x3e, 0x77, 0x8a, 0xd9, 0x85, 0x57, 0xfb, 0x96, 0x3a, + 0x6e, 0x9f, 0x05, 0x97, 0xe5, 0xee, 0x16, 0x85, 0xec, 0x29, 0x95, 0xc4, 0x38, 0x80, 0x75, 0x1e, 0x5d, 0xd9, 0x9a, + 0x2e, 0x65, 0xb0, 0xfb, 0x13, 0xa4, 0x00, 0x8e, 0x96, 0x0c, 0x24, 0x60, 0x37, 0xf2, 0x6b, 0xd7, 0x64, 0xe6, 0x9b, + 0x8f, 0x03, 0x0b, 0x82, 0xc8, 0x04, 0xce, 0x10, 0x31, 0x91, 0x86, 0xf0, 0xf3, 0x3e, 0xce, 0xbe, 0xda, 0x4c, 0x34, + 0x51, 0x7b, 0x23, 0xe4, 0xf3, 0xf0, 0x1a, 0x76, 0xf3, 0xc0, 0x94, 0xf7, 0x5b, 0x3a, 0x45, 0x1c, 0x34, 0x89, 0xa9, + 0xd5, 0x33, 0xf6, 0x5b, 0xe6, 0x72, 0xc3, 0x2f, 0xc4, 0x14, 0x77, 0x77, 0x71, 0x2a, 0x0c, 0x2c, 0x99, 0xf0, 0xcb, + 0x83, 0xa9, 0x89, 0x29, 0x7b, 0x88, 0xef, 0xfb, 0xf0, 0xe1, 0x71, 0x63, 0xf6, 0xc9, 0x5d, 0x71, 0x9d, 0x58, 0xaa, + 0xb0, 0xaf, 0xe9, 0xeb, 0x21, 0x63, 0x4e, 0x44, 0xd2, 0x52, 0x99, 0xae, 0x0f, 0x36, 0xfe, 0xac, 0x62, 0xc9, 0xca, + 0x11, 0xb6, 0x46, 0x80, 0xf4, 0x4b, 0x83, 0xa6, 0xe1, 0x90, 0x7a, 0x18, 0xfa, 0x40, 0x8a, 0x39, 0xc1, 0xc0, 0xd1, + 0x25, 0x71, 0x6d, 0xeb, 0x70, 0x58, 0x24, 0x3d, 0x96, 0x68, 0xe9, 0xe7, 0x6e, 0x73, 0x7e, 0xb6, 0x07, 0xc7, 0xc2, + 0x65, 0xe5, 0x65, 0x65, 0x5e, 0x78, 0xc6, 0xc9, 0x62, 0xaf, 0x5a, 0x35, 0x7e, 0xe7, 0xf7, 0x7d, 0x2d, 0x99, 0x83, + 0x91, 0x1b, 0x99, 0x2b, 0x5a, 0x78, 0x30, 0xef, 0xe4, 0x15, 0x34, 0x6e, 0xb6, 0x12, 0x87, 0x12, 0xda, 0x7a, 0x70, + 0xea, 0xfd, 0x99, 0x02, 0x57, 0x10, 0x28, 0xbc, 0x7e, 0x3f, 0x1e, 0x6f, 0xc8, 0x68, 0x73, 0x85, 0x0c, 0x7a, 0x6e, + 0xf5, 0x02, 0xd5, 0x79, 0xdf, 0x7c, 0x3e, 0x67, 0x6f, 0xcc, 0xb3, 0xee, 0x63, 0x48, 0x7d, 0x64, 0x88, 0x1a, 0xb2, + 0xbc, 0x16, 0x0a, 0x93, 0x05, 0xf4, 0x38, 0xaa, 0x2a, 0x44, 0x56, 0x87, 0xb2, 0x71, 0x33, 0x54, 0xd8, 0x4f, 0xaf, + 0x7f, 0x80, 0x11, 0x72, 0x94, 0x52, 0x68, 0x4f, 0x4c, 0x55, 0x46, 0x08, 0x81, 0xb1, 0x21, 0x1a, 0x96, 0x91, 0x29, + 0x6c, 0xb3, 0x8a, 0x76, 0x9c, 0xae, 0xec, 0xd6, 0x37, 0xab, 0x14, 0x73, 0xde, 0x0d, 0x9e, 0x25, 0x68, 0xf7, 0xf6, + 0xb6, 0xc7, 0x31, 0xf4, 0x53, 0xf1, 0x3f, 0x82, 0x1d, 0x9d, 0xc3, 0x12, 0x15, 0x9c, 0x12, 0xfb, 0x9c, 0xf9, 0xab, + 0x63, 0x25, 0x8e, 0x7b, 0xda, 0xe2, 0xde, 0x8e, 0x1d, 0x33, 0x2b, 0x3f, 0x36, 0x59, 0x72, 0x2d, 0x43, 0x12, 0xd5, + 0x35, 0x97, 0x8e, 0x41, 0x53, 0x22, 0x37, 0x6f, 0x66, 0x96, 0xf6, 0x06, 0xcc, 0x8f, 0xf6, 0x41, 0xfb, 0x25, 0x21, + 0xc2, 0x6a, 0xa9, 0x99, 0x8b, 0x2f, 0x71, 0xca, 0x38, 0xc9, 0x7d, 0x03, 0xe6, 0xef, 0xc4, 0xf5, 0xef, 0xa2, 0x07, + 0x87, 0x39, 0x02, 0x18, 0x88, 0xb7, 0x52, 0x6d, 0xe5, 0x4d, 0x44, 0x69, 0x05, 0x86, 0x5d, 0x9b, 0xca, 0x86, 0xa3, + 0x21, 0x7f, 0xc3, 0x23, 0xfb, 0x32, 0x5f, 0x6f, 0x6c, 0xa1, 0x38, 0xf1, 0x2e, 0xff, 0xfc, 0xd3, 0x87, 0xe7, 0xc7, + 0x9c, 0x0b, 0x76, 0x73, 0xe3, 0xbc, 0x6a, 0x13, 0xc8, 0xb6, 0x5d, 0x0c, 0x12, 0x9c, 0x42, 0x23, 0x27, 0x40, 0xfa, + 0xe1, 0xc2, 0x22, 0xc4, 0xcf, 0xdf, 0x3d, 0xd9, 0xf5, 0xf6, 0x3a, 0x6c, 0x46, 0xb3, 0xbd, 0x23, 0x1a, 0x41, 0x4e, + 0x57, 0xc7, 0xec, 0xfb, 0xe4, 0x60, 0xfc, 0x4b, 0xe2, 0x7e, 0xa6, 0xca, 0xcf, 0x35, 0xd7, 0x37, 0x55, 0x7e, 0xea, + 0xe0, 0xc6, 0x27, 0xb0, 0x6a, 0x53, 0x72, 0x13, 0xe6, 0xca, 0xad, 0x3e, 0x79, 0x4c, 0x0c, 0xa6, 0xd5, 0x3f, 0x7d, + 0x7f, 0x1a, 0x06, 0x06, 0x17, 0xbb, 0x3b, 0x4f, 0xbe, 0xce, 0xf4, 0xc7, 0x79, 0xdf, 0xb1, 0xfb, 0x3a, 0xf8, 0x71, + 0x5c, 0x5d, 0xd6, 0x23, 0x49, 0x23, 0x07, 0xc4, 0x7b, 0xca, 0xa8, 0x61, 0x2f, 0x77, 0x95, 0x87, 0x55, 0xfd, 0x7d, + 0xd6, 0xbb, 0x44, 0xef, 0x8e, 0x9d, 0x65, 0xad, 0x26, 0x94, 0x17, 0x98, 0xd3, 0x79, 0x4c, 0xb3, 0x42, 0x47, 0x85, + 0x9a, 0x89, 0xf6, 0x32, 0xb2, 0x4a, 0x7f, 0xf3, 0x4b, 0xda, 0xaf, 0x16, 0xc1, 0xb0, 0xac, 0xc2, 0xe5, 0x3c, 0x6a, + 0xb0, 0x59, 0xbb, 0x36, 0x7f, 0xfd, 0xef, 0x69, 0xc3, 0xce, 0x04, 0x51, 0x7d, 0x52, 0x2b, 0x79, 0xd6, 0x77, 0xb8, + 0xea, 0xf6, 0x7c, 0xbe, 0x91, 0x79, 0xaf, 0x9e, 0x2f, 0x9a, 0x8f, 0xb7, 0x5f, 0xbb, 0x07, 0xe0, 0x97, 0x5d, 0x59, + 0xab, 0x37, 0x2b, 0x8b, 0x21, 0xf5, 0x9e, 0xf5, 0x7e, 0x21, 0x53, 0x02, 0x03, 0x52, 0x5f, 0xf8, 0xbc, 0x76, 0x1d, + 0xf4, 0x3a, 0x2a, 0xdd, 0x7e, 0x59, 0xb4, 0x16, 0x85, 0x94, 0x27, 0x92, 0x52, 0x92, 0x4d, 0x5c, 0xc5, 0xcc, 0x30, + 0xcd, 0x3b, 0xbf, 0xa9, 0x27, 0xfd, 0x55, 0x6d, 0xf6, 0xf5, 0xd6, 0x66, 0x6f, 0x08, 0xaf, 0x79, 0x8a, 0xb0, 0x7a, + 0xb7, 0x4e, 0x39, 0x5e, 0xbd, 0xed, 0xf4, 0x2f, 0x5a, 0xfb, 0xf4, 0xbd, 0x5b, 0xc3, 0xd8, 0xa8, 0x9c, 0x15, 0xca, + 0x6f, 0x72, 0x4a, 0xc3, 0x35, 0xa3, 0x0d, 0x1b, 0x61, 0x8a, 0x7d, 0xb9, 0x7a, 0xb7, 0x3a, 0x61, 0x85, 0x48, 0xef, + 0xc1, 0x33, 0xc2, 0xe3, 0xcd, 0x1f, 0x24, 0x54, 0xfd, 0x82, 0x8c, 0xe5, 0x8d, 0xba, 0xb5, 0x68, 0x3c, 0xda, 0x46, + 0xce, 0x24, 0xcc, 0x37, 0xe8, 0xa6, 0xc9, 0x6c, 0x6d, 0xc2, 0xa9, 0x23, 0xb7, 0x49, 0xb1, 0x19, 0xa9, 0x6a, 0xef, + 0x32, 0x98, 0xd2, 0x7d, 0xd2, 0x3e, 0xb1, 0xa7, 0xd4, 0x63, 0xd9, 0x19, 0x62, 0x5a, 0x10, 0xa0, 0xa6, 0x5c, 0xb5, + 0x57, 0x88, 0x65, 0x70, 0x4a, 0x5b, 0x4f, 0xb6, 0xcf, 0x30, 0x5b, 0x34, 0x93, 0x10, 0x9c, 0x15, 0x5a, 0x36, 0xdd, + 0xa4, 0xad, 0x04, 0x2f, 0x23, 0xd5, 0x68, 0xb4, 0x99, 0xe0, 0xb1, 0xf3, 0x5e, 0x34, 0xf3, 0x43, 0x87, 0xb4, 0xb0, + 0x16, 0x25, 0xfc, 0xc2, 0x91, 0x9c, 0xa5, 0x8d, 0xe0, 0xb4, 0x86, 0xee, 0xde, 0x35, 0xaf, 0xb7, 0xf7, 0x23, 0x1f, + 0xd8, 0x78, 0xd3, 0x88, 0x34, 0xc7, 0x7a, 0xc3, 0xbe, 0x09, 0xc6, 0x04, 0x1c, 0x78, 0xe6, 0xe5, 0x2f, 0x9e, 0x00, + 0xe7, 0x07, 0xd8, 0x90, 0x5e, 0xe6, 0xab, 0x8a, 0x60, 0x25, 0xaa, 0x34, 0xe3, 0xc2, 0xec, 0x31, 0xe8, 0xbb, 0x6d, + 0xe9, 0x37, 0xe3, 0xcf, 0x4c, 0x1c, 0xa5, 0x70, 0xf2, 0x9c, 0x6e, 0x2c, 0xdc, 0x43, 0x02, 0xbe, 0x21, 0xab, 0x9e, + 0x78, 0x93, 0xd3, 0xb8, 0xc1, 0xf5, 0x9b, 0x57, 0xb3, 0x13, 0x3e, 0x28, 0xcd, 0x0a, 0x10, 0xb2, 0xeb, 0x50, 0xd9, + 0xf0, 0x32, 0x53, 0x55, 0x7b, 0xad, 0x9c, 0xdc, 0x2f, 0xc4, 0x88, 0x82, 0x52, 0x31, 0x1f, 0x1f, 0xc8, 0x28, 0x8d, + 0xe2, 0xa2, 0xe4, 0xde, 0x43, 0x8a, 0x5d, 0x73, 0xde, 0xd0, 0x29, 0x3f, 0xa7, 0x81, 0xb6, 0x7e, 0x37, 0x14, 0x5e, + 0xfd, 0xee, 0x1e, 0x8d, 0x1d, 0xdc, 0x7a, 0x7a, 0xeb, 0x64, 0xbd, 0xb1, 0xb5, 0xf9, 0x08, 0x39, 0x35, 0x20, 0x7e, + 0x63, 0xc2, 0x1f, 0x7d, 0x7b, 0xa9, 0x29, 0xac, 0xa1, 0xb1, 0x8f, 0x6c, 0x6e, 0xc4, 0x56, 0x78, 0xe3, 0xd4, 0x0a, + 0x5f, 0x82, 0x28, 0x16, 0xe3, 0x17, 0x3f, 0x6b, 0x34, 0xb8, 0xa6, 0x12, 0x1a, 0x0e, 0x09, 0xee, 0x45, 0x91, 0xa7, + 0x9f, 0xba, 0xf8, 0x59, 0x5c, 0xbc, 0x98, 0xaf, 0x86, 0xc4, 0xcc, 0xd3, 0xb6, 0xd2, 0x62, 0xd9, 0xb4, 0x15, 0x3f, + 0x5b, 0x13, 0x0d, 0x77, 0xd1, 0x1a, 0x9f, 0xd5, 0xd8, 0x56, 0x55, 0xaa, 0x1f, 0xca, 0xef, 0x7b, 0x1b, 0x9b, 0x4c, + 0x9d, 0x81, 0x0e, 0x92, 0x86, 0xa4, 0x97, 0x8a, 0x6e, 0x81, 0x8c, 0x3d, 0x3d, 0x26, 0x0d, 0x4b, 0xc4, 0x58, 0x05, + 0xa1, 0x9c, 0x61, 0xd6, 0x8e, 0x72, 0xf3, 0xb0, 0xde, 0x42, 0xaf, 0xd8, 0x6d, 0x4c, 0x7a, 0xea, 0x8d, 0x65, 0x79, + 0xd6, 0xaa, 0xfb, 0x1c, 0x05, 0x14, 0xff, 0x1c, 0xee, 0xc0, 0x1f, 0x6e, 0x0d, 0x9a, 0xbd, 0x51, 0xb9, 0xd8, 0xd4, + 0xeb, 0x10, 0x6f, 0xd2, 0x1d, 0x8f, 0x25, 0x64, 0x21, 0xa2, 0xf1, 0x4d, 0x37, 0x05, 0x0c, 0xcd, 0x54, 0x46, 0x1d, + 0x4b, 0xe3, 0x28, 0xa8, 0x88, 0x15, 0xd9, 0x3b, 0x47, 0xe4, 0x55, 0x41, 0x85, 0x20, 0xad, 0x59, 0x36, 0x09, 0x99, + 0x7f, 0x1a, 0x64, 0x40, 0x49, 0x61, 0x13, 0xfd, 0x69, 0x13, 0x27, 0x85, 0x04, 0xdc, 0xad, 0xec, 0xa2, 0x8b, 0xad, + 0xa9, 0x15, 0xfa, 0x0c, 0x46, 0x5b, 0x70, 0x14, 0xb2, 0x2a, 0x44, 0x0b, 0xcd, 0x7c, 0xc3, 0xbf, 0x45, 0x9e, 0x02, + 0x12, 0x44, 0x41, 0x13, 0x0e, 0x65, 0x37, 0xdd, 0x41, 0x8a, 0x74, 0xf4, 0x10, 0xc1, 0x07, 0xa4, 0x84, 0x0a, 0xd0, + 0x79, 0x1e, 0xd7, 0xdd, 0x4b, 0x4d, 0x23, 0x2a, 0x23, 0x1b, 0x7c, 0x4f, 0x07, 0x45, 0xae, 0x57, 0xac, 0xf4, 0xff, + 0x1f, 0x79, 0x2c, 0xe5, 0x05, 0xec, 0x50, 0xc0, 0x9b, 0x0f, 0xd8, 0x42, 0x8a, 0x43, 0xad, 0x9e, 0x2f, 0x28, 0x51, + 0x1c, 0x49, 0x34, 0xbd, 0xaf, 0x68, 0xa7, 0x47, 0x8c, 0x32, 0xf1, 0xe1, 0x24, 0x50, 0xb7, 0xcd, 0xad, 0xba, 0x64, + 0xb8, 0xba, 0xca, 0xda, 0x7f, 0x3a, 0xec, 0xab, 0xa9, 0x82, 0x0b, 0x3b, 0xbd, 0xb8, 0x83, 0x60, 0x0a, 0x85, 0x1e, + 0x3b, 0x7f, 0x87, 0x89, 0xca, 0xea, 0x2f, 0x24, 0x52, 0xac, 0x5a, 0x2e, 0x43, 0x03, 0x1c, 0xc4, 0x4d, 0x81, 0xda, + 0x11, 0x0c, 0x7a, 0xc6, 0x2e, 0x41, 0x1a, 0xcb, 0x72, 0x49, 0x65, 0x38, 0x69, 0xa5, 0xd5, 0xe7, 0x93, 0x23, 0xe4, + 0x31, 0xde, 0x49, 0xad, 0x12, 0x15, 0x9c, 0x3d, 0x96, 0x65, 0x6d, 0xd4, 0x73, 0xd8, 0x8c, 0xce, 0xb2, 0x8a, 0x5a, + 0xe7, 0xda, 0x6a, 0xa7, 0x14, 0x4a, 0x75, 0x2c, 0x08, 0x36, 0x2d, 0x1c, 0x0f, 0xd2, 0xc2, 0x0e, 0xf4, 0x74, 0x42, + 0x8d, 0x4b, 0x9a, 0x1d, 0x52, 0x91, 0x77, 0x8b, 0x8e, 0xd0, 0x4c, 0xa7, 0x1b, 0x74, 0x53, 0x1e, 0x2b, 0x82, 0x3a, + 0x98, 0xd9, 0x11, 0x86, 0x57, 0x87, 0x61, 0x3c, 0x47, 0x5f, 0x52, 0x36, 0xc4, 0xca, 0x15, 0xb7, 0xb3, 0x36, 0xa5, + 0x83, 0xb7, 0x0a, 0x3f, 0x34, 0x8f, 0xca, 0xc4, 0xe2, 0xa8, 0x58, 0xa1, 0xc4, 0x35, 0xcd, 0x68, 0x8b, 0xab, 0xa5, + 0x9b, 0x18, 0x23, 0xe4, 0x2b, 0xe6, 0xaf, 0x91, 0x32, 0xcc, 0x0d, 0x64, 0x0d, 0xd2, 0x45, 0x32, 0xc5, 0x2c, 0x4c, + 0x90, 0x71, 0xc0, 0x98, 0xf8, 0x3e, 0x5b, 0x5c, 0xfa, 0x33, 0x70, 0x85, 0x63, 0x36, 0xad, 0xa4, 0xfb, 0x10, 0x14, + 0xba, 0xef, 0xf1, 0xa0, 0x41, 0x3d, 0x76, 0x10, 0x3d, 0x53, 0x70, 0xf9, 0x5c, 0x62, 0xa3, 0x7e, 0x6f, 0xd8, 0xd9, + 0x11, 0x8a, 0xcd, 0x86, 0x04, 0x03, 0xcc, 0x27, 0x4a, 0xf7, 0x3e, 0xf8, 0xd0, 0xd2, 0x2f, 0x5f, 0xdf, 0x22, 0x84, + 0x0c, 0xe5, 0x4e, 0x94, 0x9a, 0x29, 0x81, 0x8a, 0xa6, 0xe6, 0xa0, 0x99, 0x0f, 0x4e, 0xdc, 0xed, 0xf0, 0x7a, 0x42, + 0x2f, 0x57, 0xbb, 0x75, 0x4f, 0xe5, 0xe5, 0x8a, 0x34, 0x42, 0xab, 0x4f, 0x1b, 0x95, 0x0f, 0xe6, 0xb1, 0xb8, 0x5e, + 0xe9, 0xae, 0x1f, 0xb8, 0x43, 0xab, 0xdd, 0xc5, 0x87, 0xd2, 0x32, 0x3e, 0xd4, 0x93, 0xac, 0xd7, 0x60, 0x11, 0x78, + 0xa8, 0xb1, 0x14, 0xcd, 0xf1, 0x26, 0xeb, 0xd9, 0x60, 0x53, 0xa3, 0xd9, 0x58, 0x4a, 0xcb, 0xdf, 0xdb, 0xb8, 0x5f, + 0x67, 0x53, 0x85, 0xd2, 0x87, 0x81, 0xf4, 0x3e, 0x81, 0x92, 0x39, 0x0a, 0xdd, 0xe9, 0x0c, 0x95, 0x8f, 0xe6, 0x09, + 0x30, 0x56, 0xf0, 0x8b, 0x4b, 0x1a, 0xd3, 0x59, 0x73, 0x9c, 0xaf, 0xe0, 0xd4, 0xe2, 0xa8, 0xb1, 0x75, 0xe9, 0x89, + 0x20, 0xbc, 0x5a, 0xdc, 0x12, 0xbd, 0x24, 0xe9, 0xa8, 0x23, 0x25, 0xfe, 0x53, 0x8c, 0x73, 0xaa, 0xa9, 0xa3, 0x9d, + 0x4d, 0xe3, 0x0d, 0x15, 0x9c, 0x01, 0x35, 0x39, 0x6c, 0xfb, 0x12, 0x0a, 0xe0, 0xff, 0x9d, 0xa6, 0x12, 0xf1, 0x32, + 0x11, 0x37, 0xab, 0x0a, 0x65, 0x40, 0x19, 0x01, 0x94, 0x5f, 0xab, 0x91, 0xd1, 0x37, 0x7e, 0x34, 0x51, 0x9f, 0xc7, + 0x98, 0x03, 0x1d, 0xb4, 0x34, 0xf9, 0x1b, 0x38, 0x22, 0xd9, 0x36, 0xc7, 0x66, 0x06, 0x55, 0x5b, 0x3c, 0x09, 0xbc, + 0x44, 0x34, 0x56, 0xb3, 0x7e, 0x8c, 0xdf, 0xa6, 0x73, 0x3f, 0x78, 0xeb, 0x00, 0xfc, 0xc2, 0x82, 0x6a, 0x67, 0xd6, + 0xea, 0x7b, 0x09, 0x1a, 0xf0, 0xa3, 0xd8, 0xe8, 0x2c, 0x75, 0x42, 0xcd, 0xc9, 0x0e, 0xbd, 0xa9, 0xc9, 0x39, 0x27, + 0xca, 0x9e, 0x4e, 0x66, 0x1c, 0x85, 0x43, 0xd4, 0x19, 0x71, 0xf2, 0xa9, 0xf7, 0xcd, 0x8c, 0x78, 0xf4, 0x89, 0x8b, + 0x84, 0x43, 0x70, 0x4e, 0x15, 0x5b, 0x36, 0x9b, 0x8b, 0xd8, 0xfc, 0x4c, 0x8a, 0x4d, 0xc6, 0x04, 0xab, 0x05, 0xbd, + 0xbf, 0x21, 0x12, 0xc2, 0xb4, 0x21, 0x24, 0x4b, 0x53, 0x93, 0x1a, 0x8f, 0x9b, 0xab, 0x60, 0x33, 0xba, 0xc4, 0xfc, + 0x73, 0x75, 0x41, 0xa1, 0xf0, 0x8a, 0x82, 0x9f, 0xd7, 0x70, 0xec, 0x35, 0xc2, 0xd8, 0x33, 0x24, 0xfa, 0xd0, 0x0e, + 0x9a, 0x19, 0xc9, 0x2d, 0xa6, 0xf6, 0x64, 0x47, 0xe0, 0x95, 0x97, 0x21, 0xdd, 0xb0, 0xdf, 0x04, 0x94, 0xc0, 0x6b, + 0xea, 0x9b, 0xe5, 0x50, 0xe6, 0x70, 0x39, 0xdd, 0xd5, 0x6f, 0x80, 0xc6, 0xce, 0x16, 0x1e, 0xa6, 0x68, 0x1d, 0xaa, + 0x7d, 0x48, 0x5d, 0x3d, 0xb3, 0x57, 0x31, 0x47, 0x39, 0x08, 0xea, 0xc6, 0x6c, 0x13, 0xfb, 0x3a, 0x75, 0x5d, 0x03, + 0xf5, 0x7b, 0xec, 0x67, 0xa0, 0xf5, 0x30, 0xd2, 0x6c, 0x1d, 0x4f, 0xe9, 0x2d, 0x12, 0x46, 0x5b, 0x4a, 0x24, 0x0d, + 0x93, 0x66, 0x4d, 0x6a, 0x00, 0xd3, 0x22, 0xcc, 0x41, 0xfd, 0x46, 0xef, 0xd8, 0x53, 0xc2, 0x74, 0x96, 0x6d, 0xd7, + 0xa8, 0x6c, 0x92, 0x3b, 0xce, 0x15, 0x3a, 0x09, 0x29, 0x15, 0x65, 0x5f, 0x32, 0x05, 0xa9, 0x3c, 0x26, 0x9c, 0xe3, + 0x6a, 0x40, 0x32, 0x4c, 0xa9, 0xa0, 0xf6, 0xd6, 0x59, 0x4a, 0x5d, 0x72, 0xe6, 0x08, 0x9f, 0x62, 0xf9, 0xb3, 0xaa, + 0x79, 0xd8, 0x54, 0x63, 0x38, 0xed, 0xd5, 0xcb, 0xc5, 0x82, 0x07, 0xf3, 0x27, 0x70, 0x01, 0x45, 0xbe, 0xa2, 0xa6, + 0x3c, 0x97, 0x87, 0x72, 0x12, 0x7d, 0x32, 0xfe, 0x3d, 0xd5, 0x2d, 0x88, 0x5c, 0xe6, 0x33, 0x44, 0x66, 0xfa, 0x8d, + 0xfb, 0xea, 0xc3, 0x72, 0xb0, 0xa9, 0x24, 0xa6, 0x7f, 0x3f, 0x79, 0xb7, 0x42, 0x19, 0x7e, 0xe8, 0x89, 0x6d, 0xd1, + 0x52, 0xd0, 0xb3, 0xc8, 0xa8, 0xc2, 0x1c, 0x91, 0x13, 0x25, 0x70, 0x96, 0x3d, 0x4a, 0xf0, 0x92, 0x1a, 0x3a, 0x42, + 0x5b, 0x10, 0x27, 0xac, 0xa8, 0x1c, 0xc9, 0xb3, 0xbf, 0x55, 0xf5, 0x92, 0xea, 0x94, 0xc7, 0x80, 0xd5, 0x5f, 0x7b, + 0xa8, 0xbe, 0xbe, 0xb7, 0x41, 0x04, 0x5b, 0xd0, 0xea, 0x71, 0xf8, 0x12, 0xc0, 0x41, 0x30, 0x59, 0x20, 0x53, 0x1e, + 0x53, 0x43, 0xf5, 0xaa, 0x6f, 0x4f, 0x8e, 0x42, 0xde, 0x80, 0x22, 0xdc, 0x12, 0x4f, 0xa7, 0xa7, 0x71, 0x29, 0x6e, + 0x3f, 0x95, 0xfe, 0x81, 0x2f, 0xc0, 0xae, 0x55, 0x0a, 0x1e, 0x60, 0x4a, 0xc2, 0x1b, 0x99, 0x0a, 0x6b, 0xf9, 0xa6, + 0xd3, 0x9b, 0x97, 0x3c, 0xc6, 0x43, 0xb8, 0xb7, 0x3b, 0x70, 0xd6, 0x57, 0xef, 0x35, 0x9a, 0xca, 0xc0, 0xc5, 0x14, + 0x6d, 0x38, 0x3a, 0xc0, 0xe5, 0x1c, 0x61, 0x59, 0xdd, 0x7d, 0x38, 0xa3, 0x54, 0x06, 0x91, 0x32, 0x3a, 0xec, 0xaa, + 0xb4, 0x98, 0xf4, 0xd8, 0x62, 0x16, 0xf2, 0x3e, 0xc5, 0x19, 0x61, 0x13, 0x51, 0x4c, 0x13, 0x7d, 0x58, 0x04, 0xe2, + 0x19, 0x18, 0xdd, 0xae, 0x5d, 0x3f, 0x90, 0xec, 0x0d, 0x43, 0x28, 0xbf, 0x54, 0xee, 0x52, 0xbb, 0xae, 0xba, 0x00, + 0x96, 0xef, 0xc2, 0x7c, 0x42, 0x90, 0xa7, 0xaf, 0x38, 0xac, 0xab, 0xdf, 0xca, 0xcc, 0x46, 0x6e, 0xac, 0x6e, 0x85, + 0x4e, 0x2e, 0xdc, 0xd6, 0x2b, 0x1d, 0xe8, 0x4c, 0x40, 0xc2, 0x07, 0xcf, 0xbe, 0x8f, 0xb6, 0x91, 0x4a, 0x32, 0x57, + 0xdc, 0x73, 0xde, 0x5b, 0x10, 0x56, 0x0f, 0x64, 0x3d, 0x22, 0x17, 0x24, 0xe8, 0x05, 0x1c, 0xcf, 0xe7, 0xd1, 0xa9, + 0x79, 0xc5, 0xde, 0x28, 0x9f, 0x18, 0x96, 0xb8, 0x99, 0x9b, 0x4f, 0x87, 0xa7, 0x88, 0xf4, 0x7d, 0x8c, 0x84, 0x83, + 0x30, 0x24, 0x55, 0xde, 0xbc, 0x91, 0x50, 0xc4, 0x53, 0xc9, 0xce, 0xb8, 0xdc, 0x9c, 0xd5, 0x88, 0xc5, 0xa5, 0x28, + 0xaf, 0x5e, 0x3b, 0x8a, 0xf2, 0x8e, 0xad, 0x5a, 0xd2, 0xce, 0xf6, 0x95, 0x32, 0xb6, 0x2a, 0x71, 0x44, 0x23, 0xa3, + 0x13, 0xb7, 0x7a, 0x51, 0x2a, 0x87, 0xac, 0x91, 0xb2, 0x81, 0x75, 0x65, 0x32, 0x0b, 0xca, 0xc3, 0xca, 0xf9, 0x22, + 0xee, 0xd8, 0x2e, 0x82, 0x56, 0xa1, 0xb0, 0x74, 0x50, 0x2f, 0x28, 0x7a, 0x3f, 0x00, 0x5b, 0x27, 0xf9, 0xdb, 0x52, + 0x74, 0xf1, 0x3d, 0x74, 0x95, 0x5d, 0xd0, 0x81, 0x30, 0x1e, 0x25, 0x69, 0x18, 0x18, 0xc8, 0x37, 0x0f, 0xb6, 0x23, + 0xd0, 0xe5, 0xf5, 0xf6, 0xa8, 0xa4, 0xd3, 0x29, 0xc8, 0x29, 0x03, 0xc0, 0x34, 0x51, 0x58, 0x5d, 0xad, 0x31, 0xfb, + 0xbb, 0xe3, 0xe2, 0x57, 0xc7, 0xae, 0x15, 0xcc, 0xf0, 0x41, 0xd8, 0xcd, 0xbd, 0xab, 0xad, 0xc1, 0x17, 0xa7, 0x8e, + 0x99, 0x58, 0x98, 0x95, 0x96, 0x3f, 0xaf, 0x37, 0xb3, 0x7f, 0x74, 0x7d, 0x4c, 0xe1, 0x03, 0x3d, 0x8e, 0x5d, 0x8b, + 0xda, 0x47, 0xf1, 0xbc, 0x94, 0xf8, 0xc2, 0xef, 0x25, 0x8f, 0x9b, 0xa1, 0xaf, 0xbe, 0x86, 0x39, 0x0c, 0xad, 0x58, + 0x1b, 0xa9, 0xfc, 0x02, 0x9b, 0xde, 0x84, 0x3b, 0x71, 0xc4, 0x80, 0x73, 0x3d, 0x47, 0x9a, 0xf9, 0xcc, 0x64, 0xeb, + 0x99, 0xa4, 0xd1, 0x30, 0x1d, 0x89, 0x38, 0x6a, 0x01, 0x2a, 0x3e, 0x71, 0x70, 0x7f, 0x78, 0xc0, 0x48, 0x71, 0x18, + 0xa3, 0x1f, 0x8b, 0x7c, 0x9b, 0x12, 0xcb, 0x86, 0xaf, 0xe0, 0xb9, 0x66, 0x97, 0x3f, 0xd8, 0x6f, 0x8d, 0x8b, 0x55, + 0x8f, 0x95, 0x41, 0xbc, 0x9c, 0xae, 0xd9, 0x1b, 0x94, 0xb1, 0x3a, 0x8f, 0xb0, 0xdc, 0x9b, 0xcc, 0xe6, 0xcc, 0x05, + 0x72, 0x12, 0xa8, 0xde, 0xe8, 0xe6, 0x97, 0x22, 0xba, 0xd5, 0xae, 0xc1, 0x39, 0x3f, 0x33, 0xdf, 0xa3, 0x48, 0xfc, + 0xe4, 0x47, 0x5d, 0x32, 0xcb, 0xd6, 0x09, 0x74, 0xb2, 0xec, 0xca, 0x8d, 0xef, 0xb6, 0xbe, 0x78, 0xb7, 0xbe, 0xb0, + 0xfe, 0x44, 0x86, 0xf3, 0x4b, 0x72, 0xf7, 0xf8, 0x27, 0xd6, 0x41, 0x6c, 0xfd, 0xe7, 0x2f, 0x94, 0xfc, 0x4f, 0xa9, + 0x71, 0xe7, 0x8f, 0x9d, 0x21, 0x54, 0x8c, 0x50, 0xe3, 0x0d, 0xc6, 0x82, 0x73, 0x77, 0xa4, 0x64, 0xdd, 0x8c, 0x71, + 0x5a, 0x49, 0x59, 0x03, 0xf7, 0x95, 0x26, 0xa7, 0x55, 0x6e, 0xaf, 0x05, 0x31, 0xbb, 0x34, 0xb5, 0x43, 0x81, 0x4f, + 0x7d, 0x26, 0x65, 0x49, 0x72, 0x9d, 0xf2, 0xec, 0x1f, 0xa9, 0xed, 0x08, 0x60, 0xae, 0x7e, 0x05, 0x10, 0x8c, 0xf3, + 0xe5, 0xee, 0x5a, 0xe9, 0xa9, 0xcf, 0x60, 0x54, 0x8f, 0xbc, 0xf4, 0x2a, 0x5f, 0x16, 0x71, 0xa5, 0x1b, 0xe5, 0x3b, + 0x5c, 0xc2, 0x46, 0xb4, 0x78, 0xf7, 0xd2, 0x6b, 0x85, 0xbc, 0xa3, 0x7c, 0x50, 0x55, 0x39, 0x8c, 0x8a, 0xe6, 0xab, + 0x9b, 0x12, 0x2e, 0xb3, 0x77, 0xb0, 0xc9, 0x26, 0x1d, 0x74, 0x16, 0x6c, 0xc9, 0x04, 0x89, 0xc4, 0xb0, 0xec, 0x90, + 0x90, 0xab, 0xb4, 0x6f, 0xf0, 0x32, 0x54, 0xa7, 0x3a, 0xa7, 0xe8, 0xa7, 0x1d, 0x2f, 0xe9, 0x88, 0x69, 0xa1, 0x2d, + 0xd2, 0x11, 0xa5, 0x03, 0x63, 0xcc, 0x78, 0x85, 0x54, 0x35, 0x30, 0xbc, 0x56, 0x13, 0x4f, 0xb1, 0xbc, 0xf6, 0xe0, + 0x31, 0x91, 0x09, 0x62, 0xdf, 0xc2, 0xd5, 0x46, 0x1c, 0xdc, 0xc8, 0xf2, 0x5a, 0xb9, 0x0a, 0xaf, 0xee, 0xe1, 0x59, + 0xdb, 0x99, 0x7c, 0x48, 0x28, 0x90, 0x58, 0xe6, 0x17, 0x3a, 0x7e, 0xad, 0xa7, 0xbb, 0xe2, 0x25, 0xda, 0x65, 0x07, + 0x48, 0x53, 0x4b, 0xbc, 0x9c, 0xbe, 0xcb, 0x5e, 0x99, 0xd5, 0x4b, 0x4d, 0x1a, 0xdd, 0x42, 0xba, 0xf6, 0x08, 0xf1, + 0x30, 0x1f, 0x0a, 0x76, 0x5f, 0x92, 0x06, 0xe8, 0x0a, 0xb3, 0x5b, 0xfc, 0xa5, 0x8d, 0x0b, 0xf7, 0xe1, 0xa3, 0x88, + 0x48, 0xf4, 0x25, 0xbf, 0x16, 0x2f, 0xfe, 0x93, 0xef, 0x48, 0xc0, 0xe8, 0x22, 0x3e, 0xc9, 0xed, 0x07, 0x56, 0x45, + 0x97, 0x09, 0xcd, 0xfd, 0xe2, 0x34, 0x81, 0xd9, 0x0d, 0xf4, 0xe2, 0x26, 0xf3, 0x35, 0xdd, 0x5d, 0x69, 0xea, 0xde, + 0x1f, 0x8e, 0x55, 0x1d, 0xb5, 0xf9, 0xd2, 0x53, 0x77, 0x93, 0xed, 0x75, 0x8d, 0x4b, 0xdd, 0x42, 0x4d, 0xba, 0x62, + 0x6b, 0x6d, 0x51, 0x9f, 0xa6, 0x79, 0x6f, 0x56, 0xfe, 0x55, 0xb6, 0x75, 0x03, 0xb8, 0x5e, 0x88, 0x75, 0xcd, 0x46, + 0x4d, 0x90, 0xb2, 0xd4, 0x85, 0x68, 0xdf, 0xb8, 0x01, 0x68, 0xb1, 0x86, 0x75, 0x9d, 0x2a, 0xd3, 0x79, 0x9a, 0x8f, + 0x26, 0x04, 0x7d, 0x1f, 0x07, 0x97, 0xf2, 0x46, 0xff, 0x8a, 0x46, 0xad, 0xea, 0xf3, 0xcd, 0x73, 0x1e, 0x9d, 0x08, + 0x6f, 0x1c, 0xd0, 0x2e, 0x8d, 0x11, 0x2f, 0x3d, 0xfd, 0x4c, 0xf9, 0xd2, 0x8d, 0x51, 0x60, 0xbc, 0x00, 0x79, 0x3d, + 0xf4, 0xcb, 0x8d, 0x74, 0x81, 0x5e, 0x55, 0x46, 0x19, 0x5f, 0xdc, 0xcf, 0xbc, 0x7e, 0x25, 0x3e, 0xa0, 0x7c, 0x83, + 0x20, 0x54, 0x58, 0x56, 0x71, 0xc8, 0x20, 0x03, 0x7c, 0xdd, 0xa2, 0x5f, 0x46, 0xbf, 0x68, 0x73, 0x1e, 0xe6, 0x45, + 0xac, 0xbc, 0x39, 0xfc, 0x66, 0x78, 0x79, 0xf5, 0xbc, 0x1e, 0xf3, 0x03, 0xd9, 0xdb, 0xb5, 0xd2, 0x8e, 0x51, 0x3a, + 0x38, 0xc4, 0xae, 0x70, 0x9d, 0x02, 0x30, 0x2a, 0x41, 0xc7, 0x41, 0x54, 0x40, 0x5b, 0xfb, 0x81, 0xd4, 0x8a, 0x32, + 0x52, 0x90, 0x56, 0x6b, 0x38, 0x83, 0xb4, 0x03, 0x4a, 0xb6, 0x4d, 0xb3, 0x18, 0x99, 0x9d, 0x47, 0xba, 0xab, 0x8e, + 0xd3, 0xa2, 0xc1, 0x8b, 0xb3, 0x32, 0x31, 0xee, 0x51, 0x92, 0xab, 0xa4, 0x71, 0x63, 0x78, 0x79, 0xf3, 0x46, 0xa4, + 0x1b, 0xf7, 0x87, 0x4b, 0xae, 0x6e, 0xd9, 0xb9, 0x04, 0xa7, 0x37, 0xbf, 0x04, 0xe2, 0xe3, 0xa6, 0xf9, 0xda, 0x47, + 0x02, 0xee, 0x3f, 0x97, 0x80, 0xbb, 0x62, 0xf2, 0x32, 0x9b, 0xc0, 0xe0, 0x47, 0xe3, 0x9c, 0x69, 0xf0, 0x03, 0x63, + 0xcf, 0x85, 0x5e, 0x0b, 0x18, 0xfc, 0xa8, 0x23, 0x5e, 0xa3, 0x96, 0x90, 0x0e, 0xa3, 0xd2, 0xf7, 0xc4, 0xd8, 0xa0, + 0x3d, 0x32, 0xdd, 0xeb, 0xe0, 0xc6, 0x10, 0x22, 0x2f, 0x4c, 0xa1, 0x70, 0x04, 0xc0, 0xf9, 0xff, 0x0a, 0x92, 0xe6, + 0x9b, 0x43, 0xe1, 0x02, 0xe4, 0xb9, 0xd6, 0x8d, 0x48, 0x87, 0x4e, 0x2c, 0x63, 0xd8, 0x11, 0x33, 0x66, 0x63, 0xa6, + 0xaa, 0xe8, 0x31, 0x27, 0xce, 0x00, 0xca, 0x86, 0xaf, 0xc1, 0xd7, 0x36, 0x03, 0x13, 0xa2, 0x2a, 0x82, 0xc1, 0xbb, + 0xb7, 0xb0, 0x11, 0x74, 0x36, 0x25, 0x46, 0x34, 0x54, 0x43, 0x93, 0xaf, 0xf2, 0x62, 0x3b, 0xa1, 0xb1, 0x3f, 0x06, + 0x99, 0xac, 0x7e, 0xfa, 0xcc, 0x70, 0x1f, 0xeb, 0xf7, 0x3b, 0xd1, 0x56, 0x98, 0x14, 0xe4, 0xd3, 0x96, 0xa5, 0x73, + 0xe9, 0xc5, 0x25, 0x78, 0x69, 0xfa, 0xa6, 0xe6, 0x60, 0x7d, 0x99, 0x83, 0x2c, 0xa7, 0xfe, 0x3c, 0x98, 0x3b, 0x48, + 0x30, 0x75, 0x9e, 0x16, 0x01, 0x2e, 0x21, 0xa2, 0xf4, 0x5c, 0x66, 0x04, 0x36, 0x93, 0x87, 0x99, 0x6c, 0xae, 0xb0, + 0x78, 0x7e, 0xa4, 0x99, 0x9b, 0x51, 0xa1, 0xd7, 0xfd, 0xfc, 0xee, 0xa3, 0x34, 0xfb, 0xba, 0x3c, 0x8e, 0xbb, 0x5b, + 0xcd, 0x19, 0x88, 0xaa, 0x9d, 0xd2, 0x83, 0x5f, 0x64, 0x1d, 0xd8, 0xdb, 0xd6, 0xf4, 0xed, 0xe3, 0x9f, 0x7e, 0xe9, + 0x90, 0x4c, 0x9d, 0xdb, 0xd0, 0x59, 0x74, 0xfa, 0x7e, 0x8f, 0x91, 0x36, 0x5b, 0xe1, 0x88, 0x81, 0xca, 0x53, 0x43, + 0x36, 0xa9, 0x37, 0x71, 0x82, 0x1b, 0x1f, 0x91, 0xaa, 0x4d, 0x7f, 0x03, 0x8f, 0xf5, 0xc3, 0x8f, 0xe6, 0x4e, 0xd5, + 0xed, 0x85, 0xef, 0xdb, 0x3b, 0xa1, 0xdd, 0x3c, 0xbe, 0x56, 0xaf, 0xcd, 0xfb, 0xce, 0x48, 0x5d, 0x50, 0xf4, 0xbc, + 0xf6, 0xbf, 0x52, 0x33, 0x0e, 0xde, 0x36, 0xf7, 0x89, 0x81, 0x6f, 0xc7, 0xe7, 0x31, 0xcf, 0x80, 0xac, 0x65, 0x16, + 0x2d, 0x8d, 0x5c, 0xe3, 0x1a, 0x07, 0x94, 0x15, 0xe2, 0x8a, 0x66, 0xaa, 0x8d, 0x87, 0xa8, 0xeb, 0x1d, 0x2f, 0x67, + 0x2b, 0x5c, 0xdc, 0x62, 0x5a, 0xc5, 0x37, 0x71, 0xe1, 0xec, 0xe6, 0x99, 0xe2, 0x2a, 0x9b, 0x53, 0x75, 0x91, 0xe9, + 0x77, 0x41, 0x57, 0x1d, 0x06, 0xc1, 0x66, 0xd2, 0x87, 0xeb, 0xce, 0x43, 0x17, 0x6e, 0x5c, 0x0c, 0x0f, 0x01, 0xa9, + 0xb4, 0x9c, 0x40, 0x01, 0x63, 0x5b, 0xdc, 0x50, 0x96, 0x38, 0xbe, 0xfe, 0xf9, 0xc0, 0xc3, 0x00, 0xf0, 0x8d, 0x3d, + 0xc4, 0xc4, 0x6c, 0x65, 0x33, 0xcd, 0x09, 0x3f, 0xc3, 0x40, 0x8e, 0x2b, 0xef, 0x34, 0x6e, 0x87, 0xff, 0x33, 0x36, + 0x11, 0x29, 0xa0, 0x49, 0x2c, 0x2c, 0x64, 0xa6, 0xed, 0x14, 0x7d, 0xa2, 0x10, 0xba, 0x62, 0x29, 0x1f, 0x5c, 0xe6, + 0xe0, 0xbb, 0xd6, 0x7b, 0x5f, 0x57, 0x7e, 0x7d, 0x10, 0xb4, 0x54, 0x4d, 0xb0, 0x96, 0x14, 0x0a, 0x49, 0x60, 0xed, + 0x48, 0xa7, 0xf5, 0xb5, 0x1d, 0x28, 0x28, 0x59, 0x16, 0x44, 0xd2, 0xf9, 0x5a, 0x3b, 0xa4, 0x4e, 0xc5, 0x5f, 0xf8, + 0x6f, 0x3f, 0x4d, 0xe0, 0xd7, 0x56, 0xc4, 0x40, 0x7d, 0x1d, 0x5f, 0x77, 0x5f, 0x45, 0xbb, 0x21, 0x6d, 0xd5, 0x8f, + 0xa9, 0xb2, 0x99, 0x91, 0xf2, 0x7e, 0xac, 0xfe, 0xfc, 0xd9, 0x86, 0xa1, 0x69, 0xe2, 0x78, 0x78, 0x73, 0x33, 0x77, + 0x98, 0x29, 0x9f, 0x43, 0xaf, 0x36, 0x56, 0xdf, 0x00, 0x6c, 0x91, 0x93, 0xda, 0x35, 0x51, 0x30, 0xc1, 0x34, 0xd9, + 0x44, 0xdf, 0x3d, 0x52, 0x92, 0x98, 0xb5, 0x47, 0xa1, 0x77, 0x97, 0x32, 0x2d, 0x5a, 0xaa, 0xb9, 0x1a, 0x0b, 0x65, + 0x3a, 0xa9, 0x18, 0x6c, 0x6a, 0xfc, 0xd9, 0x95, 0xf3, 0x99, 0x53, 0x10, 0x79, 0xb1, 0xe1, 0x91, 0xeb, 0x73, 0xc8, + 0xc3, 0xad, 0x7c, 0xd5, 0xe7, 0xe7, 0xf6, 0xc2, 0x2b, 0xde, 0xeb, 0xbd, 0x72, 0x1d, 0x6a, 0xe9, 0x31, 0xcf, 0x8b, + 0xba, 0x5f, 0x96, 0x6b, 0x1c, 0x18, 0x50, 0x3b, 0x01, 0xc6, 0xb9, 0x88, 0x02, 0x0c, 0xf0, 0x4a, 0xba, 0x67, 0x24, + 0x3d, 0x9e, 0xc5, 0x25, 0xfa, 0x91, 0xa1, 0x9a, 0xa7, 0xcd, 0x4b, 0x40, 0x94, 0x2a, 0x3b, 0xce, 0x2d, 0x9d, 0x4c, + 0xb3, 0xa8, 0x2d, 0xbd, 0x33, 0x9d, 0x46, 0xf9, 0xbe, 0x02, 0x80, 0xf4, 0x9d, 0x7e, 0xe4, 0x4c, 0x87, 0x72, 0x73, + 0x80, 0x70, 0xa3, 0x64, 0xc6, 0x8d, 0x89, 0xc2, 0xf3, 0x13, 0x03, 0x22, 0x84, 0xb8, 0x1a, 0xf8, 0xca, 0x4b, 0xda, + 0x27, 0x2a, 0x42, 0x43, 0xfc, 0x80, 0x1e, 0xdc, 0x87, 0x5b, 0xfb, 0xf7, 0x7e, 0x50, 0x55, 0x72, 0xb0, 0x0c, 0x25, + 0x46, 0xe9, 0xde, 0xf8, 0x55, 0x81, 0xdd, 0x4f, 0xcc, 0x4a, 0x2d, 0x11, 0x50, 0x69, 0xf9, 0x7e, 0x71, 0x51, 0xe6, + 0xfc, 0xe9, 0x0f, 0xd7, 0x71, 0x48, 0xa8, 0x91, 0x2f, 0x53, 0xd9, 0x01, 0xf9, 0xf0, 0x1d, 0xfd, 0x2c, 0xca, 0x6a, + 0x0a, 0xbf, 0x8d, 0x2d, 0xdc, 0x5d, 0x16, 0x59, 0xea, 0x00, 0x50, 0x84, 0x63, 0x34, 0x1b, 0x3f, 0xed, 0x92, 0x0c, + 0xed, 0xa2, 0x8d, 0xdf, 0x69, 0x49, 0x33, 0x2a, 0x2a, 0x8a, 0x86, 0xd0, 0x6c, 0x34, 0x43, 0x0a, 0xe6, 0x09, 0x7a, + 0xf1, 0x31, 0x3b, 0xf0, 0xe7, 0x46, 0x49, 0x59, 0xba, 0x35, 0x7f, 0xbd, 0xbd, 0x90, 0xac, 0xa7, 0xac, 0x6e, 0x8a, + 0x30, 0x59, 0xd0, 0x0c, 0x7d, 0xe5, 0xff, 0x30, 0x80, 0xa7, 0x90, 0x97, 0x2b, 0x16, 0xfe, 0x5e, 0xd5, 0x3d, 0x7c, + 0xb9, 0x11, 0xc7, 0xf5, 0xa2, 0x29, 0x1f, 0xb4, 0x0f, 0x21, 0xa9, 0xea, 0x7b, 0x1c, 0xf6, 0x9c, 0xfa, 0x8f, 0x85, + 0x4d, 0xb8, 0x15, 0x05, 0x02, 0xcf, 0x66, 0x2d, 0x9a, 0x88, 0xa9, 0xcb, 0x8c, 0x08, 0x63, 0x49, 0x10, 0xc4, 0xad, + 0xce, 0x79, 0x3e, 0xca, 0xcd, 0xc9, 0x49, 0x9e, 0xb7, 0xb3, 0xeb, 0x68, 0xdf, 0x9b, 0x5b, 0x29, 0xab, 0x5c, 0x37, + 0x84, 0x16, 0x2f, 0x5d, 0x5c, 0xa5, 0x32, 0x4c, 0xcb, 0x55, 0x71, 0x43, 0xab, 0xd6, 0xb4, 0x6a, 0xc0, 0x07, 0x19, + 0xb4, 0x2a, 0x4f, 0x9e, 0x76, 0x95, 0x9b, 0x6c, 0xd3, 0x97, 0x15, 0x5d, 0x77, 0xc0, 0xf0, 0x4a, 0x61, 0x6d, 0xd7, + 0xc1, 0x36, 0x9c, 0x68, 0x70, 0xde, 0xb7, 0xdb, 0x06, 0x90, 0xbc, 0xdd, 0xc5, 0x0a, 0x1e, 0x4e, 0x8e, 0xff, 0x62, + 0x87, 0xe2, 0xf7, 0xbe, 0x68, 0x65, 0x14, 0x23, 0x23, 0x34, 0xf5, 0xaf, 0x8e, 0x08, 0xff, 0xc2, 0x77, 0xa5, 0xf6, + 0x98, 0xab, 0x08, 0x65, 0xed, 0x66, 0x15, 0xfb, 0x83, 0x24, 0xbf, 0x34, 0x49, 0xf5, 0x36, 0x4f, 0x4f, 0xb0, 0x4a, + 0x41, 0x7b, 0x73, 0xd8, 0x60, 0x6b, 0xae, 0x8d, 0x14, 0x37, 0x98, 0xd0, 0xc6, 0xff, 0x60, 0x23, 0xc0, 0x27, 0x52, + 0xbc, 0xe0, 0x72, 0x5c, 0x59, 0x8a, 0xe6, 0x44, 0xf3, 0xd2, 0xc8, 0x3e, 0x85, 0x79, 0x3e, 0xaa, 0x90, 0xeb, 0xe6, + 0x3c, 0x50, 0x2f, 0x87, 0x3e, 0x71, 0xca, 0x38, 0xcf, 0x8e, 0x70, 0x3e, 0x95, 0xd3, 0xae, 0xde, 0xac, 0x2d, 0x43, + 0x5c, 0x27, 0x2b, 0x42, 0x48, 0x3e, 0x8c, 0x53, 0x51, 0xa4, 0xd8, 0xbe, 0xda, 0x39, 0xcf, 0xf1, 0x95, 0x21, 0x0a, + 0x27, 0x5c, 0x44, 0x63, 0x4a, 0xe8, 0x4f, 0x5e, 0x50, 0x74, 0x67, 0xd4, 0x24, 0x98, 0xb5, 0x3a, 0x99, 0x04, 0xce, + 0xd4, 0x7f, 0xc0, 0xc2, 0xd0, 0x1b, 0x20, 0x3a, 0xa8, 0xa9, 0x32, 0x3f, 0xba, 0x5b, 0x71, 0xe3, 0x93, 0x8e, 0xcc, + 0x68, 0x13, 0x33, 0xce, 0x94, 0xda, 0xe2, 0x6b, 0xb3, 0x7b, 0x8e, 0xc0, 0xec, 0x6e, 0x01, 0xc1, 0x22, 0x8e, 0x54, + 0x68, 0xd5, 0x9f, 0xab, 0x77, 0xbb, 0x48, 0x80, 0x73, 0x42, 0x1b, 0x03, 0x2d, 0x3e, 0xe3, 0x74, 0x35, 0xe7, 0xdb, + 0x38, 0xec, 0x18, 0x32, 0x55, 0x9c, 0xdf, 0x45, 0x9f, 0xfb, 0x99, 0x00, 0xdd, 0x2d, 0x44, 0x3a, 0xdf, 0x5b, 0x17, + 0x6a, 0x16, 0x0e, 0x21, 0x6c, 0x7f, 0x12, 0x25, 0x64, 0xa8, 0xbf, 0x16, 0x7e, 0x8e, 0xda, 0xab, 0x97, 0x5a, 0x26, + 0x1b, 0x7e, 0x30, 0xa2, 0xc5, 0xa3, 0x00, 0x92, 0x0c, 0xa3, 0xf7, 0xcf, 0xdf, 0xdc, 0xb0, 0x9f, 0xa1, 0xf0, 0x0c, + 0xe6, 0x11, 0x50, 0xc0, 0xcd, 0xdd, 0x4f, 0xe8, 0xda, 0x52, 0x2e, 0x08, 0x67, 0xb2, 0x0d, 0x09, 0x56, 0xc6, 0xb9, + 0x66, 0x6b, 0xe3, 0x45, 0xc3, 0x09, 0xe9, 0x88, 0x3a, 0x68, 0x4c, 0x7a, 0x9e, 0x33, 0x9a, 0xc7, 0x58, 0xfd, 0xc9, + 0x99, 0x60, 0xf9, 0x81, 0x8d, 0xc9, 0x15, 0x04, 0x55, 0x8b, 0x82, 0x58, 0xd3, 0x1d, 0xed, 0xc0, 0x70, 0x7f, 0x29, + 0x9e, 0x12, 0xe4, 0x6f, 0x97, 0x98, 0x38, 0x2a, 0x14, 0x72, 0xd6, 0xb8, 0xa1, 0x6f, 0x44, 0xb0, 0x5e, 0x8d, 0x07, + 0xbd, 0xe7, 0x4b, 0x91, 0xa5, 0xaa, 0x73, 0xbb, 0x51, 0x0e, 0xcd, 0x30, 0x61, 0x8c, 0x13, 0x5a, 0xca, 0x37, 0x64, + 0x25, 0x76, 0x36, 0xb5, 0x14, 0x4e, 0xff, 0x69, 0xc8, 0x53, 0xb1, 0x85, 0x80, 0xaa, 0xcf, 0x41, 0x93, 0x13, 0xd3, + 0xd4, 0x9d, 0x37, 0x72, 0x67, 0x1e, 0x60, 0x54, 0x53, 0x36, 0x3a, 0xa1, 0x77, 0xcc, 0x47, 0x66, 0xf0, 0x33, 0xb2, + 0x3b, 0x0f, 0x59, 0x2d, 0x93, 0xcb, 0x24, 0x3f, 0xeb, 0x8d, 0xef, 0x1c, 0x20, 0xb1, 0x8e, 0x41, 0xc5, 0xe6, 0x59, + 0x57, 0x59, 0xab, 0x2a, 0xd3, 0x4d, 0xfc, 0xaa, 0x5b, 0x1a, 0x28, 0x78, 0xa2, 0x02, 0x85, 0x48, 0x9a, 0x92, 0xa0, + 0x56, 0x0f, 0x21, 0x47, 0x94, 0xa3, 0xbb, 0x45, 0xcc, 0x75, 0xbc, 0xaa, 0x6c, 0xfc, 0x1b, 0xd3, 0x47, 0x8b, 0xda, + 0xa1, 0xdb, 0xcf, 0x6c, 0x54, 0xc3, 0x22, 0x55, 0x4e, 0x61, 0xc8, 0x8f, 0x38, 0x8f, 0x35, 0x09, 0xb2, 0x71, 0x32, + 0x00, 0x05, 0xbd, 0x54, 0xe0, 0x7f, 0x33, 0xe7, 0x8c, 0x15, 0x2b, 0x17, 0xa0, 0x22, 0x58, 0xbb, 0xe6, 0x5f, 0xf7, + 0x69, 0xc4, 0x28, 0x54, 0x67, 0x0f, 0xc0, 0xac, 0x85, 0x0c, 0xe4, 0x57, 0xeb, 0x6d, 0x28, 0x17, 0xb6, 0xe1, 0xa4, + 0xf5, 0xba, 0xfa, 0x2c, 0xe4, 0x22, 0xad, 0xa6, 0x68, 0xb3, 0x3a, 0x4f, 0x9d, 0x15, 0x4c, 0xf8, 0x25, 0x9c, 0x9b, + 0x4e, 0x90, 0xa5, 0xc6, 0x91, 0xf2, 0x30, 0xfb, 0x38, 0x6a, 0x9d, 0x59, 0x39, 0x76, 0xa1, 0x0a, 0xdb, 0x3c, 0xcf, + 0x9c, 0x30, 0xbd, 0xd8, 0x93, 0xaa, 0xda, 0x95, 0x95, 0xee, 0xe6, 0x5a, 0xcc, 0x9b, 0x5d, 0x1d, 0x49, 0x2d, 0x31, + 0xad, 0x93, 0xfd, 0x89, 0x95, 0x59, 0x81, 0xe0, 0x6d, 0xe8, 0x36, 0x42, 0x64, 0x17, 0xec, 0x47, 0x5a, 0xbc, 0x74, + 0x4b, 0xae, 0x8e, 0x60, 0x11, 0x5a, 0x45, 0xff, 0x50, 0x5a, 0x18, 0x90, 0xea, 0x8a, 0x92, 0xd2, 0x48, 0xff, 0xad, + 0xcc, 0x70, 0x92, 0x59, 0xbd, 0x77, 0xa8, 0x3d, 0x16, 0x41, 0xbd, 0x1f, 0x93, 0x1e, 0xe5, 0x5c, 0x2f, 0x05, 0x9c, + 0x2c, 0x81, 0xd9, 0x0b, 0x76, 0x0b, 0x00, 0x79, 0xed, 0x6d, 0x2d, 0x15, 0x99, 0x70, 0xf9, 0x3c, 0x99, 0x73, 0x69, + 0x15, 0x78, 0x05, 0xbd, 0x6b, 0x6f, 0xb0, 0xb2, 0x10, 0xdc, 0x2f, 0x72, 0xa6, 0xcf, 0x0a, 0x92, 0x4a, 0x43, 0xbc, + 0xb4, 0x04, 0xde, 0x4a, 0xaa, 0x29, 0x70, 0x6b, 0xd9, 0x70, 0x6d, 0xda, 0x46, 0x1f, 0xea, 0xfd, 0x78, 0xc7, 0x68, + 0x15, 0xfc, 0xe7, 0xd3, 0xdf, 0x2a, 0x76, 0x47, 0xf0, 0x6c, 0x15, 0xaa, 0xac, 0xeb, 0x61, 0x22, 0xd9, 0xfe, 0x6a, + 0xe7, 0x0b, 0xa0, 0x45, 0xb8, 0x52, 0xba, 0x26, 0x01, 0x9d, 0xd4, 0x14, 0x0b, 0xdc, 0xa6, 0xc0, 0x2c, 0xa3, 0x9f, + 0xc2, 0xb7, 0x91, 0x6b, 0x1c, 0xa9, 0x46, 0x34, 0x99, 0x71, 0xb8, 0x20, 0x9a, 0xbc, 0xb9, 0x5b, 0x15, 0x01, 0x04, + 0x07, 0x68, 0x2b, 0xef, 0x8c, 0xd3, 0x3b, 0xf7, 0x91, 0xd6, 0x39, 0xf0, 0x43, 0x37, 0xd9, 0x2e, 0x75, 0x68, 0xd5, + 0x12, 0xbd, 0x5d, 0x47, 0x8d, 0x06, 0x19, 0xb6, 0x44, 0x31, 0xb6, 0xe0, 0xe3, 0x13, 0x3e, 0x66, 0x90, 0x55, 0x72, + 0xc0, 0xd7, 0x8b, 0x06, 0x2a, 0x16, 0x15, 0xc8, 0xdf, 0x85, 0x50, 0xa8, 0xa3, 0x6d, 0xb4, 0x00, 0x40, 0x7d, 0x82, + 0x12, 0x3a, 0x71, 0x4b, 0xbd, 0x01, 0x55, 0xbe, 0x0f, 0x29, 0x95, 0x50, 0xdf, 0x54, 0x64, 0xca, 0xd1, 0x52, 0x31, + 0x03, 0x84, 0x91, 0x47, 0x26, 0x43, 0x6d, 0xe2, 0x2c, 0x62, 0xee, 0xde, 0x32, 0xaa, 0x7e, 0x6c, 0xcf, 0x3b, 0x59, + 0xda, 0x6b, 0x11, 0x73, 0x95, 0x33, 0xde, 0x07, 0x50, 0x02, 0x07, 0x57, 0x81, 0xb9, 0x67, 0xaa, 0x77, 0x55, 0xbc, + 0xcf, 0x2c, 0xb3, 0x86, 0x07, 0x4a, 0xcf, 0x2e, 0xc6, 0xd7, 0x98, 0xeb, 0xcf, 0xad, 0x89, 0x67, 0xf1, 0x5f, 0x1f, + 0xb7, 0x7c, 0x9e, 0xc3, 0xef, 0x26, 0xda, 0xd5, 0x19, 0xb8, 0x72, 0xc2, 0x3e, 0x4f, 0xd0, 0xae, 0x1b, 0xbc, 0x5b, + 0xb6, 0x16, 0x6b, 0x9e, 0xbc, 0x09, 0xef, 0x5b, 0x33, 0x87, 0xaa, 0xaa, 0x3c, 0xae, 0x36, 0x10, 0x48, 0xe3, 0x3b, + 0x93, 0xcc, 0xa0, 0x6b, 0x48, 0x9a, 0xe9, 0x46, 0xf0, 0xbb, 0x6f, 0xdd, 0x82, 0x8e, 0x34, 0xb0, 0xd8, 0xda, 0x3b, + 0x81, 0xcf, 0x4c, 0x86, 0x15, 0xb3, 0xe4, 0x0c, 0x7e, 0x7b, 0x1b, 0xc2, 0xd3, 0xd6, 0x9b, 0x72, 0xb9, 0x22, 0x8b, + 0x3e, 0x0f, 0xfd, 0x8a, 0x7e, 0x93, 0x96, 0xe5, 0x71, 0x0f, 0x55, 0x72, 0xff, 0x57, 0xb1, 0xe6, 0x34, 0xfa, 0x2a, + 0xa8, 0x5f, 0xbd, 0x63, 0xc0, 0xe6, 0xb6, 0xf6, 0x16, 0x72, 0xba, 0xb4, 0xc8, 0x3d, 0x18, 0x9a, 0xe9, 0xfd, 0x8f, + 0x02, 0x61, 0xc9, 0x9e, 0xd2, 0xd6, 0xf3, 0xe4, 0xa2, 0x97, 0xea, 0xdc, 0x88, 0x7f, 0xcb, 0x95, 0xdf, 0xbc, 0x8e, + 0x1a, 0xa5, 0x89, 0xff, 0x83, 0xff, 0xb5, 0x51, 0x26, 0x97, 0x3a, 0xb9, 0xd3, 0x0e, 0xca, 0xa3, 0x2e, 0x39, 0x1e, + 0xc5, 0x52, 0x33, 0x1a, 0xc5, 0x33, 0x61, 0x9f, 0xb9, 0xa0, 0x2a, 0xf4, 0x58, 0x36, 0x00, 0x6b, 0x18, 0x40, 0x32, + 0xa0, 0x26, 0x67, 0xc4, 0xa9, 0x3b, 0xc1, 0xad, 0x86, 0xd2, 0x55, 0x64, 0x46, 0x72, 0x5a, 0x78, 0x97, 0xf7, 0x2b, + 0x31, 0x44, 0xb9, 0xac, 0x6f, 0x52, 0x47, 0x54, 0x7c, 0x15, 0x5d, 0x4a, 0xdf, 0x22, 0x36, 0xda, 0x7e, 0xd8, 0xd0, + 0x8e, 0x39, 0x60, 0xe4, 0xbd, 0xd1, 0xa8, 0xe5, 0xcc, 0x20, 0xe6, 0xa7, 0x67, 0xd0, 0xc4, 0x01, 0xb3, 0x15, 0x43, + 0xcc, 0x51, 0x72, 0x55, 0x6a, 0xd2, 0x18, 0x14, 0x13, 0x3b, 0x71, 0xa4, 0x3e, 0xbf, 0xee, 0x4e, 0x0a, 0x3f, 0xcc, + 0xa9, 0xa9, 0x75, 0x3f, 0x80, 0x2d, 0x3e, 0xd5, 0xfa, 0x1d, 0x55, 0x18, 0x98, 0xed, 0x1a, 0x22, 0xfc, 0x8d, 0x8a, + 0x8b, 0xf4, 0x24, 0xfd, 0x3b, 0xf5, 0x55, 0x75, 0x1b, 0x31, 0x64, 0xcc, 0xec, 0x04, 0x6b, 0x26, 0x07, 0xb4, 0x2c, + 0xce, 0xcc, 0x2c, 0xe5, 0xb3, 0x71, 0x2c, 0xb1, 0x16, 0x58, 0x6c, 0x79, 0x9b, 0x07, 0x77, 0x68, 0x41, 0xa8, 0x48, + 0x9c, 0x58, 0xb6, 0x31, 0x73, 0x13, 0xda, 0xe0, 0x09, 0xb1, 0xa2, 0x5f, 0xf0, 0x8d, 0x10, 0x3f, 0x3a, 0xe8, 0x4d, + 0x6a, 0xa7, 0xd1, 0x95, 0xd1, 0xc1, 0x38, 0xbc, 0xe6, 0xbf, 0x5d, 0x37, 0x11, 0x74, 0x89, 0xb8, 0xa9, 0x80, 0x4b, + 0x8e, 0x9f, 0x62, 0x50, 0x27, 0x37, 0x83, 0x4d, 0x7c, 0xa7, 0xe3, 0xad, 0x1d, 0xac, 0x77, 0xc0, 0xb9, 0x3f, 0xfe, + 0x3b, 0x71, 0x1b, 0xa5, 0x5c, 0x9e, 0xfc, 0x16, 0x3b, 0x19, 0xa2, 0x39, 0x4f, 0x6f, 0x1d, 0x5e, 0x2d, 0xd2, 0x4c, + 0x75, 0x6a, 0x7a, 0x73, 0x3c, 0xd2, 0x09, 0xfc, 0x95, 0xf1, 0xec, 0x82, 0xe3, 0xb4, 0x60, 0x05, 0xe5, 0x03, 0x7e, + 0x0f, 0xa5, 0x1a, 0xae, 0x5c, 0xf4, 0x75, 0x40, 0x3d, 0x53, 0x7c, 0x59, 0x8d, 0xb5, 0x6f, 0xd2, 0x2d, 0xf8, 0xc3, + 0x1e, 0x16, 0x65, 0x5d, 0x3f, 0x3f, 0x7f, 0xb3, 0x97, 0x8d, 0xf4, 0xfc, 0x77, 0x60, 0x49, 0xfd, 0x53, 0x09, 0xaa, + 0xf6, 0xa6, 0xe6, 0x8d, 0x83, 0x78, 0x1a, 0x53, 0x1a, 0xd1, 0xff, 0xd2, 0x31, 0x75, 0x55, 0x06, 0x57, 0xc0, 0x3c, + 0x78, 0x12, 0x93, 0xa5, 0x9f, 0x8d, 0xa9, 0xa5, 0xf0, 0x6b, 0xcc, 0x4f, 0x6a, 0xf5, 0x90, 0xe3, 0x3c, 0xe4, 0xe2, + 0x95, 0xa4, 0x7b, 0x6f, 0x56, 0xdf, 0xce, 0x16, 0x06, 0xa7, 0xf9, 0x2a, 0x80, 0xff, 0xc7, 0x39, 0x01, 0x74, 0xf7, + 0xcc, 0xc5, 0x63, 0x9e, 0x7c, 0x78, 0xb3, 0xb5, 0x9a, 0x16, 0xe4, 0xdd, 0x79, 0x2a, 0xcd, 0xd6, 0x82, 0x58, 0x9b, + 0x7a, 0x34, 0x41, 0xbd, 0xd3, 0x5b, 0xd3, 0xbe, 0xb1, 0x3e, 0x8c, 0x86, 0xbe, 0x23, 0x0b, 0x85, 0xe7, 0x8f, 0x09, + 0x67, 0xc7, 0xb3, 0x89, 0x89, 0x61, 0xbf, 0x53, 0xed, 0x62, 0x60, 0xab, 0xab, 0x15, 0x0b, 0xc6, 0xfb, 0x81, 0xee, + 0x9b, 0x4c, 0x96, 0x72, 0x3c, 0xc6, 0x4c, 0x25, 0x6a, 0xda, 0xb7, 0xd4, 0xb2, 0xbb, 0x17, 0x28, 0x23, 0x66, 0xa9, + 0x81, 0xd9, 0x17, 0xaf, 0x0a, 0x0c, 0x14, 0xaa, 0xf3, 0xe1, 0x8d, 0x15, 0x94, 0xc1, 0x47, 0xf3, 0xba, 0x94, 0x15, + 0x04, 0x8e, 0x49, 0xeb, 0xc0, 0xfd, 0xf2, 0x40, 0x8f, 0x14, 0x7d, 0xf1, 0x36, 0x0a, 0x58, 0x5e, 0xd7, 0x53, 0x83, + 0xb7, 0x1a, 0xae, 0x8d, 0xf5, 0x32, 0xe3, 0x97, 0xf5, 0x40, 0x61, 0x14, 0x5c, 0xdc, 0x99, 0x5d, 0x8c, 0xc3, 0xbe, + 0xdb, 0x2a, 0x67, 0x4a, 0xa6, 0x5c, 0xaf, 0x6c, 0x7e, 0xc6, 0x40, 0xcf, 0x9b, 0xb5, 0xac, 0x71, 0xfd, 0xc4, 0xef, + 0x6e, 0x8e, 0x2b, 0xe3, 0x6c, 0x14, 0xba, 0xff, 0x23, 0x1b, 0x6a, 0x7c, 0x03, 0x35, 0x82, 0x90, 0x83, 0xab, 0xa5, + 0xb2, 0x34, 0xd2, 0x7e, 0xb6, 0x9f, 0xbe, 0x4f, 0x1e, 0x2b, 0xc8, 0xf2, 0x5f, 0xb2, 0x62, 0x63, 0x0e, 0x93, 0xc9, + 0xaf, 0x3a, 0x85, 0x74, 0x40, 0xd5, 0xa2, 0x1d, 0xa3, 0x57, 0xd9, 0x09, 0x41, 0x7d, 0x31, 0x10, 0x75, 0x00, 0x66, + 0x5b, 0xa5, 0xbc, 0x2c, 0x06, 0x9a, 0x49, 0x94, 0x2d, 0x07, 0x7d, 0x6d, 0xf8, 0xf0, 0x1a, 0xbc, 0x6a, 0x94, 0xd5, + 0xf4, 0xb2, 0x9a, 0x42, 0xa5, 0xd3, 0xa6, 0x95, 0xe0, 0x35, 0x79, 0xba, 0x5f, 0xea, 0x5c, 0x77, 0x4d, 0x1c, 0xfc, + 0x6c, 0xf5, 0x7b, 0xb0, 0xa3, 0xc9, 0xb1, 0x2b, 0xb9, 0xb9, 0xc1, 0x71, 0x1e, 0x73, 0x5c, 0xb9, 0x40, 0x44, 0xcd, + 0x42, 0x2b, 0x18, 0xd0, 0x22, 0x75, 0xa7, 0xbe, 0xbb, 0xc4, 0x6e, 0x02, 0xd8, 0x2a, 0xf6, 0x1e, 0x24, 0xdb, 0x3e, + 0x4b, 0x6f, 0x74, 0x60, 0x3b, 0x78, 0x8b, 0x26, 0xbe, 0x31, 0x57, 0xaa, 0xa9, 0xc8, 0xea, 0x8c, 0xea, 0xb0, 0x73, + 0x9a, 0xcf, 0x0f, 0x9a, 0xb1, 0x72, 0x9b, 0x84, 0xdb, 0x31, 0x52, 0x27, 0x88, 0x05, 0x2a, 0x56, 0xd3, 0xa0, 0x5a, + 0x46, 0x50, 0xb9, 0x49, 0xfa, 0xca, 0x23, 0x59, 0x8d, 0x15, 0xeb, 0x67, 0xa0, 0x6e, 0xae, 0xdc, 0xb8, 0x6d, 0x86, + 0xac, 0x5a, 0xae, 0x70, 0x46, 0x20, 0x86, 0xc6, 0x67, 0xd6, 0x48, 0x54, 0x5b, 0x09, 0xe8, 0xc0, 0xe1, 0x22, 0x05, + 0xb5, 0xbb, 0x2d, 0xaf, 0xdf, 0x8d, 0xd2, 0x23, 0x4a, 0x54, 0xd4, 0x8a, 0xca, 0x29, 0xdd, 0x50, 0xae, 0x9e, 0x89, + 0x26, 0x60, 0xa2, 0x51, 0x6c, 0xa4, 0x16, 0xe5, 0xed, 0x56, 0x85, 0xec, 0xe5, 0xba, 0x7f, 0x79, 0xff, 0x91, 0xd3, + 0xb0, 0xe9, 0x3b, 0x21, 0x69, 0x30, 0x48, 0x45, 0xc2, 0x07, 0xec, 0xa8, 0xb7, 0xe4, 0x9b, 0xcc, 0x90, 0xa9, 0x23, + 0x63, 0xd4, 0x97, 0x58, 0xf9, 0xd2, 0xfc, 0xdd, 0xab, 0x7b, 0xa3, 0x80, 0xad, 0xdf, 0xe9, 0xda, 0xdc, 0x94, 0xc2, + 0xdb, 0x0e, 0x61, 0x0a, 0xe9, 0x26, 0x23, 0xd2, 0xfa, 0xcf, 0x54, 0xfd, 0x66, 0xe2, 0x77, 0x35, 0xb6, 0x6b, 0x82, + 0x3c, 0xd1, 0x9b, 0xcd, 0xe6, 0x9c, 0xaa, 0x59, 0x00, 0x20, 0xfe, 0xab, 0xcd, 0x37, 0xf3, 0x95, 0x2a, 0x1a, 0x88, + 0xe0, 0xb3, 0xd0, 0xf5, 0x6f, 0x64, 0x54, 0x7d, 0x1a, 0xd1, 0xbf, 0x06, 0x49, 0x08, 0x65, 0xce, 0xe6, 0x7a, 0x43, + 0x50, 0xc7, 0x9e, 0x67, 0x6f, 0xf5, 0x29, 0x4c, 0xfc, 0x8f, 0xbc, 0xfa, 0x39, 0xee, 0x55, 0x14, 0xa5, 0xd8, 0xd5, + 0xa1, 0x71, 0x98, 0xc2, 0x4d, 0xa6, 0x5b, 0xef, 0x92, 0x21, 0xe0, 0xf4, 0x5f, 0x1c, 0x0e, 0x23, 0x73, 0xd3, 0x9d, + 0x0d, 0x0c, 0x06, 0x05, 0x23, 0x29, 0x96, 0x21, 0x94, 0xb9, 0xc1, 0x5c, 0xbc, 0x75, 0x80, 0x2f, 0x5d, 0x90, 0xe5, + 0x9b, 0x85, 0x8e, 0xf1, 0xd9, 0xb7, 0xe7, 0x1d, 0x1f, 0xa9, 0xd0, 0x32, 0x4b, 0x04, 0x29, 0xa4, 0x2f, 0xfe, 0x19, + 0x46, 0x2d, 0x8f, 0x89, 0x0b, 0xa6, 0xd5, 0xc3, 0x4b, 0x29, 0xc0, 0xce, 0x73, 0x50, 0x53, 0x2f, 0xa0, 0x8e, 0x85, + 0x9b, 0xca, 0x03, 0xbb, 0x12, 0x43, 0x6a, 0x53, 0x04, 0x30, 0x7e, 0xeb, 0x08, 0x11, 0x0f, 0xd2, 0xa0, 0x54, 0x4b, + 0xc8, 0x78, 0xb3, 0x9c, 0x58, 0x77, 0x17, 0x03, 0xe2, 0x9b, 0x23, 0x06, 0xb4, 0xa5, 0x66, 0x18, 0x1e, 0xe7, 0x5f, + 0x4b, 0x79, 0x13, 0x32, 0x88, 0x5d, 0x03, 0x5d, 0x49, 0xb9, 0x59, 0xfb, 0xe1, 0x18, 0xa8, 0xda, 0x86, 0x44, 0xe9, + 0x37, 0xd5, 0x95, 0x75, 0x25, 0x56, 0xa8, 0x56, 0x3b, 0xbb, 0x37, 0x79, 0x9d, 0x36, 0x34, 0xc3, 0x53, 0xb8, 0xb9, + 0x52, 0xdb, 0xc6, 0xae, 0xed, 0xff, 0x24, 0x73, 0xd0, 0x14, 0xac, 0x95, 0x1f, 0xec, 0x78, 0x36, 0xd1, 0xbf, 0x9e, + 0xd5, 0x99, 0x74, 0xfd, 0x51, 0x79, 0x96, 0x9f, 0x5b, 0x75, 0x50, 0x81, 0x87, 0xd3, 0x22, 0xff, 0xd1, 0xd7, 0x70, + 0x0d, 0xbd, 0x27, 0xef, 0x7a, 0xbb, 0xc1, 0x18, 0xbe, 0x78, 0x13, 0x4f, 0xfb, 0x9b, 0x4c, 0xe0, 0x14, 0xc2, 0xb6, + 0x75, 0x02, 0xd6, 0x3a, 0x7d, 0x47, 0x52, 0xd0, 0x22, 0xbf, 0x45, 0xb3, 0x5f, 0x2b, 0x73, 0xc3, 0x2f, 0x1c, 0xc5, + 0xcd, 0xa5, 0x74, 0x91, 0x3c, 0x59, 0xa5, 0xed, 0x30, 0xcb, 0x20, 0x8e, 0xc0, 0x72, 0xf4, 0x73, 0x27, 0x72, 0xeb, + 0x63, 0x35, 0xcc, 0xee, 0x38, 0x0e, 0xc5, 0xa8, 0x7e, 0xaa, 0x23, 0x52, 0x1e, 0x26, 0x03, 0x36, 0x35, 0xa1, 0xc5, + 0x58, 0x58, 0xba, 0x24, 0x41, 0x0a, 0x74, 0x80, 0x5a, 0x22, 0x73, 0x52, 0x8b, 0xec, 0x8a, 0x71, 0xcf, 0xb6, 0x62, + 0xe9, 0xda, 0xc7, 0x47, 0x9d, 0x3d, 0x03, 0x37, 0x8e, 0x93, 0x93, 0xcd, 0x9d, 0x2d, 0xc0, 0x4a, 0x8f, 0xc9, 0xe9, + 0xec, 0x87, 0x12, 0xcb, 0x35, 0xd9, 0x7d, 0x54, 0xb4, 0xbb, 0xef, 0xe0, 0x88, 0x2c, 0x11, 0xa3, 0xff, 0xb4, 0xce, + 0x64, 0xad, 0xbf, 0x91, 0x03, 0xf8, 0x16, 0x1a, 0xf5, 0x82, 0xc5, 0x80, 0xcb, 0xdd, 0xe5, 0x5d, 0x8d, 0x0f, 0xbc, + 0x32, 0xe1, 0xac, 0x2a, 0xd7, 0xdc, 0x6c, 0x64, 0x9a, 0xa8, 0x09, 0xe9, 0xff, 0x2b, 0x5b, 0x0d, 0xb1, 0x05, 0x78, + 0x32, 0xf6, 0xcd, 0x9b, 0x0d, 0x4c, 0xcd, 0x42, 0x8b, 0x2b, 0xec, 0x43, 0x1c, 0xa7, 0x22, 0xba, 0xb9, 0x81, 0x1a, + 0x7e, 0x90, 0xd0, 0xca, 0x77, 0x09, 0x55, 0xff, 0x41, 0x34, 0xf6, 0xbd, 0x57, 0x59, 0xc2, 0x41, 0xcf, 0x41, 0xa6, + 0xd1, 0xbd, 0x66, 0xd2, 0x93, 0xbd, 0xb9, 0x31, 0x54, 0x8d, 0xbc, 0x56, 0xee, 0x1e, 0xdc, 0x2d, 0xe1, 0xf9, 0xd9, + 0x9c, 0xf7, 0xe6, 0x23, 0xe1, 0x51, 0x37, 0x5e, 0xf5, 0x0f, 0x71, 0x87, 0xaf, 0xae, 0x1f, 0x27, 0x62, 0x45, 0x11, + 0x17, 0x1f, 0xd6, 0xbb, 0x5a, 0x79, 0xdc, 0x3a, 0x3c, 0xc5, 0xfb, 0x06, 0x74, 0x4a, 0x4a, 0x75, 0xde, 0x35, 0x81, + 0xae, 0xe0, 0xfb, 0x73, 0xed, 0xf2, 0xfd, 0x8d, 0xb3, 0x6e, 0xcb, 0xcd, 0xc6, 0xc1, 0x1b, 0x93, 0x2e, 0x5a, 0xb0, + 0xeb, 0x3b, 0x9e, 0xbe, 0xf9, 0x38, 0xfc, 0x68, 0x64, 0x58, 0xd5, 0x58, 0x40, 0x1b, 0x5a, 0xbe, 0x20, 0xef, 0xc9, + 0x22, 0x46, 0x77, 0xa5, 0xc9, 0x53, 0x72, 0xbb, 0xf9, 0x3e, 0x44, 0xbc, 0x59, 0x07, 0xba, 0x72, 0xd0, 0xdd, 0xf8, + 0xd7, 0xfa, 0xe5, 0x65, 0xe9, 0xde, 0xbc, 0x7a, 0xee, 0xb5, 0x90, 0x30, 0xa9, 0xf3, 0xc9, 0x20, 0x97, 0x0f, 0x86, + 0xc8, 0xc8, 0xe6, 0x18, 0xcf, 0x24, 0x65, 0x09, 0xbc, 0x1c, 0x57, 0x19, 0xbc, 0x33, 0x6d, 0xe4, 0x1f, 0xf7, 0x44, + 0x22, 0x1e, 0x0c, 0xb4, 0x6d, 0x50, 0x28, 0x4c, 0xea, 0xed, 0x62, 0x88, 0x7b, 0x94, 0x31, 0xd1, 0x3c, 0x76, 0x7d, + 0xbf, 0x46, 0x27, 0x47, 0x6f, 0x66, 0xd4, 0x6e, 0xff, 0x61, 0x35, 0x05, 0x7a, 0xe2, 0xe0, 0x89, 0xba, 0xa2, 0x12, + 0x1e, 0xff, 0xf4, 0x89, 0xf6, 0x4b, 0x7a, 0x38, 0x55, 0x87, 0xe7, 0xab, 0xf8, 0xca, 0x45, 0x55, 0x2b, 0x7e, 0x09, + 0xfa, 0x70, 0xb1, 0xc8, 0xc9, 0xf3, 0x48, 0xaf, 0x6c, 0xf6, 0x6a, 0x66, 0x13, 0xc5, 0x9d, 0xc2, 0xf2, 0xb8, 0xf9, + 0x8a, 0xe6, 0xd4, 0x90, 0x68, 0xf5, 0xef, 0x43, 0x7f, 0x0c, 0xf6, 0x36, 0xfb, 0xbf, 0x25, 0x71, 0xe6, 0xe9, 0x33, + 0xe2, 0x77, 0xb3, 0xf5, 0x92, 0x1f, 0xba, 0xbf, 0xc4, 0xbf, 0x8f, 0x4d, 0xa0, 0x59, 0xa6, 0x34, 0x51, 0xc6, 0x30, + 0x00, 0x38, 0x00, 0x7e, 0x6d, 0xfe, 0xe2, 0xdf, 0x2d, 0x9b, 0xdc, 0xcc, 0xe2, 0xa4, 0xc5, 0x9d, 0x7f, 0xfa, 0x42, + 0x69, 0x69, 0x9c, 0xe6, 0x01, 0x41, 0x35, 0xae, 0x4d, 0x8f, 0x8d, 0x64, 0x1e, 0xc8, 0x3a, 0x18, 0xb6, 0x96, 0x9c, + 0x60, 0x02, 0x22, 0xf7, 0xaa, 0xe6, 0x4b, 0x97, 0x6a, 0x65, 0x96, 0xa9, 0xcd, 0xd7, 0xd2, 0xc1, 0x60, 0xdf, 0x41, + 0xcc, 0xf7, 0xb9, 0xc7, 0x6c, 0x26, 0x3f, 0xb7, 0xb4, 0xe0, 0x6f, 0xa5, 0x3c, 0x19, 0x73, 0xf3, 0x46, 0x28, 0x2e, + 0x3e, 0x0a, 0xcc, 0x70, 0x46, 0xb0, 0x50, 0xab, 0xaf, 0xbc, 0x89, 0x0d, 0xff, 0x50, 0x12, 0x78, 0xb1, 0x7b, 0xb9, + 0xf2, 0x0a, 0xbc, 0x09, 0xed, 0x1f, 0x28, 0xff, 0xef, 0xa9, 0x96, 0xbd, 0xbc, 0x57, 0xa7, 0xb6, 0xe3, 0x5a, 0x50, + 0x91, 0x54, 0x05, 0x6f, 0xd7, 0xbf, 0x65, 0xa2, 0x81, 0xe5, 0xc9, 0x52, 0xf6, 0xb5, 0x33, 0xf0, 0xb1, 0x81, 0x2e, + 0xf5, 0x95, 0x54, 0xbd, 0x10, 0x67, 0x2c, 0x24, 0xcd, 0x0c, 0x80, 0xe8, 0x75, 0x9f, 0x9e, 0x54, 0xd3, 0xb0, 0x57, + 0x67, 0x2b, 0x7a, 0xd6, 0x88, 0x91, 0xde, 0xa5, 0xd2, 0x98, 0x3d, 0x3d, 0x52, 0xa6, 0xcf, 0x3b, 0x3f, 0x2a, 0x6f, + 0x48, 0x66, 0x1b, 0x12, 0xfc, 0x29, 0x2f, 0x50, 0x52, 0x66, 0xdb, 0x8a, 0x4d, 0xf1, 0x66, 0xee, 0x02, 0x98, 0xac, + 0x27, 0x98, 0xbb, 0x6f, 0x5e, 0x72, 0x30, 0xc6, 0xba, 0x52, 0x45, 0xb9, 0xf1, 0x79, 0x9c, 0x75, 0xb9, 0x43, 0xd8, + 0x44, 0x16, 0x3d, 0x07, 0x81, 0xcd, 0xea, 0x5a, 0x1e, 0xcc, 0xc7, 0x9c, 0x64, 0x97, 0x35, 0xfa, 0x85, 0x49, 0x90, + 0x6e, 0xde, 0xf0, 0x5c, 0xb3, 0x42, 0xde, 0xbc, 0x2f, 0xb9, 0x11, 0xcc, 0x60, 0xb4, 0x11, 0x29, 0xb4, 0x75, 0xca, + 0xb0, 0x8f, 0x88, 0x5e, 0x49, 0x98, 0xfe, 0x41, 0x9e, 0xaf, 0x7e, 0x10, 0xa6, 0xe7, 0xeb, 0x05, 0xaa, 0xfa, 0x87, + 0x02, 0x5e, 0x4c, 0x38, 0xc0, 0x02, 0xea, 0xe8, 0xa5, 0x5c, 0xc7, 0x9a, 0xa0, 0x9c, 0x70, 0xa9, 0xaf, 0xd9, 0x28, + 0xaf, 0xa5, 0xfa, 0x84, 0xd6, 0xb1, 0x66, 0x03, 0x4c, 0x46, 0x37, 0xb6, 0xf1, 0xb7, 0x31, 0xb7, 0xe9, 0xb2, 0x7f, + 0xaa, 0xd8, 0x1e, 0x82, 0xb2, 0xe1, 0x02, 0x3e, 0xf7, 0x08, 0xdc, 0xb9, 0x9e, 0x80, 0xd6, 0x10, 0xff, 0xe3, 0x38, + 0xd6, 0xf2, 0x65, 0x9d, 0x29, 0x89, 0x55, 0x16, 0x42, 0x85, 0xca, 0x89, 0xfd, 0xdc, 0x30, 0xd7, 0x7a, 0x1c, 0x5c, + 0x23, 0xc1, 0x40, 0x70, 0x0a, 0x30, 0x89, 0xab, 0x29, 0x0d, 0x8d, 0x3b, 0x47, 0x7f, 0x78, 0x2d, 0xbf, 0xf0, 0xaa, + 0x5c, 0x17, 0xdc, 0xf4, 0xbd, 0x19, 0x01, 0xf3, 0x0b, 0xfb, 0xc2, 0xd1, 0x45, 0xcb, 0xe8, 0xfa, 0xec, 0x80, 0x04, + 0xc8, 0x63, 0x65, 0x19, 0x49, 0xd8, 0x92, 0xb5, 0x7a, 0x93, 0x9f, 0xef, 0x99, 0x42, 0x24, 0x5b, 0xa0, 0xca, 0xf1, + 0x0b, 0x6c, 0x2d, 0x2d, 0xa9, 0x64, 0x25, 0x5a, 0xab, 0x50, 0x81, 0x68, 0xad, 0x09, 0xd5, 0xaa, 0xd3, 0x7b, 0xdf, + 0x22, 0x3a, 0x2f, 0x8d, 0xd4, 0x21, 0x86, 0x80, 0x88, 0xa5, 0xf5, 0x9d, 0xd2, 0x46, 0xeb, 0xc9, 0xb2, 0xb8, 0xaf, + 0xc6, 0xf6, 0x6b, 0xb8, 0x7a, 0x26, 0xde, 0x54, 0xde, 0xd6, 0xc5, 0xc3, 0x9c, 0x55, 0x4e, 0x74, 0x5d, 0x87, 0x69, + 0xb3, 0xb6, 0xd3, 0x5f, 0xd5, 0x55, 0x26, 0x43, 0xf0, 0xb1, 0x87, 0x50, 0x73, 0xa1, 0x4a, 0x85, 0x48, 0x2f, 0x77, + 0x62, 0x73, 0xe5, 0x1e, 0x73, 0xa5, 0x73, 0x1c, 0xd9, 0x3a, 0xb6, 0x93, 0xe1, 0xa9, 0xc9, 0x05, 0x71, 0xec, 0xee, + 0x7e, 0x88, 0x0b, 0xfe, 0xcf, 0x17, 0xd2, 0x9c, 0xc7, 0xe7, 0x2f, 0xfd, 0xf4, 0x93, 0xb1, 0x92, 0xd2, 0x38, 0x99, + 0x65, 0x4d, 0x2f, 0xcb, 0x20, 0xce, 0x7f, 0xc6, 0xcb, 0x9c, 0x85, 0xd7, 0x59, 0xfb, 0x57, 0xc3, 0xad, 0x38, 0xb4, + 0x2e, 0x45, 0x32, 0x45, 0xb9, 0xfb, 0xd7, 0x71, 0x12, 0x22, 0xc3, 0x9f, 0xf3, 0x86, 0xb1, 0xf6, 0x69, 0xd5, 0x7c, + 0x24, 0x2b, 0x76, 0xf6, 0x7e, 0xe9, 0xb1, 0x71, 0x51, 0x70, 0x27, 0xc8, 0x95, 0x56, 0x4a, 0x0e, 0x8e, 0x03, 0x4d, + 0xe5, 0x03, 0x05, 0x7f, 0x98, 0x92, 0xc6, 0x53, 0xcc, 0x56, 0xdf, 0xa7, 0x36, 0xcb, 0x98, 0x0c, 0x8f, 0x74, 0x66, + 0xcc, 0x46, 0xad, 0xa0, 0xb4, 0xc7, 0xf9, 0xb0, 0xb0, 0xce, 0x69, 0x9b, 0x71, 0x4c, 0xf2, 0xc7, 0xb7, 0x0a, 0xd9, + 0xaa, 0x7c, 0xa9, 0xf7, 0x7b, 0x69, 0x6f, 0x93, 0x17, 0x2b, 0x7a, 0x2b, 0x4c, 0x84, 0x81, 0x88, 0x4a, 0x15, 0x34, + 0x12, 0xb2, 0xb0, 0xd3, 0x4e, 0xed, 0x0c, 0x55, 0x69, 0x31, 0x00, 0x3f, 0x86, 0xf5, 0xf1, 0xf8, 0x5a, 0x34, 0xa6, + 0xd6, 0x51, 0x23, 0x36, 0x2e, 0xe7, 0x19, 0x00, 0x2f, 0x54, 0x3c, 0xb3, 0x62, 0xfa, 0x8c, 0x9c, 0x39, 0x82, 0x2a, + 0x0b, 0x41, 0xda, 0x61, 0x28, 0xb6, 0xdc, 0x98, 0xaa, 0x0d, 0xe4, 0xc2, 0x9f, 0x75, 0x52, 0xa5, 0x11, 0xca, 0x21, + 0xd7, 0x26, 0xef, 0x32, 0xdf, 0x20, 0x44, 0x1f, 0xda, 0xf8, 0xeb, 0xc9, 0x8d, 0x04, 0x64, 0x0a, 0x38, 0x8f, 0x34, + 0x5e, 0xd3, 0xf7, 0x3c, 0x03, 0xde, 0x54, 0x6f, 0x92, 0x04, 0xe4, 0x59, 0x75, 0xa2, 0xdb, 0xf0, 0x90, 0x3c, 0xfb, + 0xad, 0x1c, 0x95, 0x7b, 0x72, 0xa5, 0x65, 0xdf, 0xea, 0x36, 0x63, 0xbe, 0x64, 0xed, 0xd2, 0xda, 0xdb, 0x09, 0xb3, + 0x4e, 0x53, 0x65, 0x4a, 0xc4, 0x83, 0x4a, 0xd2, 0xda, 0x19, 0x40, 0x98, 0xfa, 0xe9, 0x5b, 0xd4, 0x8e, 0x37, 0x92, + 0x73, 0x93, 0x01, 0x0b, 0xaa, 0xac, 0x5c, 0x76, 0x81, 0x44, 0x40, 0x6e, 0xdb, 0xf8, 0xa6, 0xc9, 0x12, 0x8c, 0xc8, + 0x3f, 0xa0, 0x77, 0xc1, 0x1d, 0xd9, 0x5b, 0xa0, 0x3b, 0xd3, 0xc7, 0x9e, 0x1a, 0xef, 0xca, 0x9a, 0xec, 0x42, 0x66, + 0xbe, 0x89, 0x81, 0x6b, 0x57, 0x2d, 0x21, 0xe1, 0xba, 0xb1, 0xcb, 0xbc, 0xa8, 0x33, 0x99, 0xad, 0x59, 0x95, 0xc7, + 0x6a, 0x98, 0x4a, 0x87, 0xa9, 0x9a, 0xb0, 0x25, 0xc8, 0x05, 0x84, 0xcb, 0x6b, 0x97, 0xeb, 0xf8, 0x2a, 0x01, 0x22, + 0x3d, 0x88, 0x93, 0x62, 0xec, 0xb9, 0x91, 0x77, 0xd7, 0xcb, 0x0a, 0x14, 0xc6, 0x3b, 0x6b, 0x92, 0x93, 0x4b, 0xed, + 0x4f, 0xc6, 0xdb, 0x56, 0x33, 0xdd, 0x8e, 0x2f, 0x12, 0xba, 0x16, 0xc7, 0x16, 0x7c, 0x49, 0xed, 0xde, 0xd5, 0x22, + 0x57, 0xed, 0x65, 0x01, 0xa3, 0x6d, 0x74, 0xd6, 0x6d, 0xb1, 0x30, 0xa7, 0x44, 0x38, 0x59, 0x36, 0xe6, 0x3b, 0x11, + 0x5e, 0x24, 0xd6, 0x18, 0xa8, 0x9d, 0x79, 0xe3, 0x4f, 0x0c, 0xc1, 0x09, 0xbe, 0x10, 0x5c, 0x2c, 0x8d, 0xf9, 0xf4, + 0x05, 0x11, 0xb1, 0x59, 0x1c, 0x9e, 0xad, 0x9b, 0xe0, 0x74, 0x8d, 0xeb, 0x0d, 0xb8, 0x1b, 0x58, 0xd4, 0xdf, 0xd1, + 0x83, 0x79, 0xfb, 0xa3, 0xb0, 0x69, 0x20, 0xc3, 0xe8, 0xd1, 0x23, 0x41, 0xdc, 0xd9, 0x1c, 0x4b, 0x4a, 0x24, 0x1c, + 0xf1, 0xeb, 0xe7, 0x08, 0x16, 0xb5, 0x2b, 0xa3, 0xa3, 0x31, 0x97, 0xfa, 0x07, 0xb9, 0xb4, 0xed, 0x2b, 0x60, 0xf1, + 0xcf, 0x50, 0x92, 0x94, 0x9d, 0x31, 0xc8, 0x6b, 0xdb, 0x80, 0xa9, 0x0a, 0xa8, 0xe3, 0x10, 0x7e, 0x52, 0x12, 0xee, + 0x66, 0x6b, 0x4a, 0xe5, 0xd2, 0x8c, 0x62, 0xcf, 0x1b, 0x44, 0xd1, 0xc5, 0x16, 0xe1, 0x24, 0x03, 0x27, 0xfa, 0x6a, + 0xa3, 0x20, 0x6f, 0xb5, 0xbd, 0xf8, 0x3c, 0x03, 0x67, 0x1d, 0x3a, 0x05, 0x34, 0x19, 0x25, 0x0d, 0xa1, 0x42, 0x1b, + 0xc2, 0xac, 0x0d, 0x2e, 0x5b, 0x11, 0x9a, 0x86, 0xcc, 0xb0, 0x0f, 0xf3, 0x79, 0xe0, 0x8c, 0x22, 0x41, 0x4f, 0xbb, + 0xd4, 0x6f, 0x56, 0xbf, 0xb9, 0x30, 0xdf, 0xdd, 0x48, 0x27, 0x02, 0x10, 0xad, 0xf4, 0xe9, 0xa1, 0x78, 0x91, 0x5b, + 0x10, 0x51, 0x6b, 0x0e, 0x6f, 0x09, 0x0e, 0x3e, 0x26, 0x2c, 0xb5, 0xea, 0xae, 0xb6, 0xf8, 0x17, 0x09, 0xdf, 0xb5, + 0x79, 0x40, 0xcc, 0x46, 0x6f, 0xe8, 0xfa, 0x5e, 0x9a, 0xa7, 0x92, 0xea, 0x89, 0x2d, 0x06, 0x2e, 0x0b, 0x05, 0x55, + 0xfc, 0x66, 0x7c, 0x8d, 0x91, 0x15, 0x01, 0x34, 0x38, 0xbd, 0xc5, 0x08, 0x1c, 0x32, 0xe6, 0xe5, 0xd8, 0x1f, 0xd7, + 0x6c, 0x82, 0x7c, 0xd6, 0x98, 0x90, 0x88, 0xb7, 0xbd, 0x37, 0xd8, 0x2a, 0x94, 0x8d, 0x44, 0x5a, 0x1e, 0x39, 0x8c, + 0x7b, 0x50, 0xf1, 0x30, 0x22, 0x36, 0xac, 0x29, 0xf3, 0x09, 0xa1, 0xcd, 0x1e, 0xc4, 0x9c, 0x5d, 0x98, 0xb0, 0xd0, + 0x4b, 0x0c, 0x44, 0xe8, 0x6d, 0x00, 0xfb, 0x46, 0x6c, 0x91, 0x48, 0x21, 0x89, 0x44, 0x3e, 0x9a, 0x13, 0xe2, 0xb0, + 0x15, 0x19, 0x1e, 0xac, 0xf6, 0x2e, 0x46, 0xf2, 0x67, 0x9c, 0x94, 0xd6, 0x65, 0x62, 0xf3, 0xc7, 0x28, 0x61, 0x0c, + 0x38, 0xbb, 0x3b, 0x29, 0xce, 0xbb, 0x61, 0xf9, 0xe8, 0x03, 0x15, 0x7c, 0xcb, 0x15, 0xc1, 0x1e, 0x4d, 0xe4, 0x48, + 0x95, 0x15, 0xcb, 0xb9, 0x7e, 0x14, 0x1a, 0x3c, 0x65, 0xe1, 0xa8, 0x6a, 0xc3, 0x48, 0x10, 0x51, 0x69, 0x5c, 0x30, + 0x5a, 0xc9, 0x40, 0x47, 0x63, 0xda, 0x6a, 0x44, 0xb8, 0x80, 0xe7, 0x59, 0xfb, 0xa7, 0x05, 0xe3, 0x3c, 0x5e, 0x86, + 0xe3, 0x0f, 0x9a, 0x41, 0xff, 0x1d, 0x99, 0x8c, 0x96, 0x4f, 0xee, 0x46, 0xff, 0x49, 0x3f, 0x68, 0x67, 0xef, 0xf7, + 0xd5, 0xe9, 0xc7, 0xbe, 0x5c, 0x48, 0x43, 0x7e, 0xa1, 0x2b, 0x57, 0x73, 0xbb, 0x35, 0x3c, 0x30, 0x35, 0xb7, 0xd3, + 0xeb, 0x04, 0xf5, 0xce, 0xb9, 0x41, 0xdb, 0x86, 0x0d, 0x4c, 0xe2, 0x31, 0xe7, 0xc9, 0x68, 0xac, 0xc8, 0x80, 0x5a, + 0xc1, 0xca, 0x3c, 0x4b, 0x70, 0xd7, 0x67, 0xc6, 0xe0, 0x9e, 0xb8, 0x28, 0xb3, 0xe4, 0xde, 0x07, 0xe0, 0x24, 0x68, + 0xfe, 0x92, 0xdd, 0xa2, 0x7e, 0xa2, 0x5a, 0x74, 0x07, 0x29, 0x43, 0xad, 0x25, 0xde, 0x57, 0xb5, 0xc6, 0x10, 0xec, + 0x0d, 0x00, 0xad, 0xa9, 0xd5, 0x87, 0x89, 0x1c, 0xf2, 0xc7, 0x56, 0xf5, 0x41, 0x69, 0xa2, 0x2e, 0x18, 0x90, 0xa7, + 0xe6, 0x97, 0x2e, 0x11, 0x26, 0x9d, 0xd4, 0xff, 0xab, 0x97, 0xff, 0x6d, 0x0c, 0x94, 0x89, 0xca, 0xdb, 0x90, 0x87, + 0x93, 0xc7, 0xbd, 0x29, 0xde, 0xd2, 0xf9, 0x46, 0x1b, 0xee, 0x04, 0x4f, 0xf2, 0xf0, 0xfa, 0xbc, 0xb5, 0x37, 0x43, + 0xdc, 0xd7, 0xd1, 0xa6, 0xb2, 0x6d, 0x52, 0x52, 0x52, 0x1d, 0x9c, 0x81, 0x25, 0xda, 0x05, 0x4d, 0xcb, 0x79, 0xa4, + 0x1c, 0xcb, 0x36, 0xa9, 0x72, 0x0b, 0x78, 0xca, 0x29, 0xe5, 0x3f, 0x04, 0x1d, 0xa5, 0x9a, 0x47, 0xcd, 0x65, 0x79, + 0xea, 0x52, 0x58, 0x5b, 0x21, 0xba, 0x37, 0xa7, 0xfc, 0x62, 0x96, 0xb4, 0x94, 0x6a, 0x93, 0x00, 0x91, 0xc6, 0x7b, + 0x9a, 0x58, 0xd6, 0x03, 0xe8, 0x44, 0xd5, 0x2e, 0x61, 0x12, 0x43, 0x3b, 0xd9, 0x86, 0xba, 0xfa, 0x68, 0x15, 0xd6, + 0xe7, 0x2f, 0x68, 0x78, 0xb5, 0xdf, 0xd2, 0x23, 0x46, 0xcd, 0x1a, 0xde, 0x1f, 0x1e, 0x4a, 0x70, 0xb1, 0x69, 0xec, + 0x6c, 0xb3, 0x26, 0x0e, 0x3b, 0x7e, 0x0e, 0x2b, 0x08, 0xa6, 0x67, 0x47, 0x1b, 0xc6, 0x6a, 0x70, 0x7c, 0x95, 0x5f, + 0xed, 0x7a, 0x31, 0xa0, 0x26, 0x52, 0xdc, 0x29, 0x72, 0xc0, 0x00, 0x13, 0x2d, 0xe4, 0xcd, 0xd3, 0x79, 0xfc, 0x21, + 0xbe, 0x1e, 0x0f, 0xb4, 0x9f, 0x20, 0x8f, 0x9e, 0x05, 0x8a, 0x0c, 0x50, 0xd1, 0x93, 0xfb, 0x8b, 0x53, 0x28, 0xc3, + 0x6e, 0xa2, 0xd3, 0x41, 0xd1, 0xed, 0xdd, 0x23, 0x6f, 0x7c, 0xbc, 0xa9, 0xca, 0xe5, 0x3c, 0xc2, 0x40, 0xd7, 0x1b, + 0xd8, 0x40, 0x11, 0x19, 0xcb, 0x2a, 0xc5, 0x8f, 0x31, 0xaa, 0x0c, 0x51, 0x70, 0xab, 0x4f, 0x58, 0xc3, 0x45, 0x60, + 0xef, 0x10, 0x26, 0x09, 0xa3, 0x47, 0xee, 0xb9, 0xa9, 0x79, 0x72, 0xcd, 0xec, 0x3c, 0xca, 0x1c, 0xac, 0x2a, 0x0e, + 0x4c, 0x98, 0xb2, 0x41, 0x31, 0x79, 0x2c, 0x97, 0x72, 0xab, 0x55, 0x37, 0x73, 0xa2, 0x98, 0x1e, 0xd9, 0xc3, 0xd0, + 0xc2, 0x4d, 0xba, 0x21, 0x46, 0x7f, 0xe1, 0x85, 0x7e, 0xb4, 0x1a, 0x04, 0x43, 0xb4, 0xc2, 0xce, 0xda, 0x28, 0x67, + 0x8c, 0xa2, 0xf8, 0xfb, 0x02, 0x10, 0x6c, 0xeb, 0xfa, 0x96, 0xae, 0x3e, 0x79, 0x6b, 0x77, 0xab, 0x4a, 0xcf, 0x83, + 0x12, 0x23, 0x7e, 0xcd, 0x2a, 0xe7, 0x9d, 0xea, 0x40, 0xe2, 0x87, 0x50, 0x69, 0x01, 0x57, 0x84, 0xb0, 0x4a, 0xe3, + 0x60, 0x02, 0x9c, 0xce, 0x45, 0x53, 0xdf, 0x45, 0x03, 0x48, 0x28, 0x93, 0xf8, 0xe4, 0x3c, 0x9b, 0x84, 0x5a, 0x1e, + 0x1d, 0xd2, 0x7b, 0xb7, 0x0e, 0x42, 0xe1, 0x3b, 0x53, 0xad, 0x17, 0xdc, 0x3d, 0xa5, 0xfd, 0x7a, 0xed, 0x0b, 0x2b, + 0x95, 0xc6, 0xfd, 0x77, 0xd3, 0xc7, 0xb7, 0xdf, 0xf1, 0xe2, 0xa8, 0xef, 0x26, 0xce, 0x86, 0xe5, 0x5b, 0x1e, 0x80, + 0x37, 0x0b, 0x0e, 0x08, 0xf0, 0x11, 0xf5, 0x54, 0xa7, 0xfd, 0x1e, 0xba, 0xf1, 0x75, 0x66, 0xf6, 0x2c, 0xe9, 0xfc, + 0x9d, 0x1f, 0x7c, 0xd8, 0xb6, 0x20, 0xd0, 0x05, 0xe3, 0xff, 0xa3, 0xa5, 0x02, 0x02, 0x50, 0xf0, 0xf7, 0xe1, 0x75, + 0x38, 0x45, 0xc1, 0x73, 0x18, 0xf5, 0x71, 0x44, 0x99, 0xee, 0x9d, 0x34, 0xf9, 0x5e, 0x45, 0x36, 0xcb, 0xbc, 0x42, + 0x36, 0x61, 0x6c, 0x7a, 0x59, 0xa7, 0x7c, 0x6d, 0x66, 0x60, 0xac, 0xbe, 0x04, 0xa8, 0x8c, 0x44, 0x6f, 0x4a, 0xbf, + 0x84, 0x5f, 0x5f, 0x8a, 0xc5, 0x90, 0x07, 0xdf, 0x69, 0xf5, 0xda, 0xad, 0x8f, 0x8d, 0xdf, 0xae, 0xdc, 0x83, 0xa1, + 0x0f, 0x42, 0xee, 0xe7, 0x0d, 0x59, 0x19, 0x47, 0x9b, 0xe7, 0x05, 0x97, 0xc6, 0xcb, 0x28, 0x97, 0x86, 0x8e, 0x24, + 0x6a, 0x03, 0x7d, 0x5a, 0x5a, 0x72, 0xc0, 0x65, 0x48, 0x8c, 0xfd, 0x20, 0x2b, 0x3d, 0x3e, 0x92, 0xf6, 0xc1, 0xe4, + 0x18, 0x3e, 0x9f, 0x6e, 0x71, 0x11, 0xef, 0x44, 0x60, 0xc7, 0x40, 0x95, 0x1b, 0xae, 0xda, 0xdb, 0xbd, 0xbd, 0xfd, + 0xc3, 0xf6, 0xe1, 0x66, 0xfd, 0x75, 0x85, 0x0e, 0xa9, 0xc6, 0x38, 0x9d, 0x5a, 0xab, 0xb5, 0x9c, 0xb4, 0x85, 0xbf, + 0xb7, 0x2c, 0xda, 0x24, 0xa4, 0x48, 0x0c, 0x98, 0x5b, 0x46, 0x26, 0x55, 0x2b, 0x0f, 0x30, 0x91, 0x9a, 0xba, 0x4d, + 0x4f, 0xf7, 0x99, 0x92, 0xa5, 0x06, 0xbd, 0xd8, 0xe9, 0xaa, 0x10, 0xeb, 0xa5, 0xeb, 0xc7, 0x8b, 0xa5, 0xd7, 0xba, + 0x2e, 0xb0, 0x89, 0x6c, 0x18, 0x48, 0x1d, 0x7f, 0xc7, 0x46, 0xee, 0xd7, 0xc3, 0x93, 0x25, 0x80, 0xc2, 0x25, 0xd2, + 0x75, 0x09, 0x72, 0xb4, 0x29, 0x49, 0x48, 0x2e, 0x5e, 0xa1, 0x8a, 0xf1, 0xa4, 0x66, 0x7b, 0xf3, 0x6c, 0x21, 0x12, + 0x19, 0x4a, 0x19, 0x1b, 0xbb, 0x9b, 0x74, 0xef, 0x02, 0x1c, 0xd4, 0xa2, 0x2e, 0xd7, 0x17, 0x55, 0x80, 0xed, 0x9c, + 0xbf, 0x1a, 0x8d, 0xf3, 0xa8, 0x89, 0x6e, 0xd7, 0xb0, 0x2f, 0xbb, 0xe6, 0x4c, 0x6e, 0x2e, 0x9d, 0xe6, 0xf9, 0x91, + 0xcf, 0x16, 0xab, 0x67, 0x18, 0x5c, 0xee, 0x3a, 0x01, 0x03, 0x54, 0xee, 0x95, 0x01, 0x7c, 0xcb, 0x02, 0xeb, 0x06, + 0x73, 0x49, 0x64, 0x93, 0x44, 0x5b, 0xbb, 0xa7, 0x9c, 0x84, 0x26, 0xb7, 0xee, 0x59, 0xe2, 0xca, 0x0f, 0x82, 0xaa, + 0x6c, 0xf3, 0xb4, 0x5e, 0x34, 0xf7, 0x68, 0xe9, 0x7f, 0x7a, 0x58, 0x04, 0x45, 0x81, 0xe6, 0xe1, 0x2d, 0x52, 0x73, + 0x98, 0x05, 0x51, 0x63, 0x27, 0xbc, 0xa1, 0x7d, 0x60, 0xad, 0x6d, 0xd4, 0x8e, 0x54, 0xef, 0x6f, 0x90, 0x12, 0xd6, + 0xec, 0x92, 0x14, 0x2c, 0x2b, 0xe2, 0x72, 0xd0, 0x8e, 0x08, 0xf0, 0x58, 0xd9, 0x0a, 0x1e, 0xe5, 0xc5, 0xdd, 0x6c, + 0xec, 0x0b, 0x64, 0xac, 0xc9, 0x1c, 0x74, 0x0d, 0xbf, 0x45, 0xa8, 0xd6, 0x56, 0xb7, 0x83, 0xb5, 0x7b, 0xc3, 0x34, + 0xd1, 0x3a, 0x09, 0x76, 0x44, 0x49, 0xfb, 0x05, 0x07, 0x6e, 0xaa, 0xca, 0x8e, 0xdc, 0x5b, 0x89, 0x34, 0x68, 0x57, + 0xe8, 0xfc, 0x75, 0x37, 0x35, 0x02, 0xde, 0x4c, 0xa7, 0xe4, 0x28, 0xf1, 0x89, 0x94, 0x41, 0x41, 0x49, 0x72, 0xfe, + 0x9f, 0xf5, 0xb1, 0x03, 0x05, 0xf1, 0x8d, 0x9f, 0x7f, 0x17, 0x04, 0x38, 0xb0, 0xdb, 0x41, 0xd6, 0xbe, 0x1c, 0x4b, + 0x60, 0x51, 0x85, 0x39, 0xd7, 0x83, 0x5a, 0xff, 0x9e, 0x17, 0xe1, 0xf9, 0xaf, 0x17, 0x5b, 0xaa, 0x75, 0xdb, 0x5e, + 0xf7, 0x16, 0xc9, 0x35, 0x63, 0x3b, 0xec, 0xcb, 0xc1, 0x87, 0xd3, 0x4c, 0xb2, 0x05, 0x24, 0x0d, 0x99, 0xbe, 0x94, + 0x36, 0xe9, 0x86, 0x03, 0x72, 0x07, 0x64, 0x70, 0x10, 0x68, 0x32, 0x28, 0x6b, 0x78, 0xac, 0xe6, 0xe1, 0xbc, 0xbd, + 0x7a, 0xf2, 0xd7, 0x2a, 0x5f, 0xa2, 0x43, 0xea, 0x9d, 0xc5, 0x80, 0xff, 0x7e, 0x2b, 0x18, 0xc9, 0xf6, 0xcd, 0x7e, + 0x77, 0xd3, 0x94, 0xe2, 0x0a, 0xa6, 0xfd, 0x83, 0xff, 0x3f, 0xf4, 0x16, 0x5e, 0xef, 0x64, 0x68, 0xaa, 0xc3, 0x94, + 0x1b, 0xd6, 0x8b, 0x0b, 0xf9, 0xae, 0x4c, 0x8c, 0x11, 0x04, 0x46, 0x60, 0x56, 0x97, 0xe8, 0x1e, 0x86, 0x3b, 0xeb, + 0x51, 0xcd, 0x70, 0x72, 0x69, 0x33, 0x86, 0x55, 0x0b, 0x11, 0x01, 0x2e, 0x51, 0xa0, 0x44, 0x91, 0x20, 0x89, 0x01, + 0xa2, 0x7b, 0xeb, 0xf3, 0x08, 0x65, 0x51, 0xb3, 0xbe, 0xa1, 0xb6, 0xb3, 0xb2, 0x39, 0x09, 0x68, 0x6d, 0xe6, 0x98, + 0x56, 0xa3, 0x00, 0x9d, 0xbb, 0xd3, 0x00, 0x3a, 0xf4, 0x16, 0xe9, 0xa5, 0x8c, 0x15, 0xfb, 0xae, 0x67, 0x6d, 0xe9, + 0x90, 0x4f, 0xa2, 0xd6, 0xea, 0x20, 0xad, 0x55, 0x4e, 0x45, 0x66, 0x42, 0x5f, 0xe8, 0xd2, 0xc2, 0x19, 0xe8, 0x1b, + 0x6f, 0x0f, 0xd6, 0x78, 0x4a, 0x6f, 0xf2, 0xa5, 0x29, 0xe5, 0x65, 0x8f, 0x09, 0xf7, 0x3b, 0xa9, 0x8c, 0xed, 0xad, + 0x01, 0x91, 0x4b, 0xfa, 0xbb, 0x87, 0x84, 0x66, 0x1e, 0xbd, 0x0d, 0x38, 0xec, 0x82, 0x56, 0xfc, 0xaa, 0x7a, 0xbc, + 0x63, 0x82, 0x87, 0xa5, 0x34, 0xf9, 0xfe, 0xc5, 0x9b, 0x61, 0xd6, 0x30, 0x5e, 0x58, 0xec, 0x82, 0x80, 0x82, 0xd9, + 0x5b, 0xcc, 0xdd, 0xff, 0xe5, 0x8f, 0xd6, 0xc0, 0x8d, 0x99, 0x43, 0x6e, 0x3e, 0xe0, 0xf1, 0x3d, 0xbd, 0x4f, 0xbd, + 0x9b, 0xd5, 0xab, 0x4f, 0xa7, 0xc5, 0x85, 0x91, 0xf7, 0xed, 0x74, 0xb4, 0x47, 0x24, 0x5c, 0x03, 0x30, 0x01, 0x50, + 0x96, 0x78, 0x40, 0x09, 0x8b, 0xf7, 0xe5, 0xd2, 0x2a, 0x3b, 0x01, 0x4d, 0xb5, 0x67, 0x9b, 0x3a, 0x72, 0xe1, 0x19, + 0xdb, 0x51, 0x2c, 0x6d, 0xa7, 0x29, 0x61, 0xf2, 0x5a, 0xd7, 0xee, 0xf4, 0xf2, 0xa3, 0x34, 0x81, 0x9a, 0xa9, 0x5c, + 0x29, 0xbf, 0x46, 0xd6, 0x10, 0x7c, 0x0a, 0x8b, 0x28, 0x2a, 0xc0, 0xb3, 0xe8, 0x04, 0xaa, 0xd6, 0x0f, 0xed, 0x77, + 0x77, 0x58, 0x6c, 0x5d, 0x4c, 0x8f, 0x1f, 0x2a, 0x90, 0x79, 0xe6, 0xb8, 0x73, 0xa6, 0xd9, 0xd1, 0x4d, 0xe3, 0x5d, + 0x4c, 0xd9, 0x4f, 0x5f, 0xa0, 0x4f, 0x16, 0x66, 0x76, 0x2f, 0x68, 0x2c, 0x83, 0x27, 0x45, 0x36, 0x48, 0x91, 0xef, + 0xc2, 0x10, 0xc6, 0x48, 0xa5, 0x33, 0x35, 0x8f, 0xd1, 0xf4, 0xb7, 0xd0, 0x16, 0x4c, 0xed, 0xde, 0x53, 0x7d, 0xe8, + 0x7a, 0xa3, 0x54, 0x6b, 0xdf, 0x49, 0x99, 0x49, 0x2f, 0x61, 0xa4, 0x68, 0xb7, 0xd7, 0xea, 0xa7, 0x5f, 0x2b, 0x73, + 0xa9, 0xf6, 0xd2, 0x34, 0x79, 0x11, 0xdd, 0x29, 0xc8, 0xe2, 0x70, 0x31, 0xa5, 0xb4, 0x7d, 0x52, 0xfd, 0x7b, 0xbf, + 0xb8, 0x41, 0xfc, 0x6c, 0xfc, 0x63, 0xe6, 0xf3, 0xc0, 0x97, 0xba, 0xb4, 0x01, 0x72, 0x7f, 0x72, 0x6f, 0x95, 0x18, + 0x86, 0x21, 0x05, 0x64, 0xe5, 0x6a, 0x09, 0x58, 0x14, 0xc8, 0x03, 0x15, 0x10, 0x8d, 0x38, 0xa3, 0x1d, 0x52, 0x6b, + 0xd6, 0x97, 0x25, 0x40, 0x18, 0x70, 0xed, 0x2f, 0x34, 0xce, 0x7e, 0xb1, 0xb7, 0x20, 0xa8, 0x65, 0xc3, 0x4b, 0x9e, + 0x3f, 0x02, 0x23, 0x03, 0x84, 0x9c, 0x1e, 0x89, 0x3d, 0x8b, 0xd1, 0xbc, 0xa2, 0xb3, 0xe8, 0x81, 0x8c, 0x85, 0x9a, + 0x2a, 0x6f, 0xec, 0x04, 0x98, 0xdd, 0x07, 0x97, 0x54, 0xf5, 0x18, 0x0c, 0xe0, 0x05, 0x44, 0x05, 0xac, 0x68, 0x02, + 0x9d, 0xfa, 0xd8, 0x10, 0x07, 0x6f, 0x68, 0x51, 0x80, 0x20, 0xb0, 0x37, 0x10, 0xf6, 0x27, 0xd6, 0x1f, 0x5c, 0xcd, + 0xb0, 0xcb, 0x30, 0x8d, 0xe3, 0xd0, 0xd0, 0x9e, 0x82, 0x9f, 0x0a, 0x9b, 0x68, 0xaa, 0x04, 0x28, 0x37, 0x09, 0xb1, + 0x07, 0x01, 0xff, 0xca, 0x23, 0xf2, 0xb8, 0x6e, 0x6a, 0xff, 0x09, 0xa6, 0x38, 0x2a, 0x83, 0x75, 0x9b, 0xba, 0xeb, + 0xef, 0x75, 0x19, 0xc7, 0x35, 0xa0, 0xb0, 0xa5, 0x73, 0x9c, 0x1e, 0xd3, 0x10, 0xff, 0x6b, 0xa0, 0x7f, 0xd7, 0xaa, + 0xad, 0xef, 0x42, 0x6c, 0xd6, 0x66, 0xcc, 0x07, 0x0d, 0xbb, 0x8b, 0x13, 0xe3, 0xc8, 0xe3, 0xbe, 0xc0, 0xb4, 0x6b, + 0x89, 0x8f, 0x34, 0xf4, 0xe4, 0x11, 0x94, 0x9e, 0xae, 0x76, 0x95, 0xf1, 0xab, 0xf1, 0x78, 0x7b, 0xb3, 0xf5, 0x2a, + 0x86, 0x98, 0x11, 0x05, 0x6c, 0xf5, 0x3b, 0xeb, 0xf8, 0xe4, 0x60, 0x39, 0x8e, 0xb9, 0xf5, 0x12, 0x35, 0xae, 0x2f, + 0xb2, 0x14, 0x8b, 0x54, 0xfb, 0x72, 0xf7, 0x35, 0x1f, 0x4c, 0xaf, 0x7c, 0xfc, 0xfb, 0xf3, 0x50, 0x08, 0x2e, 0xa8, + 0x12, 0x23, 0xd1, 0x40, 0x77, 0x6e, 0x5b, 0x41, 0x0b, 0xbf, 0x95, 0x94, 0x56, 0x3c, 0x0f, 0x56, 0xa3, 0x5d, 0x02, + 0x42, 0x55, 0x03, 0x5e, 0x9f, 0xa2, 0xc9, 0x85, 0x03, 0xc7, 0x08, 0xb5, 0x68, 0x72, 0x96, 0x30, 0x9c, 0x74, 0xfb, + 0x6d, 0x7e, 0xfa, 0xeb, 0x9c, 0x0c, 0x91, 0x02, 0x90, 0xfa, 0x76, 0x4c, 0xf8, 0xf4, 0x3b, 0x5e, 0x4c, 0xfe, 0xf3, + 0x8d, 0x90, 0xbe, 0xe9, 0xc4, 0xc6, 0x43, 0x90, 0x37, 0x8a, 0x42, 0x84, 0x08, 0x76, 0x71, 0x20, 0xcc, 0x76, 0xf8, + 0x95, 0xdc, 0xc2, 0x57, 0xf4, 0x96, 0x9a, 0xa3, 0xa7, 0xd1, 0x41, 0x0b, 0x27, 0xac, 0x4d, 0x7f, 0x9e, 0x47, 0x5f, + 0x60, 0xc0, 0xe1, 0x33, 0x2b, 0xc0, 0x8d, 0x61, 0x15, 0xc0, 0x5a, 0x63, 0xee, 0x18, 0xbe, 0x96, 0xe9, 0x89, 0xb5, + 0xcc, 0x01, 0xf8, 0xb8, 0x92, 0xe3, 0x86, 0xee, 0x1c, 0x2a, 0x05, 0xf3, 0x76, 0x60, 0x8b, 0xfc, 0x9f, 0x69, 0x47, + 0x59, 0x55, 0x4c, 0x2c, 0x03, 0xe1, 0x72, 0x44, 0x42, 0xe6, 0xeb, 0xde, 0xc5, 0x20, 0x0a, 0x3e, 0x62, 0x64, 0xa7, + 0x54, 0x5c, 0xe7, 0x26, 0xbf, 0xea, 0x9f, 0x5f, 0x22, 0xf6, 0xba, 0x78, 0x5d, 0xbf, 0x7f, 0xe8, 0xef, 0xfe, 0xa4, + 0x15, 0xa0, 0x7a, 0xae, 0xec, 0xca, 0x6a, 0x26, 0x07, 0x9b, 0xc8, 0xf0, 0x73, 0xbd, 0x84, 0xca, 0xb4, 0x99, 0x00, + 0x21, 0x9c, 0xe3, 0x72, 0x72, 0x3d, 0x5a, 0x4c, 0xfc, 0x04, 0xd2, 0x18, 0x7a, 0x09, 0x4a, 0xe6, 0xfd, 0x11, 0x1e, + 0x5c, 0x0e, 0x08, 0xc4, 0xbb, 0xb8, 0x0a, 0x39, 0x5a, 0x1a, 0x24, 0x31, 0xbb, 0x9f, 0x62, 0x08, 0x25, 0x2e, 0x23, + 0x05, 0x6a, 0xd9, 0x9a, 0xb2, 0x6f, 0xc1, 0x72, 0x47, 0xd5, 0x61, 0x47, 0x98, 0x29, 0x4c, 0x95, 0xc8, 0x7f, 0x78, + 0x8c, 0xa4, 0x0a, 0x4f, 0xdd, 0xc9, 0xb3, 0x15, 0x52, 0x96, 0x93, 0x06, 0x12, 0x12, 0x78, 0x28, 0x44, 0x01, 0xfa, + 0x01, 0x5b, 0xa3, 0x8a, 0xc7, 0xff, 0x61, 0x5b, 0x02, 0xdd, 0x12, 0x9f, 0x58, 0x76, 0xbc, 0x61, 0x68, 0x0e, 0x79, + 0x8c, 0x44, 0x11, 0xb4, 0xc2, 0xcf, 0xaa, 0xe4, 0x07, 0x81, 0x12, 0x50, 0xc6, 0x45, 0x76, 0x14, 0xa8, 0x4a, 0x4c, + 0x70, 0x35, 0xd0, 0x83, 0xe8, 0xde, 0x65, 0xa0, 0x69, 0x3a, 0x78, 0xed, 0xd0, 0x30, 0x96, 0xc6, 0x54, 0x07, 0xdb, + 0x51, 0x21, 0x38, 0xd2, 0xe9, 0x90, 0x51, 0x70, 0x72, 0xfb, 0x0e, 0x97, 0x0d, 0x39, 0xdd, 0xee, 0x5a, 0xa1, 0xe8, + 0x19, 0xc8, 0xea, 0x5c, 0x6c, 0x9e, 0x67, 0x63, 0x22, 0x40, 0xfa, 0xc4, 0x3c, 0x54, 0x9c, 0x97, 0x19, 0x98, 0x36, + 0x79, 0xbc, 0x2d, 0x13, 0xc5, 0x1c, 0x4b, 0xae, 0x86, 0x7d, 0xc4, 0xd3, 0x7c, 0x3d, 0x93, 0xb2, 0xdf, 0x85, 0x01, + 0x47, 0x62, 0x98, 0xf5, 0x3b, 0xed, 0xf3, 0xda, 0x68, 0x73, 0x09, 0xf5, 0xa6, 0xc2, 0x24, 0xd1, 0xec, 0x58, 0x43, + 0xbd, 0x0a, 0x2d, 0x7e, 0x32, 0xb0, 0x7e, 0x0d, 0xa9, 0x37, 0x52, 0x33, 0xec, 0x8a, 0xe7, 0x23, 0x8f, 0x1f, 0xdd, + 0xa6, 0x56, 0xd6, 0x95, 0xd5, 0xcc, 0x36, 0x95, 0x18, 0xdf, 0x0f, 0xbb, 0xad, 0x6a, 0xcf, 0xb4, 0xca, 0xc7, 0xc3, + 0x97, 0x94, 0x4e, 0x07, 0xa2, 0xa9, 0x30, 0xb0, 0x87, 0x50, 0xc7, 0x02, 0xad, 0x8d, 0xc5, 0x2e, 0xca, 0xa3, 0x32, + 0xa5, 0xad, 0xd2, 0x18, 0xc6, 0x50, 0x1b, 0xc0, 0xd5, 0xed, 0x7a, 0x90, 0x96, 0x51, 0xd6, 0x5d, 0x4a, 0x0b, 0xc5, + 0x74, 0x0c, 0x6b, 0x85, 0x33, 0x25, 0xc3, 0x4d, 0x21, 0x4e, 0x03, 0x7c, 0x79, 0xe1, 0xff, 0xfe, 0x00, 0x56, 0xcd, + 0xed, 0x8e, 0x64, 0x1b, 0x97, 0x1d, 0x5d, 0x69, 0x85, 0xe7, 0xe9, 0xbc, 0x7c, 0x91, 0xb2, 0x2d, 0xd5, 0xa2, 0x61, + 0x1a, 0x1d, 0x65, 0x0c, 0xb5, 0x7d, 0xbb, 0x98, 0x31, 0x9c, 0x61, 0xc4, 0x5c, 0xf9, 0x06, 0x67, 0xbd, 0x96, 0xbc, + 0xfb, 0x2d, 0x23, 0xa7, 0x52, 0x5c, 0xf1, 0xa2, 0x4a, 0x0d, 0xaf, 0x7c, 0xf2, 0x1f, 0xe4, 0x6d, 0x52, 0xfc, 0x6a, + 0xd5, 0x18, 0x4a, 0x92, 0xcb, 0x89, 0xce, 0x9b, 0xd7, 0x70, 0xc2, 0xdb, 0x9e, 0xa6, 0x62, 0x86, 0xe2, 0xb1, 0x04, + 0xbf, 0xab, 0x3a, 0xdb, 0xcd, 0x1f, 0x7c, 0x2e, 0xc8, 0x5a, 0x4c, 0x96, 0xe5, 0xab, 0x0a, 0xce, 0xa4, 0x1e, 0x3f, + 0x7b, 0xb8, 0x53, 0x12, 0xa4, 0xba, 0x6d, 0xc8, 0xa7, 0x41, 0xa4, 0xb7, 0xcd, 0xea, 0x28, 0x43, 0x5e, 0x99, 0xd8, + 0x84, 0xa9, 0x53, 0xc7, 0x1b, 0x77, 0x5b, 0x2a, 0x26, 0x3b, 0x13, 0xe7, 0xe1, 0xff, 0xcc, 0x16, 0xbe, 0x4d, 0x3d, + 0xf9, 0x2b, 0xb6, 0x74, 0x90, 0xfc, 0x0a, 0xc4, 0x87, 0x63, 0x04, 0xf3, 0x39, 0x7d, 0x87, 0xc2, 0xa3, 0x8e, 0x65, + 0x60, 0x60, 0x62, 0xe5, 0xd9, 0x77, 0xfc, 0xdb, 0xf1, 0x96, 0x58, 0xa3, 0xb2, 0xca, 0x50, 0x0c, 0xc1, 0x20, 0xcd, + 0xeb, 0x00, 0x40, 0xae, 0x6c, 0x2a, 0xb6, 0x05, 0x22, 0x5b, 0x5e, 0x44, 0x8b, 0x77, 0xda, 0xb9, 0x11, 0xdc, 0x94, + 0xf8, 0x94, 0xbd, 0x3d, 0x65, 0x0c, 0x70, 0x0b, 0xec, 0x74, 0xec, 0xe0, 0x81, 0x98, 0x23, 0xa1, 0x76, 0x45, 0x16, + 0x4b, 0x52, 0x87, 0x8a, 0x45, 0xb3, 0xbe, 0x50, 0x62, 0x22, 0x86, 0x6c, 0x4d, 0x9d, 0x60, 0x45, 0xea, 0xa8, 0x3d, + 0x07, 0x16, 0x25, 0xcd, 0x3e, 0x43, 0x5e, 0x4c, 0x72, 0xc7, 0x44, 0x34, 0xe3, 0xc1, 0xcf, 0x42, 0x49, 0xcf, 0xbd, + 0x89, 0x85, 0xfc, 0xdd, 0x66, 0xf9, 0x0c, 0x7b, 0x87, 0x3f, 0x49, 0x15, 0xbe, 0x9c, 0xc2, 0x6a, 0x92, 0xd0, 0x56, + 0x2e, 0xbc, 0x5d, 0x12, 0xa0, 0x40, 0x59, 0xda, 0xa7, 0xc1, 0x81, 0x42, 0x1f, 0x0a, 0xca, 0x16, 0xcb, 0x94, 0x12, + 0x33, 0xe3, 0x22, 0xa6, 0xe4, 0x5e, 0xf4, 0x79, 0x3c, 0x5f, 0xd3, 0x77, 0x40, 0xa0, 0x72, 0xb3, 0xdf, 0x6c, 0x4c, + 0x72, 0xc0, 0xd0, 0x4c, 0x7f, 0xc2, 0x27, 0xb4, 0x7b, 0xbd, 0x64, 0x3f, 0x72, 0xe0, 0xfb, 0xc0, 0x71, 0x30, 0x7b, + 0xf2, 0x43, 0xca, 0x59, 0xab, 0xea, 0x3e, 0x0b, 0xf8, 0xfb, 0xe2, 0x05, 0xe2, 0xca, 0x24, 0x04, 0xba, 0x8b, 0x49, + 0x82, 0xd5, 0xa7, 0x60, 0x48, 0x3a, 0x01, 0x5d, 0xac, 0xb0, 0xb9, 0xd6, 0x6c, 0x39, 0x41, 0x17, 0x53, 0x59, 0xc1, + 0x9d, 0x3a, 0x94, 0xea, 0xe5, 0x61, 0x66, 0x3d, 0xac, 0xa6, 0xa7, 0x29, 0x48, 0x22, 0x9d, 0xec, 0xf6, 0x53, 0x92, + 0xbd, 0x26, 0x61, 0x64, 0xdf, 0x37, 0x33, 0x22, 0x00, 0xbe, 0xe8, 0x15, 0x22, 0xf6, 0xbd, 0x48, 0x39, 0x49, 0xa5, + 0x6b, 0xce, 0xe4, 0xb6, 0x42, 0x83, 0x58, 0x17, 0xfe, 0x55, 0x50, 0x37, 0xa5, 0xf9, 0x14, 0xdc, 0xa9, 0xbe, 0x81, + 0x5d, 0x02, 0xaf, 0xcd, 0xbb, 0x10, 0x34, 0x8d, 0x0d, 0xbe, 0x04, 0xb0, 0xb8, 0x0b, 0x03, 0x4f, 0xe0, 0x17, 0x5e, + 0x07, 0x70, 0xb3, 0x59, 0xa1, 0x56, 0x31, 0xd1, 0x9b, 0xf9, 0xa3, 0x5e, 0xd9, 0x78, 0xde, 0x9d, 0x04, 0x0b, 0xcb, + 0x49, 0x90, 0x7d, 0x86, 0x01, 0x2d, 0x5d, 0xbf, 0xe3, 0x62, 0x55, 0x0a, 0x2a, 0x21, 0xa4, 0xf4, 0xdd, 0xbf, 0x99, + 0xef, 0xe9, 0xb4, 0x5e, 0x8c, 0xf4, 0xcc, 0x00, 0x82, 0x5b, 0x92, 0x79, 0xd7, 0xd1, 0xb7, 0xa6, 0x67, 0x11, 0xf7, + 0x24, 0x2f, 0xbb, 0xcb, 0xae, 0x90, 0xd5, 0x22, 0xa6, 0xd4, 0xad, 0x9c, 0x5e, 0xc3, 0xbd, 0xcd, 0x3b, 0x09, 0xdc, + 0xcc, 0xe2, 0x96, 0x47, 0x09, 0x01, 0x57, 0x8e, 0xad, 0xa5, 0xd0, 0x30, 0xe2, 0xf5, 0x20, 0x83, 0x48, 0x90, 0xfe, + 0xed, 0x22, 0x43, 0xe9, 0x29, 0x9f, 0x8f, 0x6d, 0x24, 0xd4, 0xc3, 0x4d, 0xed, 0x08, 0x0e, 0xef, 0xde, 0x5c, 0x7d, + 0xc4, 0x1f, 0xa5, 0xd7, 0xf1, 0xa1, 0x37, 0x4e, 0xcb, 0xe5, 0x35, 0x36, 0x12, 0xc0, 0xed, 0xe3, 0xf6, 0x72, 0xe1, + 0x16, 0x0d, 0xcf, 0x6d, 0x35, 0xde, 0xed, 0xe8, 0x6f, 0x5f, 0xc0, 0xcd, 0xe7, 0xdb, 0x75, 0xe7, 0x7e, 0xf3, 0x33, + 0xe5, 0xe2, 0xa5, 0x8b, 0x8c, 0xe8, 0x82, 0xf1, 0xf2, 0x6a, 0x85, 0x14, 0x20, 0xcd, 0x0f, 0x60, 0xf7, 0xf1, 0xed, + 0x91, 0xee, 0x53, 0xd9, 0x2b, 0x24, 0x7d, 0xde, 0x2e, 0x15, 0x56, 0x22, 0x8e, 0x4f, 0x36, 0x8d, 0x2c, 0xe8, 0xb3, + 0x10, 0x5d, 0xaa, 0x9f, 0x92, 0x7c, 0x5e, 0xce, 0x0d, 0x3f, 0xfc, 0x74, 0x02, 0xba, 0x09, 0xcf, 0x06, 0x11, 0x94, + 0x45, 0x4e, 0x7b, 0x4a, 0x69, 0xdf, 0xc9, 0x3f, 0xa5, 0x28, 0xbc, 0x65, 0xa3, 0xfd, 0xd2, 0xaa, 0x9b, 0xfe, 0xac, + 0xba, 0x52, 0xbc, 0x7b, 0x78, 0xb5, 0xd9, 0x5d, 0xa6, 0xa1, 0x3c, 0x73, 0x73, 0xef, 0xab, 0x05, 0xfa, 0x15, 0xc9, + 0xc7, 0xc3, 0x00, 0x11, 0x57, 0xbb, 0xcb, 0x3c, 0x55, 0xbb, 0x67, 0x4d, 0xfb, 0x22, 0x6d, 0x0c, 0x57, 0x8e, 0x3d, + 0xbe, 0x7c, 0x12, 0x27, 0x17, 0xc7, 0xba, 0x39, 0x89, 0x54, 0x94, 0x8f, 0xf5, 0x57, 0x01, 0x86, 0x33, 0x6d, 0xce, + 0x40, 0xb2, 0xaa, 0xcb, 0x53, 0xa0, 0x1e, 0x99, 0x84, 0x27, 0x67, 0xf6, 0xcd, 0x6c, 0x00, 0xd8, 0x0c, 0x98, 0x86, + 0xd6, 0xbe, 0x9b, 0x27, 0xb3, 0xa8, 0x1a, 0x00, 0x47, 0xc9, 0x2f, 0x4e, 0x3d, 0x91, 0x65, 0x57, 0x58, 0xb3, 0xf1, + 0x7a, 0xe9, 0xee, 0xd7, 0x29, 0x49, 0x21, 0x3b, 0x67, 0x47, 0x91, 0x09, 0xf3, 0x71, 0x7c, 0xd5, 0xe8, 0x65, 0x69, + 0xfa, 0x86, 0x61, 0x00, 0x8b, 0x30, 0xcd, 0xdb, 0xdd, 0x76, 0xab, 0x3e, 0xad, 0x02, 0x42, 0xed, 0x9d, 0x73, 0x2b, + 0xed, 0x3f, 0x9c, 0x54, 0x34, 0x9c, 0xcd, 0x4b, 0x21, 0xd9, 0x57, 0x68, 0x50, 0x90, 0xd5, 0x98, 0x91, 0x8e, 0xf5, + 0x29, 0x09, 0x4c, 0x9b, 0x49, 0xfa, 0x76, 0x1b, 0xd4, 0x05, 0xa8, 0x4c, 0xf9, 0x72, 0x5d, 0x58, 0x53, 0x53, 0x6f, + 0x4c, 0xf1, 0xe5, 0xde, 0xbe, 0x40, 0xd3, 0xcc, 0xd0, 0x5e, 0xce, 0x6d, 0x28, 0x65, 0xbd, 0xec, 0x2a, 0xc2, 0x83, + 0x6c, 0xa5, 0xf3, 0xf8, 0x2e, 0xc9, 0xdf, 0xe4, 0x03, 0x6a, 0x2b, 0x16, 0x97, 0x7b, 0xf5, 0x22, 0x6e, 0x37, 0x19, + 0x9a, 0x11, 0x1a, 0x56, 0x53, 0xb0, 0xdc, 0xbd, 0xf9, 0x4c, 0xef, 0x66, 0x73, 0xf5, 0x39, 0xbb, 0xf8, 0xec, 0x60, + 0x1b, 0x24, 0x90, 0x7a, 0xc4, 0xca, 0x9a, 0xec, 0x21, 0x25, 0x86, 0x89, 0x69, 0xca, 0x9e, 0x00, 0x19, 0xc0, 0x1f, + 0x93, 0xf8, 0x7f, 0xfc, 0xfd, 0xef, 0xc1, 0x1d, 0xda, 0xef, 0xce, 0x17, 0x23, 0xef, 0xf9, 0x87, 0xd3, 0x03, 0xa7, + 0x9f, 0xdb, 0xfb, 0x38, 0xb7, 0x47, 0x44, 0x8d, 0x2a, 0x2e, 0x2a, 0x5a, 0xf1, 0xe4, 0x50, 0x55, 0x5a, 0x87, 0xf9, + 0x4e, 0xdc, 0x29, 0x15, 0xae, 0xdc, 0xcb, 0xe0, 0x7e, 0xbf, 0x1f, 0xae, 0xff, 0x5f, 0x9d, 0x2d, 0x59, 0x7f, 0xff, + 0x6f, 0x6b, 0xfa, 0x7f, 0xe9, 0x4d, 0x58, 0x1a, 0xee, 0x7f, 0x6b, 0x70, 0xe9, 0xb7, 0x67, 0x5a, 0x5f, 0xbb, 0xf6, + 0x6f, 0x1d, 0x20, 0x28, 0x64, 0x3f, 0xd9, 0xb3, 0x76, 0xe9, 0xa9, 0xcb, 0x2c, 0x06, 0xca, 0xc1, 0xff, 0x9f, 0x65, + 0x77, 0xec, 0xd9, 0x09, 0x53, 0x1b, 0x1f, 0xdf, 0xcf, 0x30, 0x0e, 0xb8, 0x55, 0x22, 0x8c, 0x71, 0xc8, 0xeb, 0xca, + 0xef, 0x6a, 0xe4, 0x73, 0x48, 0x27, 0xd6, 0x2a, 0xa0, 0x5f, 0xd6, 0x2f, 0x0a, 0xe2, 0xbe, 0x87, 0x3b, 0x13, 0xb1, + 0x24, 0x78, 0xa0, 0x6e, 0x9c, 0x0a, 0xca, 0x8f, 0xa4, 0x69, 0x7a, 0x8e, 0x92, 0x5f, 0xda, 0xff, 0x31, 0x5b, 0xc3, + 0xaa, 0xf7, 0x17, 0xc4, 0x8b, 0x93, 0xdb, 0x7f, 0x61, 0x21, 0xed, 0x1b, 0x92, 0x18, 0x1b, 0x53, 0xb7, 0x6e, 0x9c, + 0x3a, 0x9d, 0xde, 0xb3, 0xad, 0xea, 0x0c, 0xc2, 0x1f, 0x55, 0x29, 0x4c, 0xde, 0xae, 0x05, 0x51, 0x4d, 0xef, 0xb3, + 0x77, 0x75, 0x34, 0xa0, 0x96, 0x92, 0x67, 0x7e, 0x9b, 0xc1, 0xb3, 0x2b, 0x7c, 0xbf, 0x1a, 0xeb, 0xa7, 0xe0, 0x84, + 0x34, 0x72, 0x99, 0xb2, 0x7e, 0x04, 0x6b, 0xed, 0xe6, 0x83, 0x17, 0x38, 0x89, 0xce, 0xd9, 0x2a, 0xe7, 0x24, 0xaa, + 0xc6, 0xfb, 0x82, 0xf0, 0x3f, 0x67, 0x2c, 0x7c, 0x86, 0x86, 0x0b, 0xb1, 0x9c, 0x80, 0x6a, 0x4c, 0xe1, 0x98, 0x79, + 0xc7, 0xf5, 0x73, 0x7b, 0x6d, 0xbf, 0xf2, 0x8b, 0x21, 0xd2, 0x6c, 0x0c, 0xde, 0xaa, 0x7e, 0xc1, 0x50, 0xb2, 0x1f, + 0x0f, 0x7b, 0x70, 0xe8, 0xa5, 0xe9, 0x45, 0xd6, 0xfe, 0x29, 0x7c, 0x91, 0xaf, 0x7c, 0x48, 0x2d, 0xcd, 0x6b, 0xa5, + 0x18, 0x2f, 0x6e, 0xd8, 0xc5, 0xbf, 0x83, 0xf4, 0xc6, 0xec, 0xb0, 0xdb, 0xb8, 0x81, 0x22, 0x91, 0xc6, 0x1a, 0x32, + 0xf6, 0x3f, 0xad, 0x93, 0x1c, 0x26, 0x2c, 0x31, 0x08, 0xeb, 0x27, 0xb1, 0x79, 0xd5, 0xe7, 0x4e, 0xb2, 0x6f, 0x92, + 0x66, 0x57, 0xa1, 0x69, 0x00, 0x08, 0xcf, 0x1e, 0x91, 0xbb, 0xab, 0x8f, 0x96, 0x6c, 0x7b, 0xc9, 0xe5, 0x6f, 0xc3, + 0xc8, 0xd9, 0x87, 0x4d, 0x5b, 0x1b, 0x9c, 0xda, 0x9c, 0xc4, 0xa6, 0x8d, 0x55, 0xf8, 0xdc, 0x74, 0xc2, 0x7d, 0x7f, + 0xed, 0x59, 0x5c, 0x33, 0x2b, 0x89, 0xe2, 0xda, 0x0a, 0x71, 0x53, 0xf0, 0x03, 0x0c, 0x24, 0xcc, 0x18, 0x73, 0xb6, + 0x51, 0x20, 0x20, 0x49, 0x99, 0xb2, 0x6a, 0x43, 0x7c, 0xf9, 0x41, 0x0c, 0x70, 0x33, 0x13, 0x36, 0x01, 0xb5, 0xfe, + 0xc8, 0xca, 0x0d, 0x27, 0x4b, 0x42, 0xc8, 0xb8, 0xdb, 0x27, 0xbf, 0x60, 0x60, 0xc6, 0x8f, 0x18, 0xa5, 0xc6, 0x77, + 0xeb, 0xfd, 0x63, 0x26, 0x7f, 0xba, 0xfe, 0x93, 0x6d, 0xe3, 0xb7, 0xe1, 0x42, 0x19, 0xb6, 0xe6, 0x33, 0xb4, 0xac, + 0x0a, 0x0c, 0xca, 0xa8, 0xbc, 0xb3, 0x9e, 0xb9, 0xed, 0x93, 0x58, 0x55, 0x49, 0x7c, 0x43, 0xab, 0x32, 0x47, 0xf0, + 0xb8, 0x17, 0xa5, 0x34, 0x25, 0x58, 0x82, 0xdb, 0xf7, 0x2b, 0xe4, 0x2a, 0xe7, 0xe1, 0xcb, 0x13, 0x47, 0x92, 0x2b, + 0x17, 0xa5, 0x57, 0x6f, 0x38, 0xe2, 0xd4, 0xa5, 0x94, 0x9d, 0x65, 0x60, 0x4f, 0x36, 0x0f, 0xa9, 0x20, 0xa5, 0xa1, + 0x96, 0x6d, 0xdb, 0x5a, 0xf9, 0x25, 0x7a, 0x2d, 0xb5, 0xba, 0x60, 0x69, 0x29, 0xe0, 0xc6, 0x8c, 0x28, 0x8f, 0x6a, + 0xeb, 0xe6, 0xea, 0x28, 0xa5, 0x79, 0x50, 0x57, 0xc1, 0x43, 0x6d, 0x1e, 0xb9, 0xb0, 0x86, 0x5f, 0xfa, 0xf8, 0xe8, + 0x91, 0x31, 0x32, 0xed, 0x06, 0x3e, 0x9e, 0x66, 0xc3, 0x66, 0x07, 0x5f, 0xa8, 0x3a, 0x35, 0x21, 0x94, 0x2f, 0xd0, + 0x79, 0xa3, 0x4a, 0xb2, 0x1c, 0xbc, 0x42, 0xc6, 0x2d, 0x4e, 0x12, 0xf7, 0x6f, 0xc8, 0xfa, 0xa2, 0x58, 0x5a, 0xb4, + 0xa7, 0x95, 0x55, 0x41, 0x69, 0x9b, 0xd4, 0xfc, 0xd7, 0x98, 0x7e, 0xe5, 0x21, 0xa9, 0xa7, 0x35, 0xde, 0x1f, 0x72, + 0xbb, 0xe4, 0x1e, 0x77, 0xdf, 0x82, 0x33, 0xa3, 0x76, 0xbb, 0x02, 0x90, 0x76, 0x7d, 0x1c, 0x21, 0x91, 0x39, 0x11, + 0x4e, 0x29, 0xe9, 0xc1, 0x8d, 0x1c, 0xa1, 0xf9, 0xdd, 0x3e, 0xb6, 0x9a, 0x48, 0xb7, 0x70, 0x1c, 0xb1, 0xbf, 0x2c, + 0x63, 0x67, 0x70, 0x12, 0xaf, 0x5d, 0xfc, 0xda, 0x23, 0x14, 0xd9, 0x92, 0x4a, 0x7d, 0x6d, 0xc9, 0x95, 0x76, 0xf9, + 0x4e, 0xed, 0x65, 0xdc, 0xa1, 0xb0, 0x4d, 0x6f, 0x5d, 0x8a, 0xff, 0xc3, 0x29, 0xa5, 0xfa, 0x8e, 0xdf, 0xa8, 0xf4, + 0xb7, 0xdd, 0xfd, 0x5e, 0x6d, 0x05, 0xcb, 0xf9, 0xab, 0x1a, 0xd1, 0x36, 0xed, 0xda, 0x2e, 0x5a, 0xbc, 0x39, 0xd0, + 0xd6, 0xa1, 0xbe, 0x42, 0xff, 0xbc, 0x63, 0x54, 0x05, 0x3a, 0x24, 0x1d, 0xca, 0xb0, 0x99, 0x36, 0xe4, 0xc4, 0x6a, + 0x18, 0x84, 0xfd, 0xa2, 0x50, 0xfb, 0xe0, 0x7f, 0x32, 0x65, 0x45, 0x03, 0x6a, 0xcd, 0x39, 0xd3, 0x96, 0x33, 0xe0, + 0xfa, 0x64, 0xb3, 0xdb, 0xd4, 0x7a, 0xaa, 0x31, 0xce, 0x68, 0xca, 0xb0, 0xad, 0x5b, 0xb6, 0xec, 0xd6, 0xcd, 0x1c, + 0x49, 0xf1, 0x07, 0x33, 0xc3, 0x27, 0xfd, 0xe7, 0xd7, 0xba, 0x01, 0xca, 0xbb, 0x57, 0xef, 0x67, 0x72, 0xaa, 0x3a, + 0xe5, 0x4f, 0xf3, 0xf5, 0xd3, 0x5f, 0x2d, 0x79, 0xfd, 0xa3, 0xbf, 0x78, 0x89, 0xde, 0xf0, 0x17, 0x6c, 0x19, 0xe3, + 0x66, 0xbb, 0x4c, 0x7a, 0x09, 0x3a, 0x2b, 0x35, 0xfa, 0x6c, 0x83, 0xa5, 0xe0, 0x2e, 0x18, 0x09, 0xd4, 0xb4, 0x4d, + 0x59, 0x97, 0xf6, 0x7d, 0x71, 0xfd, 0x74, 0xa3, 0xad, 0x2f, 0xb6, 0xaa, 0x87, 0xb8, 0xef, 0xab, 0xd7, 0xc1, 0x7c, + 0x3e, 0xec, 0xbe, 0xfd, 0x84, 0x4d, 0xf8, 0xa7, 0x10, 0xa0, 0x0d, 0x3b, 0x3d, 0x56, 0x8d, 0x8b, 0xf7, 0xd5, 0xb0, + 0xb8, 0xae, 0xda, 0xe2, 0xac, 0x9a, 0x17, 0xe7, 0xd5, 0xf5, 0xe1, 0xdd, 0x5d, 0xbf, 0x65, 0xf8, 0x1b, 0x56, 0xd3, + 0x1b, 0xb2, 0xf6, 0x33, 0xa6, 0xa9, 0x65, 0xc2, 0xe9, 0x69, 0xb7, 0x7c, 0x84, 0xd3, 0x2e, 0xdd, 0x9d, 0xdd, 0x79, + 0xbc, 0x7d, 0x83, 0x5e, 0xa5, 0x68, 0x97, 0x05, 0x86, 0xea, 0xc4, 0x82, 0xc4, 0xbc, 0xc6, 0xb6, 0x37, 0xeb, 0x90, + 0x33, 0x18, 0xc8, 0x73, 0xc5, 0x35, 0xce, 0x5d, 0x8c, 0x99, 0xbc, 0xa1, 0x00, 0x85, 0x63, 0x49, 0x54, 0xc3, 0xaa, + 0x95, 0x15, 0x75, 0x24, 0xb1, 0x20, 0x88, 0x17, 0x4c, 0x9d, 0x54, 0xc1, 0x2e, 0xdd, 0xc8, 0xbb, 0x1a, 0xc1, 0x00, + 0xb7, 0x9d, 0x4d, 0xb9, 0xb8, 0x2f, 0x1a, 0xd9, 0x62, 0x2b, 0x55, 0x2d, 0xc2, 0x95, 0x48, 0x39, 0x2e, 0xad, 0x6f, + 0x99, 0xdb, 0xf7, 0xba, 0x5f, 0x9c, 0x97, 0xe2, 0x7f, 0xda, 0x01, 0x5e, 0x47, 0x86, 0xac, 0xec, 0x05, 0xbf, 0x52, + 0x32, 0xad, 0x13, 0xeb, 0x54, 0xd3, 0xba, 0xc6, 0x61, 0xf6, 0xf2, 0xd7, 0xf2, 0x40, 0x14, 0x23, 0xfa, 0xa2, 0x56, + 0x2a, 0x6b, 0x74, 0x98, 0xc4, 0x20, 0xd3, 0xd0, 0x94, 0x63, 0x0d, 0xad, 0x15, 0x67, 0xf1, 0x68, 0x57, 0x41, 0x62, + 0xe3, 0x5b, 0xf9, 0x35, 0x27, 0x36, 0xe8, 0x00, 0x62, 0x81, 0x8e, 0xcb, 0x3a, 0x13, 0xfe, 0x3f, 0xea, 0xa1, 0xdc, + 0x37, 0xfd, 0x9f, 0x28, 0xaf, 0x0a, 0xd1, 0x67, 0xe8, 0xdb, 0x25, 0x57, 0x70, 0x09, 0x31, 0xea, 0xc1, 0x9a, 0xa8, + 0xe6, 0xce, 0x6f, 0xd1, 0x27, 0x90, 0x02, 0x82, 0xa7, 0x33, 0x18, 0x9c, 0xa8, 0x36, 0xd2, 0xa0, 0x99, 0x11, 0xa9, + 0x18, 0x0a, 0xef, 0x47, 0x53, 0xb5, 0x6e, 0x47, 0x32, 0xb6, 0x57, 0x32, 0x6f, 0xf5, 0x6b, 0xab, 0x40, 0x61, 0x3e, + 0x5e, 0xae, 0x1a, 0x01, 0xa0, 0xe5, 0xef, 0xdb, 0x9f, 0xd4, 0xd5, 0x38, 0x7a, 0xd7, 0x6d, 0x0a, 0x47, 0xe7, 0x88, + 0x27, 0x86, 0xc5, 0x16, 0xa2, 0xd5, 0x13, 0xa8, 0xf9, 0x0e, 0x0d, 0x57, 0xed, 0x9b, 0xcc, 0x60, 0x5e, 0x4e, 0x4e, + 0x72, 0x7e, 0x87, 0xa9, 0x77, 0xbe, 0x67, 0x8a, 0x30, 0xa9, 0x09, 0xa2, 0xea, 0x3d, 0x14, 0x04, 0x0b, 0xf6, 0x42, + 0x0b, 0xf7, 0xeb, 0x51, 0x92, 0x82, 0xc9, 0x80, 0xae, 0x68, 0xed, 0x88, 0x95, 0x15, 0x53, 0x6a, 0x34, 0x12, 0x19, + 0xae, 0x72, 0xd3, 0xdf, 0xc7, 0x84, 0x4a, 0x01, 0x68, 0xb7, 0x7f, 0x21, 0x63, 0xe4, 0xe0, 0x82, 0x35, 0xd1, 0x6e, + 0x48, 0x43, 0x0b, 0xb7, 0x54, 0x16, 0x04, 0xf0, 0x82, 0x06, 0xab, 0xfd, 0x82, 0xca, 0x71, 0xe1, 0x13, 0x0b, 0x53, + 0xaf, 0x84, 0x5d, 0xf0, 0xe7, 0x86, 0xa5, 0xf5, 0xcf, 0x0f, 0x03, 0x8a, 0xf5, 0x0f, 0x61, 0xd8, 0x97, 0xcf, 0xf3, + 0x9c, 0xf8, 0xd8, 0x08, 0xc9, 0xd5, 0x56, 0x83, 0x10, 0x2f, 0x4a, 0x7a, 0x2b, 0x66, 0x16, 0xb5, 0xde, 0x1e, 0x9e, + 0xd7, 0xbe, 0x74, 0x07, 0xb1, 0xea, 0x97, 0xd8, 0xd8, 0xec, 0x6e, 0x40, 0x90, 0xfd, 0xa6, 0xa8, 0x94, 0xb1, 0xc9, + 0xf7, 0x3c, 0xc9, 0xee, 0xe5, 0xf3, 0x19, 0x81, 0x53, 0xf6, 0xd9, 0x67, 0xbe, 0x26, 0xe0, 0xcb, 0x9e, 0x1e, 0x9b, + 0x3d, 0xad, 0xb3, 0x73, 0x4e, 0x1f, 0x1e, 0xa2, 0x86, 0xda, 0x74, 0x2f, 0x0c, 0x86, 0x2b, 0x90, 0x5f, 0xb8, 0x4f, + 0x88, 0x09, 0x97, 0x9f, 0x9f, 0x46, 0x3b, 0x73, 0x27, 0xe4, 0xc1, 0xd9, 0xe1, 0x13, 0x50, 0x01, 0x33, 0x7b, 0xa7, + 0x92, 0xe6, 0x6d, 0xf5, 0x88, 0x8f, 0x5a, 0x91, 0xd8, 0x03, 0x98, 0xae, 0xbb, 0xe0, 0x3e, 0x5d, 0xef, 0x56, 0xf6, + 0x5d, 0x3c, 0x15, 0xa8, 0x7b, 0x6d, 0xab, 0x6d, 0xea, 0xaf, 0x74, 0xc7, 0xd3, 0x17, 0x85, 0x01, 0xc0, 0xec, 0x2e, + 0x41, 0x0b, 0xbe, 0x92, 0x18, 0xf6, 0xe0, 0xbd, 0x9c, 0xa4, 0xdf, 0x62, 0x07, 0x4f, 0xc6, 0xb5, 0x51, 0x0d, 0xd4, + 0xc2, 0x7c, 0x77, 0x43, 0xcd, 0xaa, 0x1a, 0x48, 0x9c, 0x23, 0xe1, 0x6c, 0xfd, 0xac, 0x3d, 0xe6, 0x8b, 0x9d, 0x2b, + 0x8e, 0xfd, 0x8f, 0x0a, 0xbf, 0xc2, 0xf6, 0x8c, 0xa5, 0x03, 0xaf, 0x0c, 0x2b, 0xe9, 0x18, 0x0c, 0xc8, 0xcf, 0x75, + 0x9c, 0x48, 0xa3, 0xf9, 0xfb, 0xe8, 0x8b, 0x04, 0x35, 0xd0, 0x6f, 0x7c, 0x1e, 0x5f, 0xba, 0xe4, 0x53, 0xad, 0x1f, + 0x08, 0xf8, 0x20, 0x03, 0x6a, 0xcf, 0xe8, 0x8c, 0x16, 0x4f, 0x73, 0xfd, 0x49, 0x7f, 0xcc, 0x25, 0xeb, 0x1f, 0xfd, + 0xd3, 0x2c, 0x4e, 0xad, 0xc5, 0x45, 0x35, 0xc1, 0x7b, 0x0a, 0xfb, 0x9e, 0x02, 0xfe, 0x2e, 0x59, 0x64, 0xc3, 0x32, + 0x9a, 0x47, 0xb1, 0xa6, 0x41, 0x94, 0xd4, 0xfa, 0xc8, 0xad, 0x4d, 0x3e, 0xf6, 0x7d, 0x0f, 0xab, 0x42, 0x5f, 0xe9, + 0xc2, 0x77, 0x55, 0x8b, 0xc5, 0x6c, 0xd5, 0x99, 0x48, 0xb9, 0x9e, 0x51, 0xa9, 0xc0, 0x11, 0x56, 0x9a, 0x23, 0xc7, + 0x34, 0xa5, 0xe1, 0xc0, 0xe1, 0x14, 0x6b, 0x52, 0x80, 0x7d, 0xfd, 0x4b, 0xdf, 0xda, 0x5a, 0x9e, 0x4f, 0xe1, 0xb6, + 0xe1, 0x2d, 0xce, 0xeb, 0x32, 0x94, 0xa4, 0x56, 0x01, 0xcb, 0xbe, 0x8a, 0x05, 0xc4, 0x45, 0xbe, 0xaa, 0x36, 0x27, + 0x8c, 0x51, 0x93, 0x0b, 0xb5, 0x87, 0xcc, 0x0d, 0xd4, 0x44, 0xa7, 0x90, 0x5e, 0x70, 0xda, 0x77, 0x93, 0xd8, 0x5a, + 0xd7, 0x32, 0xeb, 0xeb, 0xc4, 0x52, 0xa5, 0xcd, 0xb3, 0xbd, 0x23, 0x1d, 0x90, 0xcb, 0x98, 0x84, 0x20, 0x89, 0x25, + 0xa8, 0xf0, 0xd8, 0xfe, 0xaa, 0x9f, 0x8b, 0x04, 0x20, 0x81, 0xed, 0x8b, 0xf8, 0x32, 0x70, 0x94, 0xa4, 0xa2, 0x6a, + 0x6a, 0x6d, 0x06, 0x4c, 0xcc, 0x3b, 0x1d, 0x55, 0x6a, 0x51, 0x83, 0x20, 0x40, 0x64, 0xe2, 0x2c, 0x12, 0x39, 0x3d, + 0x8a, 0x1e, 0xee, 0x68, 0xa7, 0x85, 0x4c, 0xd1, 0x0a, 0x4a, 0x64, 0xed, 0x21, 0x49, 0x0f, 0x5f, 0x23, 0x14, 0x83, + 0x13, 0xe7, 0xcc, 0x05, 0xbf, 0xd7, 0x26, 0xbf, 0x9f, 0x5a, 0xe6, 0xde, 0xb5, 0xd8, 0x59, 0x7c, 0xe5, 0x51, 0xae, + 0x9e, 0x6c, 0x04, 0xdc, 0x0e, 0xe8, 0xee, 0x05, 0x05, 0xd8, 0xdb, 0x9b, 0x00, 0x03, 0xaf, 0xb4, 0xa8, 0xb5, 0x6c, + 0xe3, 0xb2, 0x5c, 0x13, 0xd6, 0x96, 0xfc, 0x9f, 0xdf, 0x4b, 0x27, 0x27, 0x9b, 0x28, 0x74, 0x34, 0xc9, 0xa9, 0x12, + 0x1d, 0x41, 0x1a, 0xc3, 0xaa, 0x17, 0x17, 0x90, 0x69, 0x4f, 0x93, 0x37, 0x6e, 0xd9, 0x12, 0x46, 0x66, 0x6f, 0x01, + 0xbb, 0xa7, 0xb7, 0x0c, 0x1c, 0xa9, 0xfa, 0xbf, 0x9f, 0xa6, 0x12, 0x3b, 0x05, 0x11, 0x84, 0x7a, 0xee, 0x58, 0xb2, + 0x0b, 0x64, 0x6c, 0xf5, 0x77, 0xcc, 0xb4, 0x69, 0xb2, 0x09, 0xe1, 0x11, 0x32, 0xe7, 0xbd, 0x72, 0x5b, 0x84, 0x18, + 0x4a, 0x0b, 0x52, 0xf0, 0xb5, 0xd3, 0x29, 0x82, 0xc3, 0x3c, 0x5d, 0x86, 0x0e, 0x1f, 0xc2, 0x19, 0x99, 0x31, 0xfe, + 0x54, 0xdc, 0x1b, 0x60, 0xde, 0x5d, 0x88, 0x1d, 0x26, 0xeb, 0x95, 0x21, 0x77, 0x44, 0x1e, 0xdf, 0x26, 0x79, 0x7a, + 0xb7, 0xcb, 0xa0, 0x4c, 0xe9, 0xf0, 0xc9, 0x24, 0xe2, 0x53, 0x71, 0xaa, 0x48, 0xb5, 0xa0, 0x6d, 0xf5, 0xed, 0xf7, + 0x65, 0xd0, 0x7b, 0xcf, 0xbe, 0xf5, 0x3e, 0x0a, 0x88, 0xae, 0x37, 0x0d, 0xdb, 0x34, 0x4f, 0x43, 0x83, 0x1c, 0xc3, + 0xfc, 0x74, 0x6b, 0x99, 0x4e, 0xd5, 0xe5, 0x2f, 0x7a, 0x6d, 0x91, 0x2f, 0x80, 0x4d, 0x3d, 0x0d, 0xaa, 0xb3, 0xda, + 0x26, 0x10, 0x21, 0x7d, 0x20, 0x66, 0x89, 0x8f, 0x62, 0xc5, 0xf8, 0xec, 0x35, 0x91, 0x0b, 0x7e, 0x96, 0x9f, 0x43, + 0xee, 0xed, 0x8d, 0x1f, 0xf9, 0xa4, 0xa0, 0xf7, 0xe3, 0x71, 0x76, 0x06, 0xf1, 0x7c, 0x9c, 0xce, 0x76, 0xaa, 0x20, + 0xa6, 0xbf, 0xfb, 0xff, 0x4c, 0x53, 0xd4, 0x1f, 0x20, 0x6c, 0x12, 0x2f, 0x0e, 0x13, 0xbc, 0x56, 0x09, 0x37, 0x09, + 0x3a, 0xa9, 0x7b, 0x28, 0x07, 0x6c, 0x22, 0x80, 0xaf, 0x3c, 0x23, 0x6e, 0x60, 0xba, 0x54, 0xf0, 0x34, 0xf2, 0x0e, + 0xc2, 0xe1, 0x4e, 0xc7, 0x93, 0x76, 0xb8, 0xaf, 0xa2, 0x8d, 0xc5, 0xe3, 0x63, 0x06, 0x91, 0x3f, 0x28, 0xfa, 0x9f, + 0x1a, 0x94, 0x46, 0x7e, 0xbe, 0x98, 0x2f, 0xcd, 0x5c, 0xad, 0xc7, 0x92, 0x36, 0x0a, 0x36, 0xab, 0x50, 0xba, 0x65, + 0xbc, 0x17, 0x17, 0xb6, 0xe8, 0x29, 0x34, 0xfb, 0xfd, 0x69, 0x52, 0x4e, 0xa5, 0xbd, 0xac, 0x5a, 0x93, 0x5e, 0x4b, + 0x6e, 0xef, 0x99, 0x4d, 0xf4, 0x13, 0x60, 0x25, 0x7e, 0x2b, 0x5a, 0xbc, 0xf4, 0x58, 0x94, 0xdf, 0xa5, 0x1a, 0x01, + 0x19, 0x82, 0xe7, 0x4f, 0x1e, 0x03, 0xbb, 0x15, 0xe9, 0xe9, 0xdf, 0x16, 0x97, 0xbe, 0x3b, 0x89, 0xd3, 0xff, 0x53, + 0xc8, 0xfe, 0xc0, 0x8f, 0x19, 0x58, 0x7f, 0xc6, 0x22, 0x55, 0x70, 0x09, 0xb7, 0xdb, 0xc4, 0xe6, 0x0b, 0xa8, 0x8a, + 0xcb, 0xed, 0xb9, 0xa3, 0x4a, 0xec, 0x27, 0x85, 0x0f, 0x3e, 0x8e, 0x4d, 0x6b, 0x11, 0xfe, 0x76, 0x17, 0x99, 0xfc, + 0xab, 0xe3, 0x12, 0x84, 0x57, 0xdd, 0xf8, 0xa0, 0xdf, 0x33, 0x5a, 0x3d, 0xcd, 0x7f, 0x9e, 0xfe, 0x9b, 0x25, 0xff, + 0xfc, 0xe8, 0x9f, 0x66, 0xe5, 0xad, 0x54, 0x3d, 0xe2, 0x01, 0x57, 0xe3, 0x25, 0xe2, 0xf1, 0xe4, 0xf5, 0xfc, 0xa3, + 0x64, 0x57, 0x75, 0x0d, 0x15, 0x5e, 0x9e, 0xc8, 0x05, 0x5a, 0x46, 0x35, 0xdb, 0x7a, 0x8e, 0x5e, 0x28, 0xd7, 0x1d, + 0xc5, 0x92, 0x44, 0x9b, 0x5e, 0x7e, 0x8b, 0xf4, 0x6a, 0x90, 0x24, 0x98, 0xed, 0xbf, 0x93, 0x35, 0x20, 0xd4, 0x1a, + 0x66, 0x56, 0xa9, 0x81, 0xc5, 0x73, 0xdb, 0x96, 0x94, 0xf3, 0x2a, 0xde, 0x1f, 0x45, 0x7e, 0xf9, 0x21, 0x0c, 0x58, + 0x0c, 0x46, 0x6f, 0x84, 0x26, 0xe0, 0x29, 0x22, 0x23, 0x47, 0x55, 0x5d, 0x48, 0xfc, 0x76, 0x47, 0x48, 0xbc, 0x95, + 0x4b, 0xa5, 0xaf, 0x04, 0x90, 0xaf, 0x65, 0xf5, 0xa9, 0xab, 0xc1, 0x5d, 0x7f, 0xd8, 0x93, 0xf4, 0x8d, 0x77, 0xe6, + 0x37, 0xea, 0xf2, 0x56, 0x69, 0xf4, 0x04, 0xcc, 0xce, 0x36, 0x4c, 0x65, 0xc4, 0x49, 0xe4, 0xd0, 0xe6, 0x62, 0x07, + 0x56, 0x99, 0x75, 0x33, 0xba, 0x82, 0x3f, 0x76, 0xe7, 0x2e, 0x24, 0x65, 0xcd, 0xb5, 0x4f, 0x32, 0xfd, 0xd0, 0x8a, + 0xe3, 0x2e, 0x81, 0xf1, 0xbe, 0xb4, 0xbc, 0x30, 0x3c, 0x45, 0x4a, 0x6d, 0x53, 0x0a, 0x1a, 0x90, 0x5f, 0xc5, 0x43, + 0x8a, 0x36, 0x41, 0x20, 0x27, 0x7b, 0xa5, 0x95, 0xea, 0x23, 0x95, 0xbb, 0xec, 0x19, 0xd3, 0xe6, 0x01, 0xa7, 0xd9, + 0x0c, 0x4a, 0x60, 0x9c, 0x4e, 0xfb, 0x64, 0x6d, 0x37, 0x9d, 0xdb, 0x6f, 0x92, 0xb2, 0xf0, 0x6b, 0x14, 0x4a, 0x6e, + 0xfe, 0x36, 0xfd, 0xfb, 0x96, 0xaf, 0x9e, 0xf9, 0x47, 0x82, 0xbd, 0xd2, 0x9f, 0xfd, 0xf5, 0xbe, 0xb2, 0x8b, 0x73, + 0xa5, 0x5b, 0x87, 0x85, 0xe5, 0xe2, 0x61, 0x7f, 0x74, 0x24, 0x80, 0x4c, 0x10, 0x2b, 0xdd, 0xb0, 0xc6, 0xf0, 0xfb, + 0x44, 0xcd, 0x3e, 0xf3, 0x8b, 0xa3, 0xa3, 0x61, 0xe5, 0x1b, 0xbb, 0x59, 0x27, 0x58, 0x0e, 0xff, 0xcf, 0xfd, 0x97, + 0xcd, 0x37, 0xbb, 0xcd, 0xe1, 0xc6, 0xc6, 0x6e, 0x9f, 0x05, 0xc6, 0x31, 0x37, 0xd7, 0x6b, 0x04, 0xc6, 0x48, 0xed, + 0xd0, 0xe4, 0x87, 0xc6, 0x99, 0xe3, 0xaa, 0x4c, 0xd9, 0x3b, 0xa2, 0x16, 0x69, 0x5c, 0xcf, 0x4a, 0x8e, 0xb4, 0xd0, + 0x2e, 0x96, 0xc5, 0xa1, 0x51, 0x24, 0x34, 0xad, 0x17, 0x1b, 0x39, 0xee, 0x87, 0xe7, 0xb3, 0x61, 0xc0, 0x53, 0xc2, + 0xda, 0x81, 0xb3, 0x11, 0x13, 0x41, 0x86, 0xdb, 0x29, 0x42, 0x37, 0xe4, 0x60, 0x80, 0x6e, 0xe8, 0x1c, 0xc1, 0x73, + 0x27, 0x67, 0xce, 0x8f, 0x0b, 0x6f, 0x98, 0x90, 0x0c, 0xa3, 0x04, 0x90, 0x63, 0xb2, 0x92, 0x6e, 0xdc, 0xdb, 0xbd, + 0x69, 0x77, 0x5e, 0x50, 0xd5, 0xc5, 0x50, 0x5b, 0xea, 0x49, 0x47, 0xea, 0xc5, 0x07, 0x12, 0xc3, 0xb6, 0xd3, 0xc9, + 0xf3, 0xca, 0xe8, 0xd5, 0x44, 0xf7, 0xfb, 0x98, 0xe6, 0xba, 0x2f, 0x9a, 0x23, 0xba, 0x02, 0x96, 0x33, 0x99, 0x5d, + 0x4b, 0xc2, 0xd9, 0xee, 0x3e, 0x9a, 0xd0, 0x73, 0x8d, 0x63, 0x51, 0x28, 0x14, 0x6c, 0x69, 0xba, 0x1b, 0xcf, 0xac, + 0xc3, 0xc5, 0x3f, 0xd4, 0xc5, 0x55, 0x06, 0x8a, 0xb3, 0xa6, 0x77, 0x22, 0x71, 0xdf, 0x46, 0x17, 0x06, 0x38, 0x41, + 0x93, 0x8b, 0x1e, 0xf6, 0x44, 0x18, 0x5a, 0x50, 0xd3, 0x5c, 0xca, 0x9f, 0x5b, 0x8f, 0x89, 0x6e, 0x30, 0x38, 0xce, + 0x95, 0x59, 0x4e, 0x4d, 0x1e, 0x0a, 0x57, 0x4a, 0xae, 0xb0, 0x9d, 0x59, 0x5c, 0x36, 0x4b, 0xa5, 0xf0, 0xfe, 0x7f, + 0x71, 0xf0, 0x4c, 0x48, 0xbb, 0x6a, 0xd4, 0xa6, 0xfa, 0x04, 0x3e, 0x03, 0x57, 0x52, 0x39, 0xd9, 0xc4, 0x1f, 0x06, + 0xb8, 0xd3, 0x1f, 0x44, 0x77, 0xcb, 0x86, 0x4b, 0x6e, 0x43, 0x1e, 0x0a, 0x0d, 0xc9, 0xd8, 0x07, 0xc3, 0xd5, 0xe7, + 0x51, 0xf6, 0xf0, 0xf8, 0x3b, 0x46, 0x6b, 0x54, 0xbd, 0xb8, 0x6e, 0x16, 0x3f, 0x70, 0x61, 0xdd, 0xa9, 0xab, 0x5f, + 0x51, 0xde, 0xfc, 0x69, 0xd9, 0x87, 0x55, 0x7e, 0x42, 0x16, 0xd8, 0xd7, 0xf2, 0xe6, 0x04, 0xac, 0xc5, 0x1c, 0x54, + 0x23, 0xf9, 0x45, 0x29, 0x0d, 0xec, 0x80, 0x69, 0xca, 0x35, 0x5a, 0x66, 0xea, 0x4f, 0x3d, 0x38, 0x19, 0x5f, 0x37, + 0x1c, 0x4a, 0x67, 0x77, 0xff, 0xd2, 0x71, 0x0f, 0xa1, 0x29, 0xd2, 0x84, 0xbf, 0x3e, 0x9e, 0xd8, 0x38, 0xb1, 0x8a, + 0x5a, 0x60, 0x5c, 0x39, 0xee, 0xef, 0xad, 0xae, 0x73, 0xf5, 0xd2, 0x87, 0x18, 0x48, 0x92, 0x69, 0xbc, 0x50, 0x09, + 0x52, 0x11, 0xaf, 0x50, 0x70, 0xda, 0xde, 0xef, 0xae, 0xec, 0x51, 0xde, 0xfe, 0xa7, 0x78, 0x33, 0xa3, 0xf9, 0x57, + 0x78, 0x79, 0x2f, 0xd7, 0xef, 0xba, 0xf3, 0xf5, 0x95, 0xfd, 0xb0, 0xdb, 0xff, 0x34, 0x03, 0xc9, 0x55, 0x2a, 0xfd, + 0xe9, 0x52, 0xcf, 0x67, 0x9f, 0x00, 0xf0, 0xeb, 0x95, 0xa1, 0x86, 0x64, 0x58, 0x13, 0xcd, 0x44, 0xc2, 0x5a, 0x25, + 0x62, 0x7c, 0xb3, 0x84, 0xaf, 0x69, 0x77, 0x45, 0x78, 0xa4, 0x8c, 0x8d, 0xb3, 0xb6, 0x43, 0xd6, 0xc7, 0x4c, 0x90, + 0xdd, 0x16, 0xcc, 0xf5, 0xd3, 0xac, 0x9f, 0x86, 0x55, 0xb5, 0x08, 0xd5, 0x27, 0x94, 0xe9, 0xf3, 0x68, 0x00, 0xdd, + 0xa0, 0x70, 0x64, 0x68, 0x24, 0x32, 0x36, 0xfa, 0x61, 0x37, 0x11, 0x1d, 0x47, 0x64, 0x44, 0x64, 0x25, 0x45, 0x21, + 0x9a, 0x4d, 0xfc, 0xf8, 0xfc, 0x27, 0xa5, 0x5a, 0x50, 0x24, 0xe1, 0x1a, 0x80, 0xa4, 0xf6, 0xc3, 0x35, 0x04, 0xa6, + 0xfa, 0xc3, 0xb6, 0x35, 0xe8, 0xd8, 0xc8, 0xca, 0x86, 0xa4, 0x8e, 0xa4, 0xbf, 0x0d, 0x22, 0x49, 0xa6, 0x72, 0x93, + 0x8c, 0x8d, 0x90, 0x03, 0xcc, 0x3b, 0x5a, 0x9b, 0x6f, 0x46, 0xd4, 0x91, 0x74, 0x4c, 0x58, 0x89, 0x3d, 0xa2, 0x30, + 0xc1, 0x11, 0xc2, 0xfd, 0x9a, 0xec, 0x42, 0xff, 0x29, 0xc0, 0xf6, 0x53, 0x43, 0xa2, 0x66, 0xfb, 0x48, 0xc3, 0xa7, + 0xd0, 0xf3, 0x10, 0xe2, 0x6d, 0x98, 0x97, 0x10, 0x16, 0xb9, 0xc1, 0x0e, 0xf4, 0x5e, 0x90, 0xa9, 0x08, 0x6f, 0x24, + 0x6c, 0x62, 0x2d, 0x10, 0x80, 0x67, 0xeb, 0x3e, 0x15, 0x1c, 0x00, 0xa4, 0xcd, 0xca, 0xb1, 0x7c, 0x7f, 0x3c, 0x90, + 0x43, 0x5b, 0x9a, 0x1d, 0xa9, 0x3b, 0xc4, 0xa5, 0x34, 0x9f, 0xe8, 0xd8, 0x1a, 0xc9, 0x41, 0xc2, 0x68, 0xc5, 0x33, + 0xb9, 0x28, 0x9b, 0x76, 0x7e, 0x18, 0xde, 0x57, 0xa5, 0x26, 0x9e, 0xb4, 0xbd, 0xca, 0x1c, 0xc5, 0xe4, 0xf1, 0xd0, + 0xd7, 0xba, 0x0d, 0x97, 0x1e, 0xf4, 0x34, 0x1c, 0x4f, 0x52, 0xfe, 0x7a, 0x8e, 0x62, 0x6d, 0xfc, 0x30, 0xd2, 0x50, + 0x01, 0x19, 0x1e, 0xdc, 0x72, 0xd9, 0x6c, 0xf5, 0xc3, 0xee, 0xf8, 0x61, 0x13, 0x3e, 0xda, 0x8b, 0x6b, 0x33, 0xa7, + 0x97, 0x41, 0x1a, 0xcc, 0x87, 0x92, 0x82, 0x2b, 0xab, 0xc6, 0xbe, 0x37, 0x95, 0xd4, 0xfe, 0xdd, 0xa6, 0x60, 0xdb, + 0xda, 0x46, 0x2f, 0xae, 0x3f, 0x2a, 0x91, 0xf9, 0xfa, 0xdd, 0x34, 0xee, 0x76, 0x76, 0xdb, 0x82, 0x68, 0x84, 0x95, + 0x3b, 0x26, 0x96, 0xd3, 0x6f, 0x9a, 0x74, 0x73, 0x43, 0xe8, 0x23, 0x8a, 0x7f, 0x9b, 0x94, 0xe3, 0xb3, 0xc3, 0xf3, + 0x6b, 0xe8, 0x41, 0x13, 0xa6, 0xaa, 0xc7, 0xe9, 0x0e, 0x16, 0x89, 0xe2, 0x09, 0xaf, 0x88, 0x44, 0xf6, 0xea, 0x87, + 0x43, 0xc6, 0x12, 0x85, 0x21, 0xd2, 0x98, 0xc7, 0x0f, 0xbb, 0x74, 0xd8, 0x79, 0x18, 0xc6, 0x09, 0x70, 0xd9, 0x97, + 0x94, 0xbc, 0xb1, 0x86, 0xdf, 0x7e, 0x0e, 0x4c, 0xfb, 0x7e, 0x7b, 0x9f, 0xe9, 0xad, 0x78, 0x69, 0x6c, 0xbc, 0xde, + 0xa1, 0x10, 0x21, 0xa2, 0x9c, 0x36, 0x3e, 0xae, 0x7f, 0xa4, 0xd8, 0xb0, 0x65, 0x59, 0xae, 0x18, 0xdd, 0xe2, 0xd7, + 0xc0, 0x26, 0x34, 0x6c, 0x87, 0x90, 0x3e, 0xb2, 0x6b, 0x5e, 0x09, 0x68, 0x55, 0x0f, 0x4b, 0xbd, 0xa2, 0x0b, 0x68, + 0x39, 0xc7, 0x48, 0xd9, 0x40, 0x19, 0x28, 0xf8, 0x17, 0x67, 0xd0, 0x55, 0x36, 0xb3, 0xcc, 0xd6, 0xc8, 0x82, 0x7f, + 0x10, 0x4e, 0xe7, 0x4f, 0xa2, 0xd5, 0x84, 0x2c, 0xe1, 0x52, 0xf1, 0x16, 0x14, 0xd8, 0x4a, 0x31, 0x05, 0x06, 0xb4, + 0x7d, 0x22, 0x8d, 0x5f, 0x8c, 0x69, 0x05, 0xd4, 0xd1, 0xe3, 0x32, 0xca, 0xe0, 0x33, 0xad, 0x2b, 0x16, 0x97, 0x41, + 0x7b, 0xa0, 0x31, 0xfc, 0x6b, 0x6b, 0xec, 0x5b, 0xdb, 0x65, 0xfe, 0x3d, 0xe0, 0x35, 0xb5, 0xa7, 0x14, 0x62, 0x05, + 0xd1, 0x01, 0xb2, 0x76, 0x0d, 0x9d, 0xbd, 0x67, 0xcf, 0xc7, 0xd6, 0x72, 0x05, 0x53, 0xe8, 0xa0, 0x62, 0x78, 0x83, + 0xcd, 0xfd, 0x23, 0x85, 0x33, 0x0d, 0xe9, 0x3c, 0xb3, 0x5a, 0x91, 0xcb, 0x14, 0xd4, 0x88, 0x7f, 0x9d, 0x3b, 0x58, + 0x24, 0x51, 0x3d, 0xe2, 0x14, 0x91, 0xa6, 0x93, 0x05, 0x26, 0xa1, 0x8e, 0xd4, 0xd0, 0x76, 0xbb, 0x82, 0x27, 0xca, + 0x4f, 0x38, 0xfd, 0x9b, 0xa5, 0x6b, 0xd4, 0x16, 0x7c, 0x0e, 0xcd, 0xe2, 0x0f, 0x51, 0x4b, 0x7f, 0xfd, 0xf1, 0xc1, + 0x00, 0x01, 0xc4, 0xdb, 0xb3, 0x41, 0x08, 0x13, 0x4f, 0xc7, 0xd6, 0x99, 0x7c, 0xc8, 0x40, 0x30, 0x9b, 0x6a, 0x84, + 0x6c, 0x84, 0xb9, 0xb5, 0x77, 0xd3, 0x3a, 0xf9, 0x03, 0xa7, 0xc0, 0x14, 0xe2, 0x84, 0xed, 0xa0, 0xc0, 0xfc, 0x61, + 0x1c, 0x45, 0x08, 0xf5, 0xe5, 0xd7, 0x22, 0x19, 0xc9, 0xf9, 0x36, 0x98, 0x8b, 0x18, 0x25, 0xd8, 0x5a, 0xf1, 0x5a, + 0x3f, 0x20, 0xaa, 0xda, 0xef, 0x4d, 0x86, 0xed, 0x8c, 0x3e, 0xbe, 0x1b, 0x4f, 0x8a, 0x6f, 0x1c, 0xd7, 0x73, 0x18, + 0xca, 0xfb, 0x67, 0x48, 0xa2, 0x65, 0xde, 0xff, 0xc4, 0xb9, 0xdb, 0xd5, 0xb1, 0x09, 0x2f, 0x6a, 0x03, 0xc3, 0x84, + 0xb0, 0xc1, 0xed, 0x79, 0x9b, 0xec, 0x34, 0x58, 0x9c, 0x4e, 0x17, 0xbc, 0xe1, 0x1a, 0x85, 0x7d, 0xb6, 0x33, 0x29, + 0xee, 0x5d, 0xfb, 0xd7, 0x71, 0x23, 0x1a, 0xf7, 0x19, 0x93, 0x90, 0x7f, 0x67, 0x39, 0x53, 0x9a, 0x3e, 0xad, 0x0a, + 0x4f, 0xfa, 0xce, 0xd9, 0xcd, 0x7c, 0x04, 0x17, 0xed, 0x6f, 0x80, 0xe5, 0x4e, 0xb6, 0x39, 0x27, 0x79, 0x46, 0xf3, + 0x0b, 0xbc, 0xd4, 0xd2, 0xcf, 0xed, 0xb4, 0xea, 0x40, 0x74, 0xb7, 0x00, 0x15, 0x0c, 0xd4, 0xe1, 0x81, 0xb7, 0x63, + 0x3b, 0xc4, 0xa7, 0x1a, 0x8c, 0x41, 0x60, 0xfa, 0x0f, 0xdf, 0xcd, 0xf5, 0x2e, 0x14, 0xa2, 0xcf, 0xa2, 0xe5, 0x2e, + 0xa3, 0x2f, 0xec, 0x84, 0x28, 0x23, 0x17, 0x31, 0xfa, 0x39, 0xba, 0x4b, 0xc8, 0x0d, 0x32, 0x17, 0x11, 0x54, 0xdc, + 0x93, 0xef, 0x88, 0x1f, 0xb0, 0x0b, 0x20, 0x1a, 0xc4, 0x39, 0x87, 0x8a, 0xfe, 0x26, 0x94, 0xa2, 0xd9, 0x61, 0x3c, + 0xff, 0xbb, 0x2c, 0x42, 0xe4, 0xcf, 0xa3, 0x78, 0x57, 0xc8, 0xf7, 0xee, 0xb1, 0xc5, 0x48, 0xf0, 0xc5, 0xb7, 0x41, + 0x2f, 0xe4, 0xc9, 0x9e, 0xc8, 0x20, 0xba, 0xf1, 0x0b, 0xa9, 0x56, 0x89, 0x5c, 0x5d, 0x64, 0x2c, 0x98, 0x5f, 0x21, + 0xa7, 0x3f, 0x6d, 0xef, 0xfc, 0xf2, 0x0f, 0x0c, 0xea, 0x98, 0xc5, 0x7f, 0x36, 0xee, 0x9b, 0x50, 0xa4, 0xef, 0xc5, + 0xe3, 0x03, 0xe2, 0x07, 0xd1, 0xf5, 0x2e, 0x41, 0xb1, 0x95, 0xcc, 0x09, 0x41, 0xa2, 0x70, 0x7c, 0x51, 0x7b, 0xff, + 0x9d, 0xde, 0x85, 0x9b, 0xa8, 0x3d, 0x08, 0xe8, 0x27, 0xff, 0xf0, 0xcb, 0x1f, 0x10, 0x1f, 0x88, 0x2c, 0xb8, 0xbe, + 0x9b, 0x67, 0xab, 0x3f, 0x71, 0x9e, 0xbb, 0x18, 0x44, 0x9f, 0x80, 0x0a, 0x12, 0x56, 0xa9, 0x9e, 0xc1, 0x03, 0xf6, + 0x3f, 0x2c, 0x5c, 0x8d, 0x78, 0xfd, 0xf8, 0xf4, 0x26, 0x5e, 0x43, 0xe7, 0x0a, 0xab, 0x0e, 0x5f, 0x80, 0xc8, 0x21, + 0xb9, 0x54, 0x5d, 0xec, 0x38, 0xd3, 0xff, 0x55, 0x02, 0x36, 0xde, 0x11, 0xc1, 0xe9, 0xfc, 0xc3, 0xcb, 0x17, 0x1b, + 0x7b, 0xb2, 0x9b, 0xdb, 0x61, 0xfc, 0x93, 0x06, 0x96, 0x70, 0x5f, 0xd3, 0xf4, 0x47, 0xc6, 0xe4, 0xd3, 0xfc, 0xf6, + 0x49, 0x3f, 0x1f, 0x4b, 0xbe, 0xfd, 0xe8, 0x17, 0xfe, 0xa8, 0x5f, 0xf3, 0xec, 0x57, 0xb2, 0x26, 0x3b, 0xec, 0x35, + 0xc0, 0xa7, 0xbd, 0xf1, 0xa5, 0xe5, 0xfa, 0x5a, 0xc5, 0xf8, 0x8b, 0x51, 0xe8, 0xd3, 0xef, 0x2e, 0x1f, 0xbc, 0x92, + 0x77, 0x0b, 0x25, 0xcd, 0x54, 0x50, 0xe7, 0xd6, 0xa6, 0xb6, 0x15, 0xda, 0x4d, 0x30, 0x09, 0xf6, 0x06, 0x05, 0x91, + 0x46, 0x15, 0x9e, 0xc8, 0xa2, 0x6d, 0x19, 0x94, 0x0a, 0x86, 0xd2, 0x1c, 0x47, 0x5d, 0x0f, 0x89, 0x03, 0x46, 0xf4, + 0x8c, 0x68, 0x55, 0xab, 0x38, 0x1d, 0x1d, 0x2c, 0x04, 0x9c, 0x42, 0x84, 0x11, 0xc8, 0xf7, 0xea, 0x84, 0x0a, 0x74, + 0x21, 0x69, 0x08, 0xf1, 0x3b, 0xe9, 0x58, 0x8a, 0xe2, 0xda, 0x0a, 0x5f, 0xef, 0x3f, 0xc9, 0xc6, 0xca, 0x47, 0x01, + 0x16, 0xe5, 0x1d, 0x4a, 0xa9, 0x0e, 0x29, 0x98, 0x5c, 0xa4, 0x2e, 0x47, 0xcc, 0x9c, 0x8f, 0x64, 0xb3, 0xe0, 0xb0, + 0x9a, 0x5b, 0xd1, 0x6e, 0x9c, 0xe5, 0xe0, 0xd0, 0x2a, 0xe3, 0x30, 0x86, 0x24, 0x37, 0xf9, 0x35, 0x0a, 0x28, 0x27, + 0xeb, 0x53, 0xdc, 0x02, 0xdf, 0x72, 0xfb, 0x8c, 0x5c, 0xa5, 0xd0, 0xd9, 0x23, 0xdf, 0x33, 0xfc, 0xc1, 0xe3, 0xfd, + 0xee, 0x73, 0x78, 0x34, 0x65, 0xd5, 0x84, 0xb5, 0x7f, 0xb4, 0x21, 0x21, 0x94, 0x02, 0x55, 0x04, 0x08, 0x53, 0x65, + 0x0d, 0xac, 0xeb, 0x90, 0x9a, 0x43, 0x4d, 0xd7, 0x9f, 0x58, 0xe4, 0x88, 0x77, 0x98, 0x38, 0xbf, 0x61, 0x60, 0x89, + 0xa5, 0x0c, 0xf6, 0x06, 0xbc, 0xd6, 0xc2, 0x3e, 0x8b, 0x02, 0x75, 0x26, 0xe7, 0x8a, 0x23, 0x08, 0xba, 0xa5, 0x66, + 0x26, 0x2a, 0x9d, 0x65, 0x8f, 0x34, 0x3f, 0xc5, 0xbc, 0x62, 0xbf, 0x2a, 0x93, 0x86, 0x74, 0xd0, 0x99, 0xdc, 0x9a, + 0x9a, 0x02, 0x57, 0x21, 0x55, 0xb5, 0x4e, 0xec, 0x38, 0xf1, 0xc2, 0xcf, 0xd3, 0x11, 0xc7, 0x36, 0x3e, 0x0f, 0x45, + 0x7d, 0x92, 0xf7, 0x69, 0xe9, 0xfa, 0xd0, 0x25, 0xd7, 0x06, 0x69, 0x7a, 0x3b, 0xa2, 0x2b, 0x3f, 0xbc, 0xa6, 0x31, + 0x4d, 0x5f, 0x39, 0xba, 0x4f, 0x73, 0xf3, 0x49, 0xdb, 0x58, 0xb2, 0xf9, 0xd1, 0x5f, 0xf2, 0xca, 0xac, 0x43, 0x28, + 0x72, 0x99, 0x82, 0xfb, 0x7d, 0x3e, 0xaa, 0xc9, 0xf6, 0x3b, 0x78, 0x14, 0x88, 0x2f, 0x1b, 0x81, 0x30, 0xfd, 0xe2, + 0xc1, 0x70, 0x23, 0xaf, 0x06, 0xe6, 0x6a, 0x82, 0xeb, 0x75, 0x7d, 0x02, 0xe9, 0xd9, 0x1a, 0x03, 0x1b, 0x21, 0x53, + 0x57, 0xc1, 0x7b, 0xf6, 0x2e, 0x68, 0x6f, 0xe0, 0x37, 0x7b, 0xc0, 0x08, 0xb3, 0x7a, 0xcb, 0x08, 0x28, 0x0c, 0x29, + 0xd4, 0x4d, 0x93, 0x14, 0x0d, 0xa1, 0x02, 0x06, 0xfc, 0xfc, 0x45, 0xe8, 0xc2, 0xd3, 0x12, 0xe8, 0x7f, 0x70, 0x3e, + 0xd4, 0x8a, 0x32, 0xce, 0x2f, 0x5a, 0x6c, 0x69, 0xee, 0x34, 0x4f, 0x4c, 0x7e, 0xec, 0x23, 0x3c, 0x4f, 0xe5, 0x38, + 0x9c, 0xdd, 0xd7, 0xe9, 0x4a, 0x7b, 0xa9, 0x39, 0x9e, 0x34, 0xff, 0x6a, 0xe3, 0xcb, 0xbc, 0x13, 0x91, 0x38, 0xc1, + 0x5d, 0x80, 0x7f, 0x3d, 0x8f, 0x24, 0x61, 0x38, 0x5d, 0x34, 0xdf, 0x14, 0xef, 0x56, 0x13, 0x6f, 0x71, 0xd5, 0x22, + 0x8d, 0xdb, 0x43, 0xdc, 0xf7, 0x7e, 0x0d, 0x9e, 0x3b, 0xdb, 0xf5, 0x7c, 0xd8, 0x0a, 0x1e, 0x90, 0xc9, 0xe5, 0x14, + 0x08, 0x5e, 0xc4, 0x62, 0x32, 0x0f, 0xa9, 0x57, 0xb6, 0x0e, 0xcb, 0xd2, 0x79, 0xa5, 0xb6, 0x89, 0x7a, 0xc5, 0xb8, + 0x96, 0x6f, 0x76, 0xeb, 0x45, 0x5c, 0x12, 0x0b, 0x2d, 0xae, 0x95, 0x56, 0xaa, 0x59, 0x9f, 0x18, 0x5b, 0x8e, 0xda, + 0x76, 0xff, 0x7e, 0x83, 0xc8, 0x6a, 0x9f, 0x5f, 0x3e, 0x2e, 0x88, 0x81, 0x0f, 0x99, 0xa3, 0xf4, 0xf8, 0x02, 0xad, + 0xb2, 0x6e, 0xad, 0xbd, 0x3a, 0xed, 0x4e, 0xa6, 0xdf, 0x96, 0xf5, 0x61, 0x17, 0x8c, 0xd7, 0x05, 0xb1, 0xed, 0x51, + 0xe8, 0x27, 0xd6, 0xe7, 0x7b, 0xaa, 0x10, 0xfe, 0x6d, 0x7a, 0xff, 0xb3, 0xb7, 0xcd, 0x73, 0xa9, 0x22, 0x8e, 0x90, + 0x79, 0xa2, 0x36, 0x8c, 0x95, 0x92, 0xbd, 0xa0, 0x43, 0x8a, 0xd5, 0x8c, 0x42, 0x03, 0x81, 0x54, 0x37, 0xbd, 0x93, + 0x57, 0x03, 0x80, 0x29, 0x66, 0xb0, 0xe1, 0x70, 0x17, 0xf0, 0x0e, 0xb5, 0x82, 0x70, 0x9c, 0x33, 0xaa, 0x96, 0x2e, + 0xb5, 0xde, 0x8e, 0x9f, 0xc2, 0x51, 0x1d, 0x70, 0xd1, 0xfe, 0x78, 0x2c, 0xd8, 0x8a, 0x44, 0x0d, 0x71, 0x2e, 0x4d, + 0x5a, 0xa8, 0x0f, 0x51, 0x8f, 0x7d, 0x37, 0x68, 0x33, 0xbc, 0x05, 0x5f, 0x11, 0xb8, 0xc2, 0x2f, 0x71, 0x70, 0xcb, + 0x74, 0xb8, 0x87, 0xad, 0xeb, 0x9a, 0xe8, 0x8b, 0xfa, 0x33, 0x66, 0x59, 0x08, 0x72, 0x7a, 0xc2, 0x3f, 0xa8, 0x85, + 0x4a, 0x41, 0xf0, 0x72, 0x2e, 0xe0, 0xfe, 0x1c, 0x46, 0x4f, 0x48, 0xf9, 0xa1, 0x88, 0x04, 0x69, 0x1d, 0x99, 0x1a, + 0x1c, 0xf7, 0x58, 0x97, 0x18, 0x66, 0x2f, 0x82, 0x83, 0xc5, 0xac, 0x11, 0x59, 0xd5, 0x23, 0xf8, 0xcd, 0x93, 0xa6, + 0x75, 0x88, 0x25, 0x85, 0x1a, 0xd6, 0x54, 0xfa, 0x5b, 0x10, 0xa9, 0x4d, 0x97, 0x7f, 0x02, 0x74, 0x6d, 0x4f, 0x94, + 0x9e, 0xf6, 0x92, 0x5a, 0x54, 0x1d, 0xda, 0x46, 0xc2, 0xdc, 0xa5, 0xc0, 0xd0, 0x38, 0xf0, 0x00, 0xb1, 0xf6, 0xae, + 0xc8, 0xe4, 0x3d, 0x74, 0x99, 0x3c, 0x44, 0xd5, 0x4e, 0x8d, 0xed, 0x72, 0xca, 0x0f, 0x52, 0x6d, 0x61, 0xe4, 0x14, + 0x75, 0x4a, 0x95, 0x17, 0x46, 0x08, 0xea, 0xd6, 0x57, 0xc7, 0xba, 0x08, 0xcd, 0xc3, 0x6a, 0xed, 0x44, 0x2f, 0xb1, + 0xcc, 0xfe, 0xd1, 0x20, 0xce, 0xcc, 0xc2, 0x40, 0x73, 0xfe, 0x53, 0xb7, 0x18, 0xa2, 0xfd, 0x5f, 0xf2, 0xb0, 0x5e, + 0x77, 0xfe, 0x74, 0x5c, 0x78, 0x69, 0xb7, 0x4b, 0x77, 0x1b, 0xbd, 0x37, 0xe0, 0x1a, 0x8c, 0xf9, 0x93, 0x7c, 0xa6, + 0x8d, 0x08, 0xa8, 0xf8, 0x36, 0x7c, 0x7c, 0x3f, 0xda, 0x47, 0xe4, 0x21, 0x72, 0x98, 0x3f, 0x46, 0xbf, 0x13, 0x6c, + 0x51, 0x6b, 0x44, 0xb2, 0x8a, 0xb0, 0x20, 0x35, 0x77, 0xf8, 0xd6, 0x23, 0xdf, 0x5c, 0x57, 0x3b, 0xf1, 0x79, 0x0d, + 0x02, 0xa8, 0x58, 0x4d, 0x1b, 0x07, 0xfa, 0xdc, 0xf6, 0x19, 0xcf, 0x41, 0x13, 0x1d, 0x85, 0x43, 0xfc, 0xf3, 0x9c, + 0x73, 0xb4, 0xa3, 0x9d, 0x1c, 0x87, 0xc7, 0xd8, 0x2b, 0xc5, 0xb9, 0xff, 0x8c, 0x42, 0x13, 0x96, 0x9f, 0xe5, 0x3b, + 0xd4, 0x07, 0xfc, 0x3c, 0xe5, 0x7f, 0x5c, 0xf5, 0x40, 0x88, 0x3d, 0x21, 0x00, 0xce, 0x9f, 0xfe, 0x23, 0x14, 0xf2, + 0xa7, 0x12, 0x2e, 0xfc, 0x07, 0x86, 0x84, 0x17, 0xc1, 0x3f, 0xc1, 0xef, 0x2c, 0x31, 0x3a, 0x4c, 0x51, 0xa1, 0xfc, + 0xa3, 0x03, 0x21, 0x5f, 0x73, 0x76, 0x6d, 0x0e, 0x9f, 0xcf, 0x99, 0xe5, 0x0b, 0xae, 0x09, 0xf5, 0x79, 0x2b, 0x30, + 0xff, 0xa6, 0xc9, 0x3e, 0x09, 0x48, 0x2e, 0xfc, 0x56, 0xdc, 0xad, 0x56, 0x93, 0x3c, 0x8a, 0x14, 0xfd, 0x66, 0x1a, + 0x2b, 0x6f, 0xbc, 0x45, 0x89, 0xb6, 0x43, 0x2f, 0x4d, 0x9f, 0xcb, 0x17, 0x84, 0xd9, 0x56, 0xc7, 0x89, 0xd9, 0x1f, + 0xdc, 0x5d, 0xa7, 0x4b, 0x2c, 0x41, 0x64, 0x18, 0x77, 0xc7, 0x60, 0x1d, 0xbe, 0x5a, 0x19, 0x2a, 0x63, 0x11, 0x4a, + 0x15, 0x2d, 0x3d, 0xfc, 0x42, 0x37, 0x71, 0x51, 0xba, 0x99, 0x72, 0xcc, 0xf4, 0x77, 0x68, 0xfd, 0x6b, 0x35, 0x3a, + 0xbb, 0x24, 0x7c, 0xf0, 0x78, 0x2f, 0xe8, 0x6f, 0x3a, 0x64, 0x17, 0xe1, 0x2f, 0x1f, 0x5f, 0xaa, 0x25, 0x0b, 0xa3, + 0x9b, 0xce, 0xa7, 0x34, 0x7b, 0xbb, 0xaf, 0x32, 0x0a, 0x4d, 0x0d, 0x85, 0x91, 0x38, 0x2b, 0xc7, 0x65, 0xef, 0x4c, + 0xd6, 0xf5, 0x73, 0xcf, 0xaa, 0x94, 0x5d, 0x48, 0xb0, 0xa8, 0x97, 0x7b, 0xf7, 0x0d, 0x5a, 0x48, 0xa1, 0x06, 0xd2, + 0x16, 0x03, 0x1d, 0xba, 0x67, 0x38, 0xd1, 0x25, 0x94, 0x40, 0xa4, 0x0f, 0x57, 0x59, 0xd4, 0xf4, 0x45, 0x4c, 0xa0, + 0x4f, 0x3d, 0x5b, 0xec, 0x6c, 0xd7, 0x28, 0x3b, 0x8c, 0x02, 0x72, 0xf7, 0x86, 0x67, 0x46, 0x1f, 0xef, 0xdf, 0xc8, + 0x6a, 0xf9, 0x7f, 0xa3, 0x46, 0xdb, 0x3b, 0x47, 0xa0, 0xe1, 0x99, 0xb7, 0x4b, 0x22, 0x12, 0x24, 0x2c, 0x7e, 0x3e, + 0x79, 0xf6, 0x7d, 0x17, 0x4a, 0xa4, 0xe0, 0xd0, 0x57, 0x63, 0xba, 0x7c, 0xa9, 0x26, 0xca, 0x47, 0x62, 0xc0, 0x4f, + 0x3a, 0x0f, 0x12, 0x5d, 0x4d, 0x73, 0xb0, 0x43, 0x39, 0x70, 0x7b, 0x73, 0xc6, 0xf9, 0x63, 0xbe, 0xc1, 0xca, 0xc1, + 0x93, 0xed, 0x9f, 0x7a, 0xb9, 0x8d, 0x51, 0xc5, 0x4f, 0x44, 0x63, 0x19, 0xf0, 0xf0, 0xd9, 0xe9, 0x08, 0xed, 0x8c, + 0x64, 0x01, 0xca, 0x7b, 0xbb, 0x3f, 0x86, 0x4b, 0xf8, 0x99, 0x1a, 0xef, 0x59, 0xdb, 0xa1, 0xa5, 0xdb, 0xf8, 0xa6, + 0xe4, 0x71, 0x78, 0x60, 0x2d, 0xc5, 0x6a, 0x6c, 0x0d, 0x10, 0x97, 0xb8, 0xa3, 0x6c, 0xad, 0xe2, 0xe2, 0xfe, 0x5f, + 0x1e, 0x9e, 0x39, 0x07, 0x81, 0x2a, 0xe1, 0x60, 0x22, 0x35, 0x23, 0xb6, 0x91, 0x63, 0xc7, 0x6b, 0x46, 0x1c, 0x5c, + 0x01, 0x69, 0x23, 0x26, 0x9a, 0x53, 0xb9, 0x0f, 0xc6, 0xf3, 0xe8, 0x8d, 0xaa, 0x8f, 0x73, 0xe6, 0x81, 0x6d, 0x70, + 0x27, 0x55, 0x1b, 0x16, 0x26, 0xbe, 0xd9, 0xad, 0xa5, 0xe9, 0xcb, 0x8e, 0xac, 0x17, 0x6c, 0xcf, 0x4a, 0x10, 0xfa, + 0x54, 0xfa, 0x37, 0x1a, 0xe2, 0xb9, 0xae, 0x5f, 0x47, 0x17, 0xed, 0x87, 0xb9, 0xc3, 0xfe, 0xee, 0xf8, 0xb4, 0x61, + 0x62, 0x1d, 0x7d, 0x1e, 0x3b, 0x2b, 0xcc, 0xb3, 0x6b, 0x4d, 0x3f, 0xb5, 0xf1, 0xd0, 0xc7, 0xbe, 0xb4, 0x56, 0x66, + 0xb0, 0x2a, 0x28, 0xbb, 0x53, 0x53, 0x03, 0x61, 0x0d, 0xea, 0x3a, 0x99, 0x64, 0x33, 0x65, 0xbf, 0x3c, 0x03, 0xb3, + 0xdf, 0x45, 0xc9, 0x15, 0xfa, 0xeb, 0x7d, 0x69, 0x52, 0xe7, 0x3b, 0xda, 0x22, 0x47, 0xb4, 0xc5, 0xa0, 0x16, 0x11, + 0xef, 0xd4, 0xd7, 0x29, 0xc9, 0x47, 0x2f, 0x5a, 0x82, 0x30, 0xb5, 0xa4, 0xdd, 0x15, 0x28, 0x61, 0x99, 0x91, 0xcf, + 0xf6, 0x13, 0xe3, 0xfd, 0xd3, 0xf8, 0xa5, 0x63, 0xd2, 0x76, 0xb5, 0x6b, 0x07, 0x23, 0xd7, 0xd0, 0x54, 0x41, 0xe3, + 0x16, 0xdf, 0x61, 0xa0, 0x9f, 0xe2, 0x48, 0xdb, 0xaf, 0x35, 0x4f, 0x5f, 0xda, 0xd6, 0xf3, 0xea, 0xf6, 0x89, 0x5a, + 0xeb, 0xc0, 0xb1, 0x33, 0xb4, 0x27, 0x6f, 0x4c, 0x90, 0x0f, 0x7d, 0x3e, 0x3c, 0x0d, 0xa7, 0x26, 0x1f, 0x9d, 0xa5, + 0x90, 0xc8, 0x1e, 0x15, 0x5f, 0x60, 0x3e, 0x1f, 0x28, 0x15, 0x51, 0x1b, 0xef, 0xdd, 0xd6, 0x6e, 0xbe, 0x8f, 0x47, + 0xab, 0x76, 0x8d, 0xd1, 0x46, 0xc2, 0x02, 0x3c, 0x54, 0x89, 0xd2, 0x21, 0x0e, 0xfc, 0x67, 0x92, 0x76, 0x16, 0x75, + 0x8d, 0xb7, 0x65, 0xc3, 0xa4, 0xf9, 0x3c, 0x95, 0x7a, 0x19, 0x77, 0xd8, 0x56, 0x6e, 0xf6, 0xd1, 0x13, 0xd1, 0xbe, + 0x66, 0x6d, 0x3e, 0x41, 0x50, 0x76, 0xb5, 0xc3, 0xbd, 0xea, 0x88, 0x1d, 0x27, 0x6c, 0xbf, 0xd9, 0x7c, 0xe7, 0xa8, + 0x14, 0xa5, 0xc6, 0x09, 0x6b, 0xdd, 0xd4, 0x4e, 0x34, 0x87, 0x30, 0xfc, 0xd2, 0x37, 0xf1, 0x12, 0x52, 0x37, 0x1c, + 0xf3, 0xf6, 0xfe, 0x79, 0x58, 0xd7, 0xc2, 0x09, 0x45, 0xb2, 0x26, 0xf6, 0xde, 0x20, 0xdd, 0xc1, 0x2a, 0x0c, 0x9f, + 0x90, 0x5b, 0x67, 0x75, 0xf2, 0x26, 0x78, 0xa1, 0x21, 0xb2, 0x93, 0x21, 0xdf, 0x32, 0x0e, 0x2c, 0xdd, 0xc0, 0xfe, + 0x5a, 0x95, 0x64, 0x95, 0x27, 0x6a, 0xaf, 0x52, 0xa6, 0x69, 0x49, 0xc1, 0xf2, 0x29, 0xb3, 0x07, 0x47, 0x5e, 0xf3, + 0x65, 0x73, 0xeb, 0x9b, 0x77, 0x4f, 0x9d, 0xf5, 0xd0, 0x2e, 0x76, 0xbd, 0xb5, 0x29, 0x9c, 0xe0, 0x23, 0x49, 0xfc, + 0x50, 0xfb, 0xd9, 0x7e, 0xb0, 0x71, 0xff, 0xa4, 0xf6, 0x03, 0xce, 0xec, 0x53, 0x74, 0x98, 0x87, 0x49, 0x9f, 0x15, + 0x24, 0x1c, 0xd0, 0xba, 0x8f, 0x45, 0xa6, 0xc0, 0x4e, 0x03, 0x9c, 0x40, 0x8d, 0xd8, 0xe3, 0x22, 0x07, 0xf4, 0xa6, + 0x6a, 0x6a, 0x31, 0xdf, 0xd3, 0x91, 0x3b, 0x9c, 0x62, 0x06, 0xbf, 0x68, 0xd8, 0xd2, 0xbc, 0xfa, 0xb8, 0xad, 0x1b, + 0xf4, 0x1c, 0xa4, 0x48, 0xdc, 0x20, 0xa6, 0x49, 0xf7, 0x15, 0x7a, 0xea, 0xeb, 0x37, 0xb9, 0x1d, 0xf7, 0x1d, 0xa7, + 0x8d, 0x76, 0x1b, 0xee, 0x62, 0x95, 0x4d, 0x3b, 0xa4, 0xa3, 0x06, 0xea, 0x4b, 0xff, 0x64, 0x45, 0xa7, 0xa7, 0x29, + 0x42, 0x57, 0x62, 0xdb, 0x04, 0x60, 0x72, 0x50, 0xd8, 0x59, 0x20, 0x09, 0x36, 0x38, 0x71, 0x2c, 0x13, 0x8d, 0xec, + 0x85, 0xbe, 0xda, 0xed, 0x18, 0x18, 0xf8, 0xb9, 0x27, 0xd1, 0x6f, 0xef, 0x2c, 0x52, 0x34, 0x6b, 0x19, 0x7e, 0x65, + 0x22, 0x45, 0x1f}; // Backwards compatibility alias #define INDEX_GZ INDEX_BR diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index cdb8544fbb..3fe3979a8b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -56,9 +56,8 @@ namespace esphome::web_server { static const char *const TAG = "web_server"; -// Longest: UPDATE AVAILABLE (16 chars + null terminator, rounded up) -static constexpr size_t PSTR_LOCAL_SIZE = 18; -#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), PSTR_LOCAL_SIZE - 1) +// View a state LogString as a ProgmemStr so ArduinoJson serializes it PROGMEM-aware on ESP8266. +[[maybe_unused]] static ProgmemStr json_state_str(const LogString *s) { return reinterpret_cast(s); } // Parse URL and return match info // URL formats (disambiguated by HTTP method for 3-segment case): @@ -257,8 +256,10 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char * } // used for logs plus the initial ping/config -void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, - uint32_t reconnect) { +void DeferredUpdateEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, + uint32_t id, uint32_t reconnect) { + // ESPAsyncWebServer's send() only accepts null-terminated strings + (void) message_len; this->send(message, event, id, reconnect); } @@ -279,10 +280,10 @@ void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const ch } } -void DeferredUpdateEventSourceList::try_send_nodefer(const char *message, const char *event, uint32_t id, - uint32_t reconnect) { +void DeferredUpdateEventSourceList::try_send_nodefer(const char *message, size_t message_len, const char *event, + uint32_t id, uint32_t reconnect) { for (DeferredUpdateEventSource *dues : *this) { - dues->try_send_nodefer(message, event, id, reconnect); + dues->try_send_nodefer(message, message_len, event, id, reconnect); } } @@ -304,7 +305,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource // Configure reconnect timeout and send config // this should always go through since the AsyncEventSourceClient event queue is empty on connect auto message = ws->get_config_json(); - source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); + source->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { @@ -315,7 +316,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource auto group_msg = builder.serialize(); // up to 31 groups should be able to be queued initially without defer - source->try_send_nodefer(group_msg.c_str(), "sorting_group"); + source->try_send_nodefer(group_msg.c_str(), group_msg.size(), "sorting_group"); } #endif @@ -395,8 +396,8 @@ void WebServer::setup() { return; char buf[32]; auto uptime = static_cast(millis_64() / 1000); - buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime); - this->events_.try_send_nodefer(buf, "ping", millis(), 30000); + size_t len = buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime); + this->events_.try_send_nodefer(buf, len, "ping", millis(), 30000); }); } void WebServer::loop() { @@ -414,16 +415,16 @@ void WebServer::loop() { void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { (void) level; (void) tag; - (void) message_len; - this->events_.try_send_nodefer(message, "log", millis()); + this->events_.try_send_nodefer(message, message_len, "log", millis()); } #endif void WebServer::dump_config() { + char addr_buf[network::USE_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "Web Server:\n" " Address: %s:%u", - network::get_use_address(), this->base_->get_port()); + network::get_use_address_to(addr_buf), this->base_->get_port()); } float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; } @@ -455,9 +456,58 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { } #endif +// Read a request header value portably across the Arduino and ESP-IDF web servers. +// Returns an empty string when the header is absent (only allocates when a value is present). +static std::string get_request_header(AsyncWebServerRequest *request, const char *name) { +#ifdef USE_ESP32 + // ESP32 (Arduino and ESP-IDF) uses the web_server_idf backend. + optional value = request->get_header(name); + return value.has_value() ? std::move(*value) : std::string(); +#else + // ESP8266, RP2040 and LibreTiny use the Arduino ESPAsyncWebServer backend. + const AsyncWebHeader *header = request->getHeader(name); + return header != nullptr ? std::string(header->value().c_str()) : std::string(); +#endif +} + +bool WebServer::is_request_origin_allowed_(AsyncWebServerRequest *request, const std::string &origin) { + // No Origin header: not a browser cross-origin request (e.g. curl, native API client). Allow. + if (origin.empty()) + return true; + + // Same-origin: the Origin authority (scheme stripped) matches the Host the request was sent to. + // This covers the device's own IP, mDNS name, or DNS name without knowing any at compile time. + const size_t scheme_sep = origin.find("://"); + if (scheme_sep != std::string::npos) { + const std::string host = get_request_header(request, "Host"); + if (!host.empty() && origin.compare(scheme_sep + 3, std::string::npos, host) == 0) + return true; + } + +#ifdef USE_WEBSERVER_ALLOWED_ORIGINS + // Otherwise the origin must be explicitly allowed via configuration. + for (const char *allowed_origin : this->allowed_origins_) { + // A single "*" entry allows any origin. + if (allowed_origin[0] == '*' && allowed_origin[1] == '\0') + return true; + if (origin == allowed_origin) + return true; + } +#endif + return false; +} + #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { + const std::string origin = get_request_header(request, "Origin"); + if (!this->is_request_origin_allowed_(request, origin)) { + request->send(403); + return; + } + AsyncWebServerResponse *response = request->beginResponse(200, ESPHOME_F("")); + // Echo the specific origin back so the response is valid even when auth (credentials) is enabled. + response->addHeader(ESPHOME_F("Access-Control-Allow-Origin"), origin.empty() ? "*" : origin.c_str()); response->addHeader(ESPHOME_F("Access-Control-Allow-Private-Network"), ESPHOME_F("true")); response->addHeader(ESPHOME_F("Private-Network-Access-Name"), App.get_name().c_str()); char mac_s[18]; @@ -508,26 +558,19 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J size_t device_len = device_name ? strlen(device_name) : 0; #endif - // Single stack buffer for both id formats - ArduinoJson copies the string before we overwrite + // Stack buffer for the id - ArduinoJson copies the string before it goes out of scope // Buffer sizes use constants from entity_base.h validated in core/config.py // Note: Device name uses ESPHOME_FRIENDLY_NAME_MAX_LEN (sub-device max 120), not ESPHOME_DEVICE_NAME_MAX_LEN // (hostname) - // Without USE_DEVICES: legacy id ({prefix}-{object_id}) is the largest format - // With USE_DEVICES: name_id ({prefix}/{device}/{name}) is the largest format - static constexpr size_t LEGACY_ID_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + OBJECT_ID_MAX_LEN; #ifdef USE_DEVICES static constexpr size_t ID_BUF_SIZE = - std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1, - LEGACY_ID_SIZE); + ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; #else - static constexpr size_t ID_BUF_SIZE = - std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1, LEGACY_ID_SIZE); + static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; #endif char id_buf[ID_BUF_SIZE]; memcpy(id_buf, prefix, prefix_len); // NOLINT(bugprone-not-null-terminated-result) - // name_id: new format {prefix}/{device?}/{name} - frontend should prefer this - // Remove in 2026.8.0 when id switches to new format permanently char *p = id_buf + prefix_len; *p++ = '/'; #ifdef USE_DEVICES @@ -539,12 +582,6 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J #endif memcpy(p, name.c_str(), name_len); p[name_len] = '\0'; - root[ESPHOME_F("name_id")] = id_buf; - - // id: old format {prefix}-{object_id} for backward compatibility - // Will switch to new format in 2026.8.0 - reuses prefix already in id_buf - id_buf[prefix_len] = '-'; - obj->write_object_id_to(id_buf + prefix_len + 1, ID_BUF_SIZE - prefix_len - 1); root[ESPHOME_F("id")] = id_buf; if (start_config == DETAIL_ALL) { @@ -576,9 +613,9 @@ static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix root[ESPHOME_F("value")] = value; } -template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const char *state, - const T &value, JsonDetail start_config) { +template +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, S state, const T &value, + JsonDetail start_config) { set_json_value(root, obj, prefix, value, start_config); root[ESPHOME_F("state")] = state; } @@ -1071,14 +1108,14 @@ json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, start_config); - char buf[PSTR_LOCAL_SIZE]; - root[ESPHOME_F("current_operation")] = PSTR_LOCAL(cover::cover_operation_to_str(obj->current_operation)); + root[ESPHOME_F("current_operation")] = json_state_str(cover::cover_operation_to_str(obj->current_operation)); if (obj->get_traits().get_supports_position()) root[ESPHOME_F("position")] = obj->position; if (obj->get_traits().get_supports_tilt()) root[ESPHOME_F("tilt")] = obj->tilt; if (start_config == DETAIL_ALL) { + root[ESPHOME_F("assumed_state")] = obj->get_traits().get_is_assumed_state(); this->add_sorting_info_(root, obj); } @@ -1528,17 +1565,16 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json const auto traits = obj->get_traits(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); - char buf[PSTR_LOCAL_SIZE]; char temp_buf[VALUE_ACCURACY_MAX_LEN]; if (start_config == DETAIL_ALL) { JsonArray opt = root[ESPHOME_F("modes")].to(); for (climate::ClimateMode m : traits.get_supported_modes()) - opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); + opt.add(json_state_str(climate::climate_mode_to_string(m))); if (traits.get_supports_fan_modes()) { JsonArray opt = root[ESPHOME_F("fan_modes")].to(); for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) - opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); + opt.add(json_state_str(climate::climate_fan_mode_to_string(m))); } if (!traits.get_supported_custom_fan_modes().empty()) { @@ -1549,12 +1585,12 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json if (traits.get_supports_swing_modes()) { JsonArray opt = root[ESPHOME_F("swing_modes")].to(); for (auto swing_mode : traits.get_supported_swing_modes()) - opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); + opt.add(json_state_str(climate::climate_swing_mode_to_string(swing_mode))); } if (traits.get_supports_presets()) { JsonArray opt = root[ESPHOME_F("presets")].to(); for (climate::ClimatePreset m : traits.get_supported_presets()) - opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); + opt.add(json_state_str(climate::climate_preset_to_string(m))); } if (!traits.get_supported_custom_presets().empty()) { JsonArray opt = root[ESPHOME_F("custom_presets")].to(); @@ -1570,26 +1606,26 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json } bool has_state = false; - root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); + root[ESPHOME_F("mode")] = json_state_str(climate_mode_to_string(obj->mode)); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action)); + root[ESPHOME_F("action")] = json_state_str(climate_action_to_string(obj->action)); root[ESPHOME_F("state")] = root[ESPHOME_F("action")]; has_state = true; } if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { - root[ESPHOME_F("fan_mode")] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); + root[ESPHOME_F("fan_mode")] = json_state_str(climate_fan_mode_to_string(obj->fan_mode.value())); } if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode(); } if (traits.get_supports_presets() && obj->preset.has_value()) { - root[ESPHOME_F("preset")] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); + root[ESPHOME_F("preset")] = json_state_str(climate_preset_to_string(obj->preset.value())); } if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { root[ESPHOME_F("custom_preset")] = obj->get_custom_preset(); } if (traits.get_supports_swing_modes()) { - root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); + root[ESPHOME_F("swing_mode")] = json_state_str(climate_swing_mode_to_string(obj->swing_mode)); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { root[ESPHOME_F("current_temperature")] = @@ -1693,8 +1729,7 @@ json::SerializationBuffer<> WebServer::lock_json_(lock::Lock *obj, lock::LockSta json::JsonBuilder builder; JsonObject root = builder.root(); - char buf[PSTR_LOCAL_SIZE]; - set_json_icon_state_value(root, obj, "lock", PSTR_LOCAL(lock::lock_state_to_string(value)), value, start_config); + set_json_icon_state_value(root, obj, "lock", json_state_str(lock::lock_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1775,8 +1810,7 @@ json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, start_config); - char buf[PSTR_LOCAL_SIZE]; - root[ESPHOME_F("current_operation")] = PSTR_LOCAL(valve::valve_operation_to_str(obj->current_operation)); + root[ESPHOME_F("current_operation")] = json_state_str(valve::valve_operation_to_str(obj->current_operation)); if (obj->get_traits().get_supports_position()) root[ESPHOME_F("position")] = obj->position; @@ -1861,9 +1895,8 @@ json::SerializationBuffer<> WebServer::alarm_control_panel_json_(alarm_control_p json::JsonBuilder builder; JsonObject root = builder.root(); - char buf[PSTR_LOCAL_SIZE]; - set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)), - value, start_config); + set_json_icon_state_value(root, obj, "alarm_control_panel", + json_state_str(alarm_control_panel_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1935,10 +1968,9 @@ json::SerializationBuffer<> WebServer::water_heater_all_json_generator(WebServer json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - char buf[PSTR_LOCAL_SIZE]; const auto mode = obj->get_mode(); - const char *mode_s = PSTR_LOCAL(water_heater::water_heater_mode_to_string(mode)); + ProgmemStr mode_s = json_state_str(water_heater::water_heater_mode_to_string(mode)); set_json_icon_state_value(root, obj, "water_heater", mode_s, mode, start_config); @@ -1947,7 +1979,7 @@ json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHea if (start_config == DETAIL_ALL) { JsonArray modes = root[ESPHOME_F("modes")].to(); for (auto m : traits.get_supported_modes()) - modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m))); + modes.add(json_state_str(water_heater::water_heater_mode_to_string(m))); root[ESPHOME_F("min_temp")] = traits.get_min_temperature(); root[ESPHOME_F("max_temp")] = traits.get_max_temperature(); root[ESPHOME_F("step")] = traits.get_target_temperature_step(); @@ -2275,8 +2307,7 @@ json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, J json::JsonBuilder builder; JsonObject root = builder.root(); - char buf[PSTR_LOCAL_SIZE]; - set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update::update_state_to_string(obj->state)), + set_json_icon_state_value(root, obj, "update", json_state_str(update::update_state_to_string(obj->state)), obj->update_info.latest_version, start_config); if (start_config == DETAIL_ALL) { root[ESPHOME_F("current_version")] = obj->update_info.current_version; @@ -2454,6 +2485,21 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } +#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS + // Private Network Access preflight carries a cross-origin Origin by design; its handler does the + // origin check itself, so let it run before the general enforcement below. + if (request->method() == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network"))) { + this->handle_pna_cors_request(request); + return; + } +#endif + + // Reject cross-origin browser requests unless the origin is explicitly allowed. + if (!this->is_request_origin_allowed_(request, get_request_header(request, "Origin"))) { + request->send(403); + return; + } + #if !defined(USE_ESP32) && defined(USE_ARDUINO) if (url == ESPHOME_F("/events")) { this->events_.add_new_client(this, request); @@ -2475,13 +2521,6 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { } #endif -#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS - if (request->method() == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network"))) { - this->handle_pna_cors_request(request); - return; - } -#endif - // Parse URL for component routing // Pass HTTP method to disambiguate 3-segment URLs (GET=sub-device state, POST=main device action) UrlMatch match = match_url(url.c_str(), url.length(), false, request->method() == HTTP_POST); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index e4defdbd9a..0fbe4ec551 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -160,7 +160,8 @@ class DeferredUpdateEventSource final : public AsyncEventSource { void loop(); void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); - void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0, + uint32_t reconnect = 0); }; class DeferredUpdateEventSourceList final : public std::list { @@ -173,7 +174,8 @@ class DeferredUpdateEventSourceList final : public std::listexpose_log_ = expose_log; } +#ifdef USE_WEBSERVER_ALLOWED_ORIGINS + /** Set the origins that browsers are allowed to make cross-origin requests from. + * + * Requests without an `Origin` header (e.g. non-browser clients like curl or the native API) + * are always allowed. Requests whose `Origin` matches the address the device is served on + * (same-origin) are always allowed. Any other browser origin must appear in this list, or the + * request is rejected. A single "*" entry allows any origin. Each other entry must exactly match + * the requesting page's `Origin` header (e.g. "https://example.com"). + * + * This list is also used to authorize Private Network Access requests when that feature is enabled. + * + * @param origins The list of allowed origins. + */ + void set_allowed_origins(std::initializer_list origins) { this->allowed_origins_ = origins; } +#endif + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup the internal web server and register handlers. @@ -591,6 +609,16 @@ class WebServer final : public Controller, public Component, public AsyncWebHand const char *js_include_{nullptr}; #endif bool expose_log_{true}; +#ifdef USE_WEBSERVER_ALLOWED_ORIGINS + // Extra origins allowed to make cross-origin browser requests ("*" means any origin). + // Only compiled when allowed_origins is configured; same-origin is always allowed regardless. + FixedVector allowed_origins_; +#endif + + /// Check whether the given request Origin is permitted. Same-origin (matching the Host the + /// request was sent to) and requests without an Origin header are always allowed; any other + /// origin must be listed in allowed_origins. The caller passes the already-read Origin header. + bool is_request_origin_allowed_(AsyncWebServerRequest *request, const std::string &origin); private: #ifdef USE_SENSOR diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index b587841dfd..fc575d1c06 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -63,7 +63,7 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) if CORE.is_libretiny: CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"]) - if CORE.is_rp2040: + if CORE.is_rp2: # Ignore bundled AsyncTCP libraries - we use RPAsyncTCP from async_tcp component CORE.add_platformio_option( "lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"] diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index c7162c139a..9657853a73 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -59,7 +59,15 @@ class AuthMiddlewareHandler : public MiddlewareHandler { bool check_auth(AsyncWebServerRequest *request) { bool success = request->authenticate(credentials_->username.c_str(), credentials_->password.c_str()); if (!success) { + // The scheme is chosen at build time (USE_WEBSERVER_AUTH_DIGEST); the unused path is + // compiled out. On ESP32 our own server picks the scheme internally. +#if USE_ESP32 request->requestAuthentication(); +#elif defined(USE_WEBSERVER_AUTH_DIGEST) + request->requestAuthentication(nullptr, true); +#else + request->requestAuthentication(nullptr, false); +#endif } return success; } @@ -88,7 +96,7 @@ class AuthMiddlewareHandler : public MiddlewareHandler { } // namespace internal -class WebServerBase { +class WebServerBase final { public: void init() { if (this->initialized_) { diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index c32acaf03e..993fb6c035 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -16,6 +16,11 @@ #include "utils.h" #include "web_server_idf.h" +#ifdef USE_WEBSERVER_AUTH_DIGEST +#include +#include +#endif + #ifdef USE_WEBSERVER_OTA #include #include "multipart.h" // For parse_multipart_boundary and other utils @@ -32,15 +37,27 @@ namespace esphome::web_server_idf { +// Status strings not provided by esp_http_server.h +#ifndef HTTPD_401 +#define HTTPD_401 "401 Unauthorized" +#endif #ifndef HTTPD_409 #define HTTPD_409 "409 Conflict" #endif +#ifndef HTTPD_422 +#define HTTPD_422 "422 Unprocessable Entity" +#endif #define CRLF_STR "\r\n" #define CRLF_LEN (sizeof(CRLF_STR) - 1) static const char *const TAG = "web_server_idf"; +// Chunk size for streaming request bodies; matches the Arduino AsyncWebServer buffer size. +// Buffers of this size must live on the heap - the httpd task stack is too small. +static constexpr size_t RECV_CHUNK_SIZE = 1460; +static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024; // Yield every 16KB to prevent watchdog + // Global instance to avoid guard variable (saves 8 bytes) // This is initialized at program startup before any threads namespace { @@ -184,9 +201,10 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { return server->handle_multipart_upload_(r, content_type_char); #endif } else { - ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type_char); - // fallback to get handler to support backward compatibility - return AsyncWebServer::request_handler(r); + // Other content types (e.g. application/json) are delivered raw to a matching + // custom handler via handleBody(), like the Arduino AsyncWebServer does + auto *server = static_cast(r->user_ctx); + return server->handle_raw_body_(r, content_type_char); } } @@ -237,6 +255,51 @@ esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const return ESP_ERR_NOT_FOUND; } +esp_err_t AsyncWebServer::handle_raw_body_(httpd_req_t *r, const char *content_type) { + AsyncWebServerRequest req(r); + AsyncWebHandler *handler = nullptr; + for (auto *h : this->handlers_) { + if (h->canHandle(&req)) { + handler = h; + break; + } + } + + if (handler == nullptr) { + ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type); + // fallback to get handler to support backward compatibility + return this->request_handler_(&req); + } + + const size_t total = r->content_len; + if (total > 0) { + auto buffer = std::make_unique_for_overwrite(RECV_CHUNK_SIZE); + size_t bytes_since_yield = 0; + + for (size_t index = 0; index < total;) { + int recv_len = httpd_req_recv(r, buffer.get(), std::min(total - index, RECV_CHUNK_SIZE)); + + if (recv_len <= 0) { + httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST, + nullptr); + return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL; + } + + handler->handleBody(&req, reinterpret_cast(buffer.get()), recv_len, index, total); + index += recv_len; + bytes_since_yield += recv_len; + + if (bytes_since_yield > YIELD_INTERVAL_BYTES) { + vTaskDelay(1); + bytes_since_yield = 0; + } + } + } + + handler->handleRequest(&req); + return ESP_OK; +} + AsyncWebServerRequest::~AsyncWebServerRequest() { delete this->rsp_; for (auto *param : this->params_) { @@ -276,12 +339,24 @@ void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code case 200: status = HTTPD_200; break; + case 204: + status = HTTPD_204; + break; + case 400: + status = HTTPD_400; + break; + case 401: + status = HTTPD_401; + break; case 404: status = HTTPD_404; break; case 409: status = HTTPD_409; break; + case 422: + status = HTTPD_422; + break; default: status = HTTPD_500; break; @@ -302,6 +377,125 @@ void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code } #ifdef USE_WEBSERVER_AUTH + +#ifdef USE_WEBSERVER_AUTH_DIGEST +namespace { + +// Extract the value of a Digest auth parameter (e.g. "nonce") from the comma-separated +// parameter list. Values may be quoted or bare. Returns an empty ref when the key is absent. +// Only whole parameter names match, so "nc" does not match inside "cnonce". +StringRef digest_param(StringRef params, const char *key) { + size_t key_len = strlen(key); + const char *base = params.c_str(); + size_t n = params.size(); + size_t i = 0; + while (i < n) { + while (i < n && (base[i] == ' ' || base[i] == ',')) + i++; + size_t name_start = i; + while (i < n && base[i] != '=' && base[i] != ',') + i++; + if (i >= n || base[i] == ',') + continue; // token without a '=', skip it + size_t name_len = i - name_start; + while (name_len > 0 && base[name_start + name_len - 1] == ' ') + name_len--; + i++; // consume '=' + const char *val_start; + size_t val_len; + if (i < n && base[i] == '"') { + i++; + val_start = base + i; + while (i < n && base[i] != '"') + i++; + val_len = (base + i) - val_start; + if (i < n) + i++; // consume closing quote + } else { + val_start = base + i; + while (i < n && base[i] != ',') + i++; + val_len = (base + i) - val_start; + } + if (name_len == key_len && memcmp(base + name_start, key, key_len) == 0) + return StringRef(val_start, val_len); + while (i < n && base[i] != ',') + i++; + } + return StringRef(); +} + +// Verify an RFC 2617 Digest response. Stateless (the nonce we issued is not tracked), which +// matches the ESPAsyncWebServer backend used on the Arduino platforms. +bool check_digest_auth(const char *username, const char *password, const std::string &header, const char *method) { + const size_t prefix_len = sizeof("Digest ") - 1; + StringRef params(header.c_str() + prefix_len, header.size() - prefix_len); + + if (digest_param(params, "username") != username) + return false; + + StringRef realm = digest_param(params, "realm"); + StringRef nonce = digest_param(params, "nonce"); + StringRef uri = digest_param(params, "uri"); + StringRef qop = digest_param(params, "qop"); + StringRef nc = digest_param(params, "nc"); + StringRef cnonce = digest_param(params, "cnonce"); + StringRef response = digest_param(params, "response"); + if (response.size() != 32) + return false; + + // Compute the three MD5 hashes by streaming the pieces straight into the ROM MD5 engine, so + // nothing is concatenated on the heap. Each hash is emitted as 32 lowercase hex characters. + md5_context_t ctx; + uint8_t digest[16]; + + // HA1 = MD5(username:realm:password) -- uses the realm the client echoed back. + char ha1[33]; + esp_rom_md5_init(&ctx); + esp_rom_md5_update(&ctx, username, strlen(username)); + esp_rom_md5_update(&ctx, ":", 1); + esp_rom_md5_update(&ctx, realm.c_str(), realm.size()); + esp_rom_md5_update(&ctx, ":", 1); + esp_rom_md5_update(&ctx, password, strlen(password)); + esp_rom_md5_final(digest, &ctx); + format_hex_to(ha1, digest, sizeof(digest)); + + // HA2 = MD5(method:uri) -- uses the uri the client echoed back. + char ha2[33]; + esp_rom_md5_init(&ctx); + esp_rom_md5_update(&ctx, method, strlen(method)); + esp_rom_md5_update(&ctx, ":", 1); + esp_rom_md5_update(&ctx, uri.c_str(), uri.size()); + esp_rom_md5_final(digest, &ctx); + format_hex_to(ha2, digest, sizeof(digest)); + + // expected = MD5(HA1:nonce:nc:cnonce:qop:HA2) + char expected[33]; + esp_rom_md5_init(&ctx); + esp_rom_md5_update(&ctx, ha1, 32); + esp_rom_md5_update(&ctx, ":", 1); + esp_rom_md5_update(&ctx, nonce.c_str(), nonce.size()); + esp_rom_md5_update(&ctx, ":", 1); + esp_rom_md5_update(&ctx, nc.c_str(), nc.size()); + esp_rom_md5_update(&ctx, ":", 1); + esp_rom_md5_update(&ctx, cnonce.c_str(), cnonce.size()); + esp_rom_md5_update(&ctx, ":", 1); + esp_rom_md5_update(&ctx, qop.c_str(), qop.size()); + esp_rom_md5_update(&ctx, ":", 1); + esp_rom_md5_update(&ctx, ha2, 32); + esp_rom_md5_final(digest, &ctx); + format_hex_to(expected, digest, sizeof(digest)); + + // Constant-time comparison of the two 32-char hex digests. + uint8_t result = 0; + for (size_t i = 0; i < 32; i++) + result |= static_cast(expected[i] ^ response[i]); + return result == 0; +} + +} // namespace +#endif // USE_WEBSERVER_AUTH_DIGEST + bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const { if (username == nullptr || password == nullptr || *username == 0) { return true; @@ -313,9 +507,18 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw auto *auth_str = auth.value().c_str(); +#ifdef USE_WEBSERVER_AUTH_DIGEST + // The build fixed the scheme to Digest, so the Basic path is compiled out entirely. + const auto auth_prefix_len = sizeof("Digest ") - 1; + if (strncmp("Digest ", auth_str, auth_prefix_len) != 0) { + ESP_LOGW(TAG, "Only Digest authorization supported"); + return false; + } + return check_digest_auth(username, password, auth.value(), http_method_str(this->method())); +#else const auto auth_prefix_len = sizeof("Basic ") - 1; if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) { - ESP_LOGW(TAG, "Only Basic authorization supported yet"); + ESP_LOGW(TAG, "Only Basic authorization supported"); return false; } @@ -364,16 +567,33 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw result |= static_cast(digest[i] ^ provided_ch); } return result == 0; +#endif // USE_WEBSERVER_AUTH_DIGEST } -void AsyncWebServerRequest::requestAuthentication(const char *realm) const { +void AsyncWebServerRequest::requestAuthentication() const { httpd_resp_set_hdr(*this, "Connection", "keep-alive"); - // Note: realm is never configured in ESPHome, always nullptr -> "Login Required" - (void) realm; // Unused - always use default +#ifdef USE_WEBSERVER_AUTH_DIGEST + // Issue a fresh random nonce and opaque. The nonce is not stored, so this is stateless and + // does not defend against replay -- its purpose is to keep the password off the wire. + // The header value must stay alive until httpd_resp_send_err() below sends it, so the buffer + // lives on this stack frame (httpd_resp_set_hdr stores the pointer, it does not copy). + uint8_t random_bytes[16]; + char nonce[33]; + char opaque[33]; + char header[160]; + esp_fill_random(random_bytes, sizeof(random_bytes)); + format_hex_to(nonce, random_bytes, sizeof(random_bytes)); + esp_fill_random(random_bytes, sizeof(random_bytes)); + format_hex_to(opaque, random_bytes, sizeof(random_bytes)); + snprintf(header, sizeof(header), R"(Digest realm="Login Required", qop="auth", nonce="%s", opaque="%s")", nonce, + opaque); + httpd_resp_set_hdr(*this, "WWW-Authenticate", header); +#else httpd_resp_set_hdr(*this, "WWW-Authenticate", "Basic realm=\"Login Required\""); +#endif // USE_WEBSERVER_AUTH_DIGEST httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); } -#endif +#endif // USE_WEBSERVER_AUTH AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { // Check cache first - only successful lookups are cached @@ -546,10 +766,11 @@ void AsyncEventSource::adopt_pending_sessions_main_loop_() { } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) -void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { +void AsyncEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id, + uint32_t reconnect) { for (auto *ses : this->sessions_) { if (ses->fd_.load() != 0) { // Skip dead sessions - ses->try_send_nodefer(message, event, id, reconnect); + ses->try_send_nodefer(message, message_len, event, id, reconnect); } } } @@ -600,7 +821,7 @@ void AsyncEventSourceResponse::start_session_main_loop_() { // tcp send buffer is empty on connect, so these should always go through auto message = ws->get_config_json(); - this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); + this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { @@ -612,7 +833,7 @@ void AsyncEventSourceResponse::start_session_main_loop_() { // a (very) large number of these should be able to be queued initially without defer // since the only thing in the send buffer at this point is the initial ping/config - this->try_send_nodefer(message.c_str(), "sorting_group"); + this->try_send_nodefer(message.c_str(), message.size(), "sorting_group"); } #endif @@ -647,7 +868,7 @@ void AsyncEventSourceResponse::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); auto message = de.message_generator_(web_server_, de.source_); - if (this->try_send_nodefer(message.c_str(), "state")) { + if (this->try_send_nodefer(message.c_str(), message.size(), "state")) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); } else { @@ -718,7 +939,7 @@ void AsyncEventSourceResponse::loop() { this->entities_iterator_.advance(); } -bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id, +bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id, uint32_t reconnect) { if (this->fd_.load() == 0) { return false; @@ -764,19 +985,18 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char // Fast path: check if message contains any newlines at all // Most SSE messages (JSON state updates) have no newlines - const char *first_n = strchr(message, '\n'); - const char *first_r = strchr(message, '\r'); + const char *first_n = static_cast(memchr(message, '\n', message_len)); + const char *first_r = static_cast(memchr(message, '\r', message_len)); if (first_n == nullptr && first_r == nullptr) { // No newlines - fast path (most common case) event_buffer_.append("data: ", sizeof("data: ") - 1); - event_buffer_.append(message); + event_buffer_.append(message, message_len); event_buffer_.append(CRLF_STR CRLF_STR, CRLF_LEN * 2); // data line + blank line terminator } else { // Has newlines - handle multi-line message const char *line_start = message; - size_t msg_len = strlen(message); - const char *msg_end = message + msg_len; + const char *msg_end = message + message_len; // Reuse the first search results const char *next_n = first_n; @@ -789,7 +1009,7 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char if (next_n == nullptr && next_r == nullptr) { // No more line breaks - output remaining text as final line event_buffer_.append("data: ", sizeof("data: ") - 1); - event_buffer_.append(line_start); + event_buffer_.append(line_start, msg_end - line_start); event_buffer_.append(CRLF_STR, CRLF_LEN); break; } @@ -828,8 +1048,8 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char } // Search for next newlines only in remaining string - next_n = strchr(line_start, '\n'); - next_r = strchr(line_start, '\r'); + next_n = static_cast(memchr(line_start, '\n', msg_end - line_start)); + next_r = static_cast(memchr(line_start, '\r', msg_end - line_start)); } // Terminate message with blank line @@ -884,7 +1104,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e deq_push_back_with_dedup_(source, message_generator); } else { auto message = message_generator(web_server_, source); - if (!this->try_send_nodefer(message.c_str(), "state")) { + if (!this->try_send_nodefer(message.c_str(), message.size(), "state")) { deq_push_back_with_dedup_(source, message_generator); } } @@ -893,9 +1113,6 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e #ifdef USE_WEBSERVER_OTA esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) { - static constexpr size_t MULTIPART_CHUNK_SIZE = 1460; // Match Arduino AsyncWebServer buffer size - static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024; // Yield every 16KB to prevent watchdog - // Parse boundary and create reader const char *boundary_start; size_t boundary_len; @@ -949,12 +1166,11 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c } }); - // Use heap buffer - 1460 bytes is too large for the httpd task stack - auto buffer = std::make_unique_for_overwrite(MULTIPART_CHUNK_SIZE); + auto buffer = std::make_unique_for_overwrite(RECV_CHUNK_SIZE); size_t bytes_since_yield = 0; for (size_t remaining = r->content_len; remaining > 0;) { - int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE)); + int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, RECV_CHUNK_SIZE)); if (recv_len <= 0) { httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST, diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index c622d53e89..baa55898bb 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -129,7 +129,7 @@ class AsyncWebServerRequest { #ifdef USE_WEBSERVER_AUTH bool authenticate(const char *username, const char *password) const; // NOLINTNEXTLINE(readability-identifier-naming) - void requestAuthentication(const char *realm = nullptr) const; + void requestAuthentication() const; #endif void redirect(const std::string &url); @@ -233,6 +233,7 @@ class AsyncWebServer { static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd); + esp_err_t handle_raw_body_(httpd_req_t *r, const char *content_type); #ifdef USE_WEBSERVER_OTA esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); #endif @@ -291,7 +292,8 @@ class AsyncEventSourceResponse { friend class AsyncEventSource; public: - bool try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + bool try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0, + uint32_t reconnect = 0); void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); void loop(); @@ -343,7 +345,8 @@ class AsyncEventSource : public AsyncWebHandler { // NOLINTNEXTLINE(readability-identifier-naming) void onConnect(connect_handler_t &&cb) { this->on_connect_ = std::move(cb); } - void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0, + uint32_t reconnect = 0); void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); /// Returns true if there are sessions remaining (including pending cleanup). bool loop(); diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 6f38f58318..02a39d3c84 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -381,6 +381,15 @@ class WeikaiChannel : public uart::UARTComponent { /// we wait until all bytes are gone with a timeout of 100 ms uart::UARTFlushResult flush() override; +#if defined(USE_ESP8266) || defined(USE_ESP32) + /// @brief Re-apply the current line settings (baud, parity, etc) to the channel. + void load_settings(bool dump_config) override { + this->set_line_param_(); + this->set_baudrate_(); + } + using UARTComponent::load_settings; // also bring in the no-arg overload for convenience +#endif + protected: friend class WeikaiComponent; diff --git a/esphome/components/weikai_i2c/weikai_i2c.h b/esphome/components/weikai_i2c/weikai_i2c.h index 940dbad9f2..6d8da031ac 100644 --- a/esphome/components/weikai_i2c/weikai_i2c.h +++ b/esphome/components/weikai_i2c/weikai_i2c.h @@ -38,7 +38,7 @@ class WeikaiRegisterI2C : public weikai::WeikaiRegister { /// @brief The WeikaiComponentI2C class stores the information to the WeiKai component /// connected through an I2C bus. //////////////////////////////////////////////////////////////////////////////////// -class WeikaiComponentI2C : public weikai::WeikaiComponent, public i2c::I2CDevice { +class WeikaiComponentI2C final : public weikai::WeikaiComponent, public i2c::I2CDevice { public: weikai::WeikaiRegister ®(uint8_t reg, uint8_t channel) override { reg_i2c_.register_ = reg; diff --git a/esphome/components/weikai_spi/weikai_spi.h b/esphome/components/weikai_spi/weikai_spi.h index 3b581ef44c..cdfa148c24 100644 --- a/esphome/components/weikai_spi/weikai_spi.h +++ b/esphome/components/weikai_spi/weikai_spi.h @@ -31,9 +31,9 @@ class WeikaiRegisterSPI : public weikai::WeikaiRegister { /// @brief The WeikaiComponentSPI class stores the information to the WeiKai component /// connected through an SPI bus. //////////////////////////////////////////////////////////////////////////////////// -class WeikaiComponentSPI : public weikai::WeikaiComponent, - public spi::SPIDevice { +class WeikaiComponentSPI final : public weikai::WeikaiComponent, + public spi::SPIDevice { public: weikai::WeikaiRegister ®(uint8_t reg, uint8_t channel) override { reg_spi_.register_ = reg; diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index 03b4cf21a8..b705ee95fa 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -16,7 +16,7 @@ const float WHIRLPOOL_DG11J1_3A_TEMP_MIN = 18.0; const float WHIRLPOOL_DG11J1_91_TEMP_MAX = 30.0; const float WHIRLPOOL_DG11J1_91_TEMP_MIN = 16.0; -class WhirlpoolClimate : public climate_ir::ClimateIR { +class WhirlpoolClimate final : public climate_ir::ClimateIR { public: WhirlpoolClimate(); diff --git a/esphome/components/whynter/whynter.h b/esphome/components/whynter/whynter.h index d67bfa8fa0..fa8f201b05 100644 --- a/esphome/components/whynter/whynter.h +++ b/esphome/components/whynter/whynter.h @@ -12,7 +12,7 @@ const uint8_t TEMP_MAX_C = 32; // Celsius const uint8_t TEMP_MIN_F = 61; // Fahrenheit const uint8_t TEMP_MAX_F = 89; // Fahrenheit -class Whynter : public climate_ir::ClimateIR { +class Whynter final : public climate_ir::ClimateIR { public: Whynter() : climate_ir::ClimateIR(TEMP_MIN_C, TEMP_MAX_C, 1.0, true, true, diff --git a/esphome/components/wiegand/wiegand.h b/esphome/components/wiegand/wiegand.h index 33d81ba086..079f02ed68 100644 --- a/esphome/components/wiegand/wiegand.h +++ b/esphome/components/wiegand/wiegand.h @@ -21,13 +21,13 @@ struct WiegandStore { static void d1_gpio_intr(WiegandStore *arg); }; -class WiegandTagTrigger : public Trigger {}; +class WiegandTagTrigger final : public Trigger {}; -class WiegandRawTrigger : public Trigger {}; +class WiegandRawTrigger final : public Trigger {}; -class WiegandKeyTrigger : public Trigger {}; +class WiegandKeyTrigger final : public Trigger {}; -class Wiegand : public key_provider::KeyProvider, public Component { +class Wiegand final : public key_provider::KeyProvider, public Component { public: float get_setup_priority() const override { return setup_priority::HARDWARE; } void setup() override; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 512fd63e12..4bb6629da1 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,10 +1,11 @@ import logging import math +from typing import Any -from esphome import automation +from esphome import automation, preferences from esphome.automation import Condition import esphome.codegen as cg -from esphome.components.const import CONF_USE_PSRAM +from esphome.components.const import CONF_ENABLED, CONF_USE_PSRAM from esphome.components.esp32 import ( add_idf_sdkconfig_option, const, @@ -13,6 +14,8 @@ from esphome.components.esp32 import ( request_wifi, ) from esphome.components.network import ( + add_use_address, + get_network_priority, has_high_performance_networking, ip_address_literal, ) @@ -50,6 +53,7 @@ from esphome.const import ( CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, + CONF_STORAGE, CONF_SUBNET, CONF_TIMEOUT, CONF_TTLS_PHASE_2, @@ -141,8 +145,8 @@ def has_native_wifi( """ if platform == Platform.ESP32: return variant_has_wifi(variant) if variant else True - if platform == Platform.RP2040: - from esphome.components.rp2040 import board_id_has_wifi + if platform == Platform.RP2: + from esphome.components.rp2 import board_id_has_wifi return board_id_has_wifi(board) if board else True return platform in _WIFI_FIRST_PLATFORMS @@ -299,7 +303,7 @@ def wifi_network_ap(value): if value is None: value = {} config = WIFI_NETWORK_AP(value) - if CONF_MANUAL_IP in config and CORE.is_rp2040: + if CONF_MANUAL_IP in config and CORE.is_rp2: raise cv.Invalid( "Manual AP IP configuration is not supported on RP2040. " "The AP uses the default IP 192.168.4.1" @@ -322,8 +326,8 @@ def validate_variant(_): variant = get_esp32_variant() if variant in NO_WIFI_VARIANTS and "esp32_hosted" not in fv.full_config.get(): raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") - if CORE.is_rp2040: - from esphome.components.rp2040 import board_has_wifi, get_board + if CORE.is_rp2: + from esphome.components.rp2 import board_has_wifi, get_board if not board_has_wifi(): raise cv.Invalid( @@ -367,7 +371,7 @@ def _consume_wifi_sockets(config: ConfigType) -> ConfigType: DHCP/DNS). On ESP32, CONFIG_LWIP_MAX_SOCKETS only controls the POSIX socket layer — DHCP/DNS use raw udp_new() which bypasses it entirely. """ - if not (CORE.is_bk72xx or CORE.is_rtl87xx or CORE.is_ln882x or CORE.is_rp2040): + if not (CORE.is_bk72xx or CORE.is_rtl87xx or CORE.is_ln882x or CORE.is_rp2): return config from esphome.components import socket @@ -433,7 +437,39 @@ def _validate(config): return config +def _report_provisioning_credentials(config): + """Report baked-in STA credentials to the provisioning component (if used). + + `_validate` has already folded any ``ssid``/``password`` into ``networks``, so a + non-empty list means credentials are set in the config. `provisioning:` warns + about this, since a device that uses a provisioning window should get its + credentials on first connection instead. + """ + if config.get(CONF_NETWORKS): + from esphome.components import provisioning + + provisioning.report_hardcoded_credentials("wifi") + return config + + CONF_PASSIVE_SCAN = "passive_scan" + +FAST_CONNECT_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ENABLED, default=True): cv.boolean, + **preferences.storage_schema(), + } +) + + +def _fast_connect_schema(value: Any) -> ConfigType: + """Accept the historic plain boolean (including boolean-like strings from + substitutions) or a dict with enabled/storage keys.""" + if not isinstance(value, dict): + value = {CONF_ENABLED: value} + return FAST_CONNECT_SCHEMA(value) + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -454,12 +490,12 @@ CONFIG_SCHEMA = cv.All( CONF_POWER_SAVE_MODE, esp8266="none", esp32="light", - rp2040="light", + rp2="light", bk72xx="none", rtl87xx="none", ln882x="light", ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), - cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, + cv.Optional(CONF_FAST_CONNECT, default=False): _fast_connect_schema, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_MIN_AUTH_MODE): cv.All( VALIDATE_WIFI_MIN_AUTH_MODE, @@ -497,6 +533,7 @@ CONFIG_SCHEMA = cv.All( ), _apply_min_auth_mode_default, _validate, + _report_provisioning_credentials, ) @@ -566,7 +603,11 @@ def wifi_network(config, ap, static_ip): @coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) + + prio = get_network_priority("wifi") + if prio is not None: + cg.set_setup_priority(var, prio) + add_use_address(var, config[CONF_USE_ADDRESS]) # Track if any network uses Enterprise authentication has_eap = False @@ -619,8 +660,14 @@ async def to_code(config): cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) if CONF_MIN_AUTH_MODE in config: cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) - if config[CONF_FAST_CONNECT]: + fast_connect = config[CONF_FAST_CONNECT] + if fast_connect[CONF_ENABLED]: cg.add_define("USE_WIFI_FAST_CONNECT") + # The storage default preserves this preference's historic location: + # ESP8266 has always used RTC memory; every other platform effectively + # used flash (the in_flash flag was previously ignored outside ESP8266). + if preferences.is_in_flash(fast_connect[CONF_STORAGE]): + cg.add_define("USE_WIFI_FAST_CONNECT_IN_FLASH") # passive_scan defaults to false in C++ - only set if true if config[CONF_PASSIVE_SCAN]: cg.add(var.set_passive_scan(True)) @@ -651,7 +698,7 @@ async def to_code(config): if CONF_PHY_MODE in config: cg.add_define("USE_WIFI_PHY_MODE") cg.add(var.set_phy_mode(config[CONF_PHY_MODE])) - elif CORE.is_rp2040: + elif CORE.is_rp2: cg.add_library("WiFi", None) if CORE.is_esp32: @@ -776,6 +823,7 @@ IP_STATE_LISTENERS_KEY = "wifi_ip_state_listeners" SCAN_RESULTS_LISTENERS_KEY = "wifi_scan_results_listeners" CONNECT_STATE_LISTENERS_KEY = "wifi_connect_state_listeners" POWER_SAVE_LISTENERS_KEY = "wifi_power_save_listeners" +SCAN_RESULTS_LOCK_KEY = "wifi_scan_results_lock" def request_wifi_scan_results(): @@ -788,6 +836,19 @@ def request_wifi_scan_results(): CORE.data[KEEP_SCAN_RESULTS_KEY] = True +def request_wifi_scan_results_lock() -> None: + """Request that scan results be guarded by a lock for cross-task readers. + + Components that read WiFi scan results from a task other than the main loop + (for example a web server handler) must call this function during their code + generation, and their C++ code must hold a wifi::ScanResultsLock while + iterating get_scan_result(). On multi-threaded platforms this compiles in a + lock that scan result writers hold; on single-threaded platforms it compiles + to nothing. + """ + CORE.data[SCAN_RESULTS_LOCK_KEY] = True + + def enable_runtime_power_save_control(): """Enable runtime WiFi power save control. @@ -849,6 +910,8 @@ async def final_step(): cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") if CORE.data.get(RUNTIME_ROAMING_SUPPRESSION_KEY, False): cg.add_define("USE_WIFI_RUNTIME_ROAMING_SUPPRESSION") + if CORE.data.get(SCAN_RESULTS_LOCK_KEY): + cg.add_define("USE_WIFI_SCAN_RESULTS_LOCK") # Generate listener defines - each listener type has its own #ifdef ip_state_count = CORE.data.get(IP_STATE_LISTENERS_KEY, 0) @@ -919,7 +982,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO}, + "wifi_component_pico_w.cpp": {PlatformFramework.RP2_ARDUINO}, } ) diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h index 1ad69b3992..e63faa18ab 100644 --- a/esphome/components/wifi/automation.h +++ b/esphome/components/wifi/automation.h @@ -6,32 +6,32 @@ namespace esphome::wifi { -template class WiFiConnectedCondition : public Condition { +template class WiFiConnectedCondition final : public Condition { public: bool check(const Ts &...x) override { return global_wifi_component->is_connected(); } }; -template class WiFiEnabledCondition : public Condition { +template class WiFiEnabledCondition final : public Condition { public: bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } }; -template class WiFiAPActiveCondition : public Condition { +template class WiFiAPActiveCondition final : public Condition { public: bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); } }; -template class WiFiEnableAction : public Action { +template class WiFiEnableAction final : public Action { public: void play(const Ts &...x) override { global_wifi_component->enable(); } }; -template class WiFiDisableAction : public Action { +template class WiFiDisableAction final : public Action { public: void play(const Ts &...x) override { global_wifi_component->disable(); } }; -template class WiFiConfigureAction : public Action, public Component { +template class WiFiConfigureAction final : public Action, public Component { public: TEMPLATABLE_VALUE(std::string, ssid) TEMPLATABLE_VALUE(std::string, password) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index ffc6ea8e14..650b06cae1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -45,6 +45,10 @@ #include "esphome/components/improv_serial/improv_serial_component.h" #endif +#ifdef USE_PROVISIONING +#include "esphome/components/provisioning/provisioning.h" +#endif + namespace esphome::wifi { static const char *const TAG = "wifi"; @@ -649,7 +653,13 @@ void WiFiComponent::start() { this->pref_ = global_preferences->make_preference(hash, true); #ifdef USE_WIFI_FAST_CONNECT - this->fast_connect_pref_ = global_preferences->make_preference(hash + 1, false); +#ifdef USE_WIFI_FAST_CONNECT_IN_FLASH + const bool fast_connect_in_flash = true; +#else + const bool fast_connect_in_flash = false; +#endif + this->fast_connect_pref_ = + global_preferences->make_preference(hash + 1, fast_connect_in_flash); #endif SavedWifiSettings save{}; @@ -866,8 +876,20 @@ void WiFiComponent::loop() { if (!this->has_ap() && this->reboot_timeout_ != 0) { if (now - this->last_connected_ > this->reboot_timeout_) { - ESP_LOGE(TAG, "Can't connect; rebooting"); - App.reboot(); + bool suppress = false; +#ifdef USE_PROVISIONING + // Don't reboot while a provisioning window is pending (device unprovisioned). + // The device is legitimately waiting to be onboarded (Wi-Fi must come up + // before the controller can set credentials), and an auto-reboot would reopen + // the window without the deliberate power cycle / reset that is meant to be + // required. Resumes normal reboot behavior once provisioned. + suppress = provisioning::global_provisioning_manager != nullptr && + provisioning::global_provisioning_manager->window_pending(); +#endif + if (!suppress) { + ESP_LOGE(TAG, "Can't connect; rebooting"); + App.reboot(); + } } } } @@ -1461,23 +1483,26 @@ void WiFiComponent::check_scanning_finished() { } ESP_LOGD(TAG, "Found networks:"); - for (auto &res : this->scan_result_) { - for (auto &ap : this->sta_) { - if (res.matches(ap)) { - res.set_matches(true); - // Cache priority lookup - do single search instead of 2 separate searches - const bssid_t &bssid = res.get_bssid(); - if (!this->has_sta_priority(bssid)) { - this->set_sta_priority(bssid, ap.get_priority()); + { + ScanResultsLock lock(this); + for (auto &res : this->scan_result_) { + for (auto &ap : this->sta_) { + if (res.matches(ap)) { + res.set_matches(true); + // Cache priority lookup - do single search instead of 2 separate searches + const bssid_t &bssid = res.get_bssid(); + if (!this->has_sta_priority(bssid)) { + this->set_sta_priority(bssid, ap.get_priority()); + } + res.set_priority(this->get_sta_priority(bssid)); + break; } - res.set_priority(this->get_sta_priority(bssid)); - break; } } - } - // Sort scan results using insertion sort for better memory efficiency - insertion_sort_scan_results(this->scan_result_); + // Sort scan results using insertion sort for better memory efficiency + insertion_sort_scan_results(this->scan_result_); + } // Log matching networks (non-matching already logged at VERBOSE in scan callback) for (auto &res : this->scan_result_) { @@ -1863,11 +1888,13 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { // Phase-specific setup switch (new_phase) { #ifdef USE_WIFI_FAST_CONNECT - case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: { // Move to next configured AP - clear old scan data so new AP is tried with config only this->selected_sta_index_++; + ScanResultsLock lock(this); this->scan_result_.clear(); break; + } #endif case WiFiRetryPhase::EXPLICIT_HIDDEN: @@ -2382,7 +2409,8 @@ void WiFiComponent::clear_roaming_state_() { void WiFiComponent::release_scan_results_() { if (!this->keep_scan_results_) { -#if defined(USE_RP2040) || defined(USE_ESP32) + ScanResultsLock lock(this); +#if defined(USE_RP2) || defined(USE_ESP32) // std::vector - use swap trick since shrink_to_fit is non-binding decltype(this->scan_result_)().swap(this->scan_result_); #else diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index c774e3a68e..43e44a135f 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -46,7 +46,7 @@ extern "C" { #endif #endif -#ifdef USE_RP2040 +#ifdef USE_RP2 extern "C" { #include "cyw43.h" #include "cyw43_country.h" @@ -181,12 +181,19 @@ static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8; // Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API) // Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible -#if defined(USE_RP2040) || defined(USE_ESP32) +#if defined(USE_RP2) || defined(USE_ESP32) template using wifi_scan_vector_t = std::vector; #else template using wifi_scan_vector_t = FixedVector; #endif +// A consumer component (e.g. the captive portal) reads scan results from another +// task; guard them with a real lock only on platforms that actually run multiple +// threads. See ScanResultsLock below the WiFiComponent class. +#if defined(USE_WIFI_SCAN_RESULTS_LOCK) && !defined(ESPHOME_THREAD_SINGLE) +#define WIFI_SCAN_RESULTS_LOCK_ENABLED +#endif + /// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated. /// Used internally for WiFi SSID/password storage to reduce heap fragmentation. class CompactString { @@ -501,9 +508,14 @@ class WiFiComponent final : public Component { network::IPAddress get_dns_address(int num); network::IPAddresses get_ip_addresses(); + /// Returns nullptr when no explicit use_address is configured and the address is + /// derived at runtime from the device name (see network::get_use_address_to()). const char *get_use_address() const { return this->use_address_; } void set_use_address(const char *use_address) { this->use_address_ = use_address; } + /// Main-loop callers may read this directly. Callers on any other task must + /// hold a ScanResultsLock for the whole iteration and must call + /// wifi.request_wifi_scan_results_lock() from their code generation. const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } network::IPAddress wifi_soft_ap_ip(); @@ -815,9 +827,11 @@ class WiFiComponent final : public Component { friend void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); #endif -#ifdef USE_RP2040 + friend class ScanResultsLock; + +#ifdef USE_RP2 static int s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); - void wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); + void wifi_scan_result_(void *env, const cyw43_ev_scan_result_t *result); #endif #ifdef USE_LIBRETINY @@ -829,7 +843,11 @@ class WiFiComponent final : public Component { // Large/pointer-aligned members first FixedVector sta_; std::vector sta_priorities_; + // Guarded by ScanResultsLock (see below this class) wifi_scan_vector_t scan_result_; +#ifdef WIFI_SCAN_RESULTS_LOCK_ENABLED + Mutex scan_result_lock_; +#endif #ifdef USE_WIFI_AP WiFiAP ap_; #endif @@ -996,10 +1014,30 @@ class WiFiComponent final : public Component { private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. - const char *use_address_{""}; + const char *use_address_{nullptr}; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +/// Guards WiFiComponent::scan_result_. Invariant: every mutation and every read +/// from outside the main loop holds this lock, and holders only do bounded work +/// (never unbounded waits or network sends). On every platform where the lock is +/// enabled (ESP32, LibreTiny) scan-done events are drained from the event queue +/// on the main loop, so all writers are main-loop there and main-loop reads take +/// no lock. Single-threaded platforms write from driver context and the lock is +/// a no-op. Compiles to nothing unless a cross-task reader is in the build and +/// the platform is multi-threaded (WIFI_SCAN_RESULTS_LOCK_ENABLED). +class ScanResultsLock { + public: +#ifdef WIFI_SCAN_RESULTS_LOCK_ENABLED + ScanResultsLock(WiFiComponent *parent) : guard_(parent->scan_result_lock_) {} + + private: + LockGuard guard_; +#else + ScanResultsLock(WiFiComponent *) {} +#endif +}; + } // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 84b864c0c5..e082b2c8c1 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -733,6 +733,8 @@ void WiFiComponent::s_wifi_scan_done_callback(void *arg, STATUS status) { } void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { + // Compiles to nothing here; kept so every scan_result_ mutation holds the lock + ScanResultsLock lock(this); this->scan_result_.clear(); if (status != OK) { diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 2ade015a25..d78cd21380 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -891,65 +891,65 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { const auto &it = data->data.sta_scan_done; ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); - scan_result_.clear(); - this->scan_done_ = true; - if (it.status != 0) { - // scan error - return; - } - - if (it.number == 0) { - // no results - return; - } - uint16_t number = it.number; bool needs_full = this->needs_full_scan_results_(); + { + // Mutate in place under the lock; blocking a portal request is fine and + // avoids scratch buffers + ScanResultsLock lock(this); + this->scan_result_.clear(); + this->scan_done_ = true; + if (it.status != 0) { + // scan error + return; + } - // Smart reserve: full capacity if needed, small reserve otherwise - if (needs_full) { - this->scan_result_.reserve(number); - } else { - this->scan_result_.reserve(WIFI_SCAN_RESULT_FILTERED_RESERVE); - } + if (number == 0) { + // no results + return; + } + + // Smart reserve: full capacity if needed, small reserve otherwise + this->scan_result_.reserve(needs_full ? number : WIFI_SCAN_RESULT_FILTERED_RESERVE); #ifdef USE_ESP32_HOSTED - // getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor - // Presumably an upstream bug, work-around by getting all records at once - // Use stack buffer (3904 bytes / ~80 bytes per record = ~48 records) with heap fallback - static constexpr size_t SCAN_RECORD_STACK_COUNT = 3904 / sizeof(wifi_ap_record_t); - SmallBufferWithHeapFallback records(number); - err = esp_wifi_scan_get_ap_records(&number, records.get()); - if (err != ESP_OK) { - esp_wifi_clear_ap_list(); - ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err)); - return; - } - for (uint16_t i = 0; i < number; i++) { - wifi_ap_record_t &record = records.get()[i]; -#else - // Process one record at a time to avoid large buffer allocation - for (uint16_t i = 0; i < number; i++) { - wifi_ap_record_t record; - err = esp_wifi_scan_get_ap_record(&record); + // getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor + // Presumably an upstream bug, work-around by getting all records at once + // Use stack buffer (3904 bytes / ~80 bytes per record = ~48 records) with heap fallback + static constexpr size_t SCAN_RECORD_STACK_COUNT = 3904 / sizeof(wifi_ap_record_t); + SmallBufferWithHeapFallback records(number); + err = esp_wifi_scan_get_ap_records(&number, records.get()); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err)); - esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved - break; + esp_wifi_clear_ap_list(); + ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err)); + return; } + for (uint16_t i = 0; i < number; i++) { + wifi_ap_record_t &record = records.get()[i]; +#else + // Process one record at a time to avoid large buffer allocation + for (uint16_t i = 0; i < number; i++) { + wifi_ap_record_t record; + err = esp_wifi_scan_get_ap_record(&record); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err)); + esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved + break; + } #endif // USE_ESP32_HOSTED - // Check C string first - avoid std::string construction for non-matching networks - const char *ssid_cstr = reinterpret_cast(record.ssid); + // Check C string first - avoid std::string construction for non-matching networks + const char *ssid_cstr = reinterpret_cast(record.ssid); - // Only construct std::string and store if needed - if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) { - bssid_t bssid; - std::copy(record.bssid, record.bssid + 6, bssid.begin()); - this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, - record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); - } else { - this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); + // Only construct std::string and store if needed + if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) { + bssid_t bssid; + std::copy(record.bssid, record.bssid + 6, bssid.begin()); + this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, + record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); + } else { + this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); + } } } ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(), diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 59efa4f842..ce9c4eb6ce 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -657,44 +657,48 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { return true; } void WiFiComponent::wifi_scan_done_callback_() { - this->scan_result_.clear(); - this->scan_done_ = true; - int16_t num = WiFi.scanComplete(); - if (num < 0) - return; - bool needs_full = this->needs_full_scan_results_(); + { + // Mutate in place under the lock; blocking a portal request is fine and + // avoids scratch buffers + ScanResultsLock lock(this); + this->scan_result_.clear(); + this->scan_done_ = true; - // Access scan results directly via WiFi.scan struct to avoid Arduino String allocations - // WiFi.scan is public in LibreTiny for WiFiEvents & WiFiScan static handlers - auto *scan = WiFi.scan; + if (num < 0) + return; - // First pass: count matching networks - size_t count = 0; - for (int i = 0; i < num; i++) { - const char *ssid_cstr = scan->ap[i].ssid; - if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) { - count++; + // Access scan results directly via WiFi.scan struct to avoid Arduino String allocations + // WiFi.scan is public in LibreTiny for WiFiEvents & WiFiScan static handlers + auto *scan = WiFi.scan; + + // First pass: count matching networks + size_t count = 0; + for (int i = 0; i < num; i++) { + const char *ssid_cstr = scan->ap[i].ssid; + if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) { + count++; + } + } + + this->scan_result_.init(count); // Exact allocation + + // Second pass: store matching networks + for (int i = 0; i < num; i++) { + const char *ssid_cstr = scan->ap[i].ssid; + auto &ap = scan->ap[i]; + if (needs_full || this->matches_configured_network_(ssid_cstr, ap.bssid.addr)) { + this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3], + ap.bssid.addr[4], ap.bssid.addr[5]}, + ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, + ssid_cstr[0] == '\0'); + } else { + this->log_discarded_scan_result_(ssid_cstr, ap.bssid.addr, ap.rssi, ap.channel); + } } } - this->scan_result_.init(count); // Exact allocation - - // Second pass: store matching networks - for (int i = 0; i < num; i++) { - const char *ssid_cstr = scan->ap[i].ssid; - if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) { - auto &ap = scan->ap[i]; - this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3], - ap.bssid.addr[4], ap.bssid.addr[5]}, - ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, - ssid_cstr[0] == '\0'); - } else { - auto &ap = scan->ap[i]; - this->log_discarded_scan_result_(ssid_cstr, ap.bssid.addr, ap.rssi, ap.channel); - } - } ESP_LOGV(TAG, "Scan complete: %d found, %zu stored%s", num, this->scan_result_.size(), needs_full ? "" : " (filtered)"); WiFi.scanDelete(); diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 596fd2729b..69af9e9a4e 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -1,7 +1,7 @@ #include "wifi_component.h" #ifdef USE_WIFI -#ifdef USE_RP2040 +#ifdef USE_RP2 #include @@ -109,10 +109,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // setup depends on begin() succeeding. beginNoBlock() skips the outer wait loop, saving // up to 20 additional seconds of blocking per attempt. auto ret = WiFi.beginNoBlock(ap.ssid_.c_str(), ap.password_.c_str()); - if (ret == WL_IDLE_STATUS) - return false; - - return true; + return ret != WL_IDLE_STATUS; } bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); } @@ -169,11 +166,11 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { } int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { - global_wifi_component->wifi_scan_result(env, result); + global_wifi_component->wifi_scan_result_(env, result); return 0; } -void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { +void WiFiComponent::wifi_scan_result_(void *env, const cyw43_ev_scan_result_t *result) { s_scan_result_count++; // CYW43 scan results have ssid as a 32-byte buffer that is NOT null-terminated. @@ -193,12 +190,16 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re std::copy(result->bssid, result->bssid + 6, bssid.begin()); WiFiScanResult res(bssid, ssid_buf, len, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, len == 0); + // Compiles to nothing here; kept so every scan_result_ mutation holds the lock + ScanResultsLock lock(this); if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { this->scan_result_.push_back(res); } } bool WiFiComponent::wifi_scan_start_(bool passive) { + // Compiles to nothing here; kept so every scan_result_ mutation holds the lock + ScanResultsLock lock(this); this->scan_result_.clear(); this->scan_done_ = false; s_scan_result_count = 0; @@ -282,7 +283,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { // Filter out AP interface addresses — addrList includes all lwIP netifs. // The AP netif IP lingers even after the AP radio is disabled. IPAddress ap_ip = WiFi.softAPIP(); - for (auto addr : addrList) { + for (const auto &addr : addrList) { IPAddress ip(addr.ipFromNetifNum()); if (ip == ap_ip) { continue; @@ -351,12 +352,11 @@ bool WiFiComponent::wifi_loop_() { // Detect IP address changes (only when connected) if (is_connected) { - bool has_ip = false; - // Check for any IP address (IPv4 or IPv6) - for (auto addr : addrList) { - has_ip = true; - break; - } + // Check for any IP address (IPv4 or IPv6). The iterator comparison + // operators take non-const references, so the temporaries need names. + auto addr_it = addrList.begin(); + auto addr_end = addrList.end(); + bool has_ip = addr_it != addr_end; if (has_ip && !s_sta_had_ip) { // Just got IP address diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py index 51971a1220..089a4fa99a 100644 --- a/esphome/components/wifi/wpa2_eap.py +++ b/esphome/components/wifi/wpa2_eap.py @@ -58,7 +58,7 @@ def wrapped_load_pem_private_key(value, password): def read_relative_config_path(value): # pylint: disable=unspecified-encoding - return Path(CORE.relative_config_path(value)).read_text() + return Path(CORE.relative_config_path(value)).read_text(encoding="utf-8") def _validate_load_certificate(value): diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h index 9ff4cc54a0..af41465e71 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.h +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -10,9 +10,9 @@ namespace esphome::wifi_signal { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS -class WiFiSignalSensor : public sensor::Sensor, public PollingComponent, public wifi::WiFiConnectStateListener { +class WiFiSignalSensor final : public sensor::Sensor, public PollingComponent, public wifi::WiFiConnectStateListener { #else -class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { +class WiFiSignalSensor final : public sensor::Sensor, public PollingComponent { #endif public: #ifdef USE_WIFI_CONNECT_STATE_LISTENERS diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index e128b8476d..ea9e5a3b0c 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -6,7 +6,17 @@ import esphome.codegen as cg from esphome.components import time from esphome.components.esp32 import CORE, add_idf_sdkconfig_option import esphome.config_validation as cv -from esphome.const import CONF_ADDRESS, CONF_ID, CONF_REBOOT_TIMEOUT, CONF_TIME_ID +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_REBOOT_TIMEOUT, + CONF_TIME_ID, + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_LN882X, + PLATFORM_RTL87XX, +) from esphome.core import TimePeriod CONF_NETMASK = "netmask" @@ -57,30 +67,41 @@ def _cidr_network(value): return value -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(Wireguard), - cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Required(CONF_ADDRESS): cv.ipv4address, - cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4address, - cv.Required(CONF_PRIVATE_KEY): _wireguard_key, - cv.Required(CONF_PEER_ENDPOINT): cv.string, - cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key, - cv.Optional(CONF_PEER_PORT, default=51820): cv.port, - cv.Optional(CONF_PEER_PRESHARED_KEY): _wireguard_key, - cv.Optional(CONF_PEER_ALLOWED_IPS, default=["0.0.0.0/0"]): cv.ensure_list( - _cidr_network - ), - cv.Optional(CONF_PEER_PERSISTENT_KEEPALIVE, default="0s"): cv.All( - cv.positive_time_period_seconds, - cv.Range(max=TimePeriod(seconds=65535)), - ), - cv.Optional( - CONF_REBOOT_TIMEOUT, default="15min" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_REQUIRE_CONNECTION_TO_PROCEED, default=False): cv.boolean, - } -).extend(cv.polling_component_schema("10s")) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Wireguard), + cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Required(CONF_ADDRESS): cv.ipv4address, + cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4address, + cv.Required(CONF_PRIVATE_KEY): cv.sensitive(_wireguard_key), + cv.Required(CONF_PEER_ENDPOINT): cv.string, + cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key, + cv.Optional(CONF_PEER_PORT, default=51820): cv.port, + cv.Optional(CONF_PEER_PRESHARED_KEY): cv.sensitive(_wireguard_key), + cv.Optional(CONF_PEER_ALLOWED_IPS, default=["0.0.0.0/0"]): cv.ensure_list( + _cidr_network + ), + cv.Optional(CONF_PEER_PERSISTENT_KEEPALIVE, default="0s"): cv.All( + cv.positive_time_period_seconds, + cv.Range(max=TimePeriod(seconds=65535)), + ), + cv.Optional( + CONF_REBOOT_TIMEOUT, default="15min" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_REQUIRE_CONNECTION_TO_PROCEED, default=False): cv.boolean, + } + ).extend(cv.polling_component_schema("10s")), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_RTL87XX, + PLATFORM_LN882X, + ] + ), +) async def to_code(config): diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index c11d592cd1..1fda802415 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -32,7 +32,7 @@ struct AllowedIP { }; /// Main Wireguard component class. -class Wireguard : public PollingComponent { +class Wireguard final : public PollingComponent { public: void setup() override; void loop() override; @@ -165,25 +165,26 @@ static constexpr size_t MASK_KEY_BUFFER_SIZE = 12; void mask_key_to(char *buffer, size_t len, const char *key); /// Condition to check if remote peer is online. -template class WireguardPeerOnlineCondition : public Condition, public Parented { +template +class WireguardPeerOnlineCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_peer_up(); } }; /// Condition to check if Wireguard component is enabled. -template class WireguardEnabledCondition : public Condition, public Parented { +template class WireguardEnabledCondition final : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_enabled(); } }; /// Action to enable Wireguard component. -template class WireguardEnableAction : public Action, public Parented { +template class WireguardEnableAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->enable(); } }; /// Action to disable Wireguard component. -template class WireguardDisableAction : public Action, public Parented { +template class WireguardDisableAction final : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->disable(); } }; diff --git a/esphome/components/wl_134/wl_134.h b/esphome/components/wl_134/wl_134.h index 973e5a1e7c..fad64bd8ff 100644 --- a/esphome/components/wl_134/wl_134.h +++ b/esphome/components/wl_134/wl_134.h @@ -8,7 +8,7 @@ namespace esphome::wl_134 { -class Wl134Component : public text_sensor::TextSensor, public Component, public uart::UARTDevice { +class Wl134Component final : public text_sensor::TextSensor, public Component, public uart::UARTDevice { public: enum Rfid134Error { RFID134_ERROR_NONE, diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index e0724aa94a..5150cda2a5 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -13,7 +13,7 @@ #include #endif -#ifdef USE_BK72XX +#ifdef USE_LIBRETINY #include #endif diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index bed897f5a6..07abb7c674 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -8,7 +8,14 @@ #include #include +#if defined(USE_RP2) || defined(USE_LIBRETINY) +namespace arduino { class UDP; +} // namespace arduino +using arduino::UDP; // NOLINT(google-global-names-in-headers) +#else +class UDP; +#endif namespace esphome::wled { diff --git a/esphome/components/wts01/wts01.h b/esphome/components/wts01/wts01.h index 17d4dc57a2..2a284ac86e 100644 --- a/esphome/components/wts01/wts01.h +++ b/esphome/components/wts01/wts01.h @@ -8,7 +8,7 @@ namespace esphome::wts01 { constexpr uint8_t PACKET_SIZE = 9; -class WTS01Sensor : public sensor::Sensor, public uart::UARTDevice, public Component { +class WTS01Sensor final : public sensor::Sensor, public uart::UARTDevice, public Component { public: void loop() override; void dump_config() override; diff --git a/esphome/components/x9c/x9c.h b/esphome/components/x9c/x9c.h index 112f0405d7..1cea15c26f 100644 --- a/esphome/components/x9c/x9c.h +++ b/esphome/components/x9c/x9c.h @@ -6,7 +6,7 @@ namespace esphome::x9c { -class X9cOutput : public output::FloatOutput, public Component { +class X9cOutput final : public output::FloatOutput, public Component { public: void set_cs_pin(InternalGPIOPin *pin) { cs_pin_ = pin; } void set_inc_pin(InternalGPIOPin *pin) { inc_pin_ = pin; } diff --git a/esphome/components/xdb401/xdb401.h b/esphome/components/xdb401/xdb401.h index 674d26fe8e..670425e69e 100644 --- a/esphome/components/xdb401/xdb401.h +++ b/esphome/components/xdb401/xdb401.h @@ -6,7 +6,7 @@ namespace esphome::xdb401 { -class XDB401Component : public PollingComponent, public i2c::I2CDevice { +class XDB401Component final : public PollingComponent, public i2c::I2CDevice { public: void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } diff --git a/esphome/components/xgzp68xx/xgzp68xx.h b/esphome/components/xgzp68xx/xgzp68xx.h index 1bab9b091a..d9aec6e5cc 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.h +++ b/esphome/components/xgzp68xx/xgzp68xx.h @@ -20,7 +20,7 @@ enum XGZP68XXOversampling : uint8_t { XGZP68XX_OVERSAMPLING_UNKNOWN = (uint8_t) -1, }; -class XGZP68XXComponent : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { +class XGZP68XXComponent final : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { public: SUB_SENSOR(temperature) SUB_SENSOR(pressure) diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index a4ecca0c66..1ebcf0e2f5 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -72,7 +72,7 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); bool report_xiaomi_results(const optional &result, const char *address); -class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiListener final : public esp32_ble_tracker::ESPBTDeviceListener { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; }; diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h index 02d098c31b..36068ae227 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_cgdk2 { -class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiCGDK2 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; void set_bindkey(const char *bindkey); diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index d49e3a08d1..7633458cb8 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_cgg1 { -class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiCGG1 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } void set_bindkey(const char *bindkey); diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h index 28a7a3ae2d..0fa6c76e54 100644 --- a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h @@ -10,9 +10,9 @@ namespace esphome::xiaomi_cgpr1 { -class XiaomiCGPR1 : public Component, - public binary_sensor::BinarySensorInitiallyOff, - public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiCGPR1 final : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } void set_bindkey(const char *bindkey); diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h index e14077adb0..668133f364 100644 --- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_gcls002 { -class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiGCLS002 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index 8bc6399065..cb53b47f6f 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_hhccjcy01 { -class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiHHCCJCY01 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h index 812e3a7d8f..fa2f461534 100644 --- a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h +++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h @@ -8,7 +8,7 @@ namespace esphome::xiaomi_hhccjcy10 { -class XiaomiHHCCJCY10 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiHHCCJCY10 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { this->address_ = address; } diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h index 2bdd6102be..3eda1b9859 100644 --- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_hhccpot002 { -class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiHHCCPOT002 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h index aaf34f899f..122c6776c9 100644 --- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_jqjcy01ym { -class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiJQJCY01YM final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index e45596f966..09256047ae 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_lywsd02 { -class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiLYWSD02 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h index 23efcbf8fc..efd758b972 100644 --- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_lywsd02mmc { -class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiLYWSD02MMC final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { this->address_ = address; } void set_bindkey(const char *bindkey); diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h index 03462b850f..ecdbd412cb 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_lywsd03mmc { -class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiLYWSD03MMC final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; void set_bindkey(const char *bindkey); diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index e169afc651..86afef4571 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_lywsdcgq { -class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiLYWSDCGQ final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h index daacd6be86..042a5034f1 100644 --- a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h +++ b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_mhoc303 { -class XiaomiMHOC303 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiMHOC303 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h index 225c9ff189..3570f70a16 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_mhoc401 { -class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiMHOC401 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; void set_bindkey(const char *bindkey); diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h index c75a22c9fb..3213f5d6de 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -16,7 +16,7 @@ struct ParseResult { optional impedance; }; -class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiMiscale final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h index ee4ed52520..da02dee003 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -10,9 +10,9 @@ namespace esphome::xiaomi_mjyd02yla { -class XiaomiMJYD02YLA : public Component, - public binary_sensor::BinarySensorInitiallyOff, - public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiMJYD02YLA final : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } void set_bindkey(const char *bindkey); diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h index a6d8abc5bf..4751e35e65 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h @@ -9,9 +9,9 @@ namespace esphome::xiaomi_mue4094rt { -class XiaomiMUE4094RT : public Component, - public binary_sensor::BinarySensorInitiallyOff, - public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiMUE4094RT final : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h index cc6a334a20..0d3427cc4d 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h @@ -15,7 +15,7 @@ namespace esphome::xiaomi_rtcgq02lm { -class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiRTCGQ02LM final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; void set_bindkey(const char *bindkey); diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h index 0b0cb8db0b..0573959473 100644 --- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h @@ -10,9 +10,9 @@ namespace esphome::xiaomi_wx08zm { -class XiaomiWX08ZM : public Component, - public binary_sensor::BinarySensorInitiallyOff, - public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiWX08ZM final : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h index 9bab943ab9..c7d20aa356 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h @@ -9,7 +9,7 @@ namespace esphome::xiaomi_xmwsdj04mmc { -class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiXMWSDJ04MMC final : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { this->address_ = address; } void set_bindkey(const char *bindkey); diff --git a/esphome/components/xl9535/xl9535.h b/esphome/components/xl9535/xl9535.h index 253ce76273..11fb3acc8d 100644 --- a/esphome/components/xl9535/xl9535.h +++ b/esphome/components/xl9535/xl9535.h @@ -17,7 +17,7 @@ enum { XL9535_CONFIG_PORT_1_REGISTER = 0x07, }; -class XL9535Component : public Component, public i2c::I2CDevice { +class XL9535Component final : public Component, public i2c::I2CDevice { public: bool digital_read(uint8_t pin); void digital_write(uint8_t pin, bool value); @@ -28,7 +28,7 @@ class XL9535Component : public Component, public i2c::I2CDevice { float get_setup_priority() const override { return setup_priority::IO; } }; -class XL9535GPIOPin : public GPIOPin { +class XL9535GPIOPin final : public GPIOPin { public: void set_parent(XL9535Component *parent) { this->parent_ = parent; } void set_pin(uint8_t pin) { this->pin_ = pin; } diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.h b/esphome/components/xpt2046/touchscreen/xpt2046.h index f619e06fb7..8fe9b7cc43 100644 --- a/esphome/components/xpt2046/touchscreen/xpt2046.h +++ b/esphome/components/xpt2046/touchscreen/xpt2046.h @@ -11,9 +11,9 @@ namespace esphome::xpt2046 { using namespace touchscreen; -class XPT2046Component : public Touchscreen, - public spi::SPIDevice { +class XPT2046Component final : public Touchscreen, + public spi::SPIDevice { public: /// Set the threshold for the touch detection. void set_threshold(int16_t threshold) { this->threshold_ = threshold; } diff --git a/esphome/components/yashima/yashima.h b/esphome/components/yashima/yashima.h index 336b28f5c5..864b3fce66 100644 --- a/esphome/components/yashima/yashima.h +++ b/esphome/components/yashima/yashima.h @@ -9,7 +9,7 @@ namespace esphome::yashima { -class YashimaClimate : public climate::Climate, public Component { +class YashimaClimate final : public climate::Climate, public Component { public: void setup() override; void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 9c0d10ba58..9f755a6eea 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -27,7 +27,26 @@ from .const import ( CODEOWNERS = ["@tomaszduda23"] -PrjConfValueType = bool | str | int + +class HexValue: + """Wrap an integer so it is written as 0x... in prj.conf (required for hex Kconfig types).""" + + def __init__(self, value: int) -> None: + self.value = value + + def __eq__(self, other: object) -> bool: + if isinstance(other, HexValue): + return self.value == other.value + return NotImplemented + + def __repr__(self) -> str: + return f"HexValue(0x{self.value:X})" + + def __str__(self) -> str: + return f"0x{self.value:X}" + + +PrjConfValueType = bool | str | int | HexValue class Section: @@ -141,9 +160,7 @@ def zephyr_to_code(config: ConfigType) -> None: cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") # c++ support - zephyr_add_prj_conf("NEWLIB_LIBC", True) zephyr_add_prj_conf("FPU", True) - zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) zephyr_add_prj_conf("STD_CPP20", True) # random_bytes() uses sys_rand_get() which requires the entropy subsystem zephyr_add_prj_conf("ENTROPY_GENERATOR", True) @@ -173,6 +190,8 @@ def zephyr_setup_preferences(): def _format_prj_conf_val(value: PrjConfValueType) -> str: if isinstance(value, bool): return "y" if value else "n" + if isinstance(value, HexValue): + return hex(value.value) if isinstance(value, int): return str(value) if isinstance(value, str): @@ -240,7 +259,7 @@ def copy_files() -> None: ) if image: - path = CORE.relative_build_path(f"sysbuild/{image}.conf") + path = CORE.relative_build_path(f"zephyr/sysbuild/{image}.conf") else: path = CORE.relative_build_path("zephyr/prj.conf") @@ -248,7 +267,7 @@ def copy_files() -> None: for image, content in zephyr_data()[KEY_OVERLAY].items(): if image: - path = CORE.relative_build_path(f"sysbuild/{image}.overlay") + path = CORE.relative_build_path(f"zephyr/sysbuild/{image}.overlay") else: path = CORE.relative_build_path("zephyr/app.overlay") changed |= write_file_if_changed(path, content) diff --git a/esphome/components/zephyr/cdc_acm.h b/esphome/components/zephyr/cdc_acm.h index 4dc14397d8..9d11d4b575 100644 --- a/esphome/components/zephyr/cdc_acm.h +++ b/esphome/components/zephyr/cdc_acm.h @@ -7,7 +7,7 @@ namespace esphome::zephyr { -class CdcAcm : public Component { +class CdcAcm final : public Component { public: CdcAcm(); void setup() override; diff --git a/esphome/components/zephyr/gpio.cpp b/esphome/components/zephyr/gpio.cpp index 1e4201d8f5..23da2cafac 100644 --- a/esphome/components/zephyr/gpio.cpp +++ b/esphome/components/zephyr/gpio.cpp @@ -173,6 +173,14 @@ bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { return bool(gpio_pin_get(arg->gpio, arg->pin % arg->gpio_size) != arg->inverted); } +void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { + auto *arg = (zephyr::ISRPinArg *) this->arg_; + if (arg == nullptr || arg->gpio == nullptr) { + return; + } + gpio_pin_set(arg->gpio, arg->pin % arg->gpio_size, value != arg->inverted ? 1 : 0); +} + } // namespace esphome #endif diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index 19d68cfb2b..71d1620a67 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -16,7 +16,7 @@ struct ZephyrGPIOInterrupt { void *arg{nullptr}; }; -class ZephyrGPIOPin : public InternalGPIOPin { +class ZephyrGPIOPin final : public InternalGPIOPin { public: ZephyrGPIOPin(const device *gpio, int gpio_size, const char *pin_name_prefix) { this->gpio_ = gpio; diff --git a/esphome/components/zephyr_ble_server/ble_server.h b/esphome/components/zephyr_ble_server/ble_server.h index bf69c52b12..223dbf7ac9 100644 --- a/esphome/components/zephyr_ble_server/ble_server.h +++ b/esphome/components/zephyr_ble_server/ble_server.h @@ -6,7 +6,7 @@ namespace esphome::zephyr_ble_server { -class BLEServer : public Component { +class BLEServer final : public Component { public: void setup() override; void dump_config() override; @@ -21,7 +21,7 @@ class BLEServer : public Component { CallbackManager passkey_cb_; }; -template class BLENumericComparisonReplyAction : public Action { +template class BLENumericComparisonReplyAction final : public Action { public: explicit BLENumericComparisonReplyAction(BLEServer *parent) : parent_(parent) {} diff --git a/esphome/components/zephyr_mcumgr/ota/__init__.py b/esphome/components/zephyr_mcumgr/ota/__init__.py index 0ff1825bd1..1503c94274 100644 --- a/esphome/components/zephyr_mcumgr/ota/__init__.py +++ b/esphome/components/zephyr_mcumgr/ota/__init__.py @@ -1,6 +1,8 @@ import esphome.codegen as cg +from esphome.components.nrf52.boards import BOOTLOADER_CONFIG from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code from esphome.components.zephyr import ( + HexValue, zephyr_add_cdc_acm, zephyr_add_overlay, zephyr_add_prj_conf, @@ -72,12 +74,6 @@ CONFIG_SCHEMA = cv.All( ) -def _validate_mcumgr_bootloader(config: ConfigType) -> None: - bootloader = zephyr_data()[KEY_BOOTLOADER] - if bootloader != BOOTLOADER_MCUBOOT: - raise cv.Invalid(f"'{bootloader}' bootloader does not support OTA") - - KEY_ZEPHYR_BLE_SERVER = "zephyr_ble_server" @@ -89,9 +85,22 @@ def _validate_ble_server(config: ConfigType) -> None: raise cv.Invalid(f"'{KEY_ZEPHYR_BLE_SERVER}' component is required for BLE OTA") +def _validate_bootloader(config: ConfigType) -> None: + bootloader = zephyr_data()[KEY_BOOTLOADER] + if bootloader == BOOTLOADER_MCUBOOT: + return + if bootloader not in BOOTLOADER_CONFIG: + raise cv.Invalid(f"{bootloader} does not support OTA") + framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if framework_ver < cv.Version(2, 9, 2): + raise cv.Invalid( + "OTA with Adafruit_nRF52_Bootloader requires at least SDK 2.9.2" + ) + + def _final_validate(config: ConfigType) -> None: - _validate_mcumgr_bootloader(config) _validate_ble_server(config) + _validate_bootloader(config) FINAL_VALIDATE_SCHEMA = _final_validate @@ -152,3 +161,70 @@ async def to_code(config: ConfigType) -> None: framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] if framework_ver >= cv.Version(2, 9, 2): zephyr_data()[KEY_SYSBUILD] = True + + bootloader = zephyr_data()[KEY_BOOTLOADER] + if bootloader != BOOTLOADER_MCUBOOT: + sections = BOOTLOADER_CONFIG[bootloader] + # Derive partition addresses from the SoftDevice and bootloader sections so + # that the DTS flash map matches what the Partition Manager produces: + # MCUboot sits immediately after the SoftDevice, then slot0, then slot1. + mcuboot_size = 0x9000 + sd_end = next(s.address + s.size for s in sections if "SoftDevice" in s.name) + bl_start = next(s.address for s in sections if "Adafruit" in s.name) + slot0_start = sd_end + mcuboot_size + # Align slot size down to a 4 KB sector boundary + slot_size = ((bl_start - slot0_start) // 2 // 0x1000) * 0x1000 + slot1_start = slot0_start + slot_size + + def _mcuboot_partition_overlay() -> str: + def part(name, start, size): + return f""" + {name}: partition@{start:x} {{ + reg = <0x{start:x} 0x{size:x}>; + }};""" + + return f""" + /delete-node/ &boot_partition; + /delete-node/ &storage_partition; + /delete-node/ &code_partition; + /delete-node/ &reserved_partition_0; + + &flash0 {{ + partitions {{ + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + {part("slot0_partition", slot0_start, slot_size)} + {part("slot1_partition", slot1_start, slot_size)} + }}; + }}; + """ + + def _code_partition_overlay() -> str: + return """ + / { + chosen { + zephyr,code-partition = &slot0_partition; + }; + }; + """ + + zephyr_add_overlay(_mcuboot_partition_overlay()) + zephyr_add_overlay(_mcuboot_partition_overlay(), "mcuboot") + zephyr_add_overlay(_code_partition_overlay()) + zephyr_add_overlay(_code_partition_overlay(), "mcuboot") + # mcuboot is second bootloader. It's only task is to swap partitions. + # recovery can be done by first bootloader. Keep it small. + zephyr_add_overlay( + """ + &zephyr_udc0 { + status = "disabled"; + }; + """, + "mcuboot", + ) + zephyr_add_prj_conf("USB_DEVICE_STACK", False, image="mcuboot") + zephyr_add_prj_conf("CONSOLE", False, image="mcuboot") + zephyr_add_prj_conf( + "PM_PARTITION_SIZE_MCUBOOT", HexValue(mcuboot_size), image="mcuboot" + ) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 444012bcd8..775fb35140 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -18,10 +18,13 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import ConfigType from .const import ( + CONF_ENDPOINT, + CONF_MAX_EP_NUMBER, CONF_ON_JOIN, CONF_POWER_SOURCE, CONF_REPORT, CONF_ROUTER, + CONF_USE_DEVICE_TYPE, CONF_WIPE_ON_BOOT, KEY_ZIGBEE, POWER_SOURCE, @@ -31,7 +34,7 @@ from .const import ( ) from .const_zephyr import ( CONF_IEEE802154_VENDOR_OUI, - CONF_MAX_EP_NUMBER, + CONF_MAX_EP_NUMBER_ZEPHYR, CONF_SLEEPY, CONF_ZIGBEE_ID, KEY_EP_NUMBER, @@ -71,7 +74,17 @@ BASE_SCHEMA = cv.Schema( cv.requires_component("esp32"), _check_report_deprecation, cv.enum(REPORT, lower=True), - ) + ), + cv.Optional(CONF_ENDPOINT): cv.All( + cv.requires_component("zigbee"), + cv.requires_component("esp32"), + cv.int_range(1, CONF_MAX_EP_NUMBER), + ), + cv.Optional(CONF_USE_DEVICE_TYPE): cv.All( + cv.requires_component("zigbee"), + cv.requires_component("esp32"), + cv.boolean, + ), } ) BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(BASE_SCHEMA).extend(zephyr_binary_sensor) @@ -148,8 +161,8 @@ def validate_number_of_ep(config: ConfigType) -> ConfigType: _LOGGER.warning( "Single endpoint requires ZHA or at leatst Zigbee2MQTT 2.8.0. For older versions of Zigbee2MQTT use multiple endpoints" ) - if count > CONF_MAX_EP_NUMBER and not CORE.testing_mode: - raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}") + if count > CONF_MAX_EP_NUMBER_ZEPHYR and not CORE.testing_mode: + raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER_ZEPHYR}") return config diff --git a/esphome/components/zigbee/const.py b/esphome/components/zigbee/const.py index dd36f815ab..d922ae372f 100644 --- a/esphome/components/zigbee/const.py +++ b/esphome/components/zigbee/const.py @@ -14,12 +14,17 @@ from esphome.const import ( UNIT_AMPERE, UNIT_CELSIUS, UNIT_CENTIMETER, + UNIT_CUBIC_METER, + UNIT_CUBIC_METER_PER_HOUR, UNIT_DECIBEL, UNIT_HECTOPASCAL, UNIT_HERTZ, UNIT_HOUR, UNIT_KELVIN, UNIT_KILOMETER, + UNIT_KILOPASCAL, + UNIT_KILOVOLT_AMPS, + UNIT_KILOVOLT_AMPS_REACTIVE, UNIT_KILOWATT, UNIT_KILOWATT_HOURS, UNIT_LITRE_PER_SECOND, @@ -37,6 +42,7 @@ from esphome.const import ( UNIT_PARTS_PER_MILLION, UNIT_PASCAL, UNIT_PERCENT, + UNIT_PH, UNIT_SECOND, UNIT_VOLT, UNIT_WATT, @@ -58,11 +64,14 @@ REPORT = { "default": report.ZIGBEE_REPORT_DEFAULT, } +CONF_ENDPOINT = "endpoint" +CONF_MAX_EP_NUMBER = 239 CONF_ON_JOIN = "on_join" CONF_WIPE_ON_BOOT = "wipe_on_boot" CONF_REPORT = "report" CONF_ROUTER = "router" CONF_POWER_SOURCE = "power_source" +CONF_USE_DEVICE_TYPE = "use_device_type" POWER_SOURCE = { "UNKNOWN": 0x00, # ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN "MAINS_SINGLE_PHASE": 0x01, # ZB_ZCL_BASIC_POWER_SOURCE_MAINS_SINGLE_PHASE @@ -87,10 +96,13 @@ BACNET_UNITS = { UNIT_OHM: 4, UNIT_WATT: 47, UNIT_KILOWATT: 48, + UNIT_KILOVOLT_AMPS: 9, + UNIT_KILOVOLT_AMPS_REACTIVE: 12, UNIT_WATT_HOURS: 18, UNIT_KILOWATT_HOURS: 19, UNIT_PASCAL: 53, UNIT_HECTOPASCAL: 133, + UNIT_KILOPASCAL: 54, UNIT_HERTZ: 27, UNIT_MILLIMETER: 30, UNIT_CENTIMETER: 118, @@ -107,6 +119,9 @@ BACNET_UNITS = { UNIT_LUX: 37, UNIT_DECIBEL: 199, UNIT_PERCENT: 98, + UNIT_CUBIC_METER: 80, + UNIT_CUBIC_METER_PER_HOUR: 135, + UNIT_PH: 234, } BACNET_UNIT_NO_UNITS = 95 diff --git a/esphome/components/zigbee/const_esp32.py b/esphome/components/zigbee/const_esp32.py index 81a8fc52cd..bfc4d93d5b 100644 --- a/esphome/components/zigbee/const_esp32.py +++ b/esphome/components/zigbee/const_esp32.py @@ -2,16 +2,13 @@ import esphome.codegen as cg DEVICE_TYPE = "device_type" ROLE = "role" -CONF_MAX_EP_NUMBER = 239 -CONF_NUM = "num" CONF_CLUSTERS = "clusters" CONF_ATTRIBUTES = "attributes" -CONF_ENDPOINT = "endpoint" CONF_CLUSTER = "cluster" SCALE = "scale" CONF_ATTRIBUTE_ID = "attribute_id" -KEY_BS_EP = "binary_sensor_ep" -KEY_SENSOR_EP = "sensor_ep" +KEY_ZIGBEE_EP = "zigbee_ep" +KEY_ZIGBEE_EP_NO_NUM = "zigbee_ep_no_num" DEVICE_ID = { "RANGE_EXTENDER": cg.RawExpression("EZB_ZHA_RANGE_EXTENDER_DEVICE_ID"), diff --git a/esphome/components/zigbee/const_zephyr.py b/esphome/components/zigbee/const_zephyr.py index 63d03c7952..bf8e8287c4 100644 --- a/esphome/components/zigbee/const_zephyr.py +++ b/esphome/components/zigbee/const_zephyr.py @@ -1,4 +1,4 @@ -CONF_MAX_EP_NUMBER = 8 +CONF_MAX_EP_NUMBER_ZEPHYR = 8 CONF_ZIGBEE_ID = "zigbee_id" CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor" CONF_ZIGBEE_SENSOR = "zigbee_sensor" diff --git a/esphome/components/zigbee/zigbee_attribute_esp32.cpp b/esphome/components/zigbee/zigbee_attribute_esp32.cpp index c6f2aa0af6..d7176e6ca5 100644 --- a/esphome/components/zigbee/zigbee_attribute_esp32.cpp +++ b/esphome/components/zigbee/zigbee_attribute_esp32.cpp @@ -50,25 +50,6 @@ void ZigbeeAttribute::report_(bool has_lock) { } } -void ZigbeeAttribute::setup_reporting() { - ezb_zcl_reporting_info_t reporting_info = ezb_zcl_reporting_info_find( - this->endpoint_id_, this->cluster_id_, this->role_, this->attr_id_, EZB_ZCL_STD_MANUF_CODE); - if (reporting_info == EZB_ZCL_INVALID_REPORTING_INFO) { - ESP_LOGD(TAG, "Could not find reporting info for attribute 0x%04X in cluster 0x%04X in endpoint %u", this->attr_id_, - this->cluster_id_, this->endpoint_id_); - this->report_enabled = false; - this->force_report_ = false; - } else { - ESP_LOGD(TAG, "Found reporting info for attr 0x%04X in cluster 0x%04X", this->attr_id_, this->cluster_id_); - ezb_zcl_attr_variable_t delta = {.u64 = 0}; - ezb_zcl_reporting_info_update_default_interval(reporting_info, 0, 65000); - ezb_zcl_reporting_info_update(reporting_info, 0, 65000, &delta); - if (ezb_zcl_reporting_start_attr_report(reporting_info) != EZB_ERR_NONE) { - ESP_LOGE(TAG, "Could not start reporting for attribute"); - } - } -} - void ZigbeeAttribute::set_report(ZigbeeReportT report) { this->report_enabled = true; if (report == ZigbeeReportT::ZIGBEE_REPORT_FORCE) { diff --git a/esphome/components/zigbee/zigbee_attribute_esp32.h b/esphome/components/zigbee/zigbee_attribute_esp32.h index b5afb57910..e5f8c8b1cf 100644 --- a/esphome/components/zigbee/zigbee_attribute_esp32.h +++ b/esphome/components/zigbee/zigbee_attribute_esp32.h @@ -42,7 +42,6 @@ class ZigbeeAttribute final : public Component { scale_(scale) {} void loop() override; template void add_attr(T value); - void setup_reporting(); template void set_attr(const T &value); uint8_t attr_type() { return attr_type_; } void set_report(ZigbeeReportT report); diff --git a/esphome/components/zigbee/zigbee_ep_esp32.py b/esphome/components/zigbee/zigbee_ep_esp32.py index f4efa7bf4e..2ed3dddb67 100644 --- a/esphome/components/zigbee/zigbee_ep_esp32.py +++ b/esphome/components/zigbee/zigbee_ep_esp32.py @@ -2,16 +2,22 @@ from typing import Any import esphome.config_validation as cv from esphome.const import CONF_DEVICE, CONF_ID, CONF_TYPE +from esphome.core import CORE -from .const import CONF_REPORT, REPORT +from .const import ( + CONF_MAX_EP_NUMBER, + CONF_REPORT, + CONF_USE_DEVICE_TYPE, + KEY_ZIGBEE, + REPORT, +) from .const_esp32 import ( - CLUSTER_ROLE, CONF_ATTRIBUTE_ID, CONF_ATTRIBUTES, CONF_CLUSTERS, - CONF_MAX_EP_NUMBER, - CONF_NUM, DEVICE_TYPE, + KEY_ZIGBEE_EP, + KEY_ZIGBEE_EP_NO_NUM, ROLE, ) @@ -22,12 +28,12 @@ ep_configs: dict[str, dict[str, Any]] = { CONF_CLUSTERS: [ { CONF_ID: "BINARY_INPUT", - ROLE: CLUSTER_ROLE["SERVER"], + ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x55, CONF_TYPE: "BOOL", - CONF_REPORT: REPORT["default"], + CONF_REPORT: cv.enum(REPORT, lower=True)("default"), CONF_DEVICE: None, }, { @@ -47,16 +53,15 @@ ep_configs: dict[str, dict[str, Any]] = { ], }, "analog_input": { - DEVICE_TYPE: "CUSTOM_ATTR", CONF_CLUSTERS: [ { CONF_ID: "ANALOG_INPUT", - ROLE: CLUSTER_ROLE["SERVER"], + ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x55, CONF_TYPE: "SINGLE", - CONF_REPORT: REPORT["default"], + CONF_REPORT: cv.enum(REPORT, lower=True)("default"), CONF_DEVICE: None, }, { @@ -78,22 +83,144 @@ ep_configs: dict[str, dict[str, Any]] = { } -def create_ep(ep_list: list[dict[str, Any]], router: bool) -> list[dict[str, Any]]: +def get_next_ep_num(eps: list[int]) -> int: + try: + ep_num = [i for i in range(1, CONF_MAX_EP_NUMBER + 1) if i not in eps][0] + eps.append(ep_num) + except IndexError as e: + raise cv.Invalid( + f"Too many devices. Zigbee can define only {CONF_MAX_EP_NUMBER} endpoints." + ) from e + return ep_num + + +def compare_clusters( + existing_ep: dict[str, Any], + ep: dict[str, Any], +) -> tuple[str | int, str] | None: + existing_clusters = [(cl[CONF_ID], cl[ROLE]) for cl in existing_ep[CONF_CLUSTERS]] + for cl in [(cl[CONF_ID], cl[ROLE]) for cl in ep[CONF_CLUSTERS]]: + if cl in existing_clusters: + return cl + return None + + +def merge_endpoints( + existing_ep: dict[str, Any], + ep: dict[str, Any], + use_type: bool | None, +) -> bool: + if compare_clusters(existing_ep, ep): + return False + if ( + ep.get(DEVICE_TYPE) + and existing_ep.get(DEVICE_TYPE) + and ep.get(DEVICE_TYPE) != existing_ep.get(DEVICE_TYPE) + ): + return False + if ( + ep.get(DEVICE_TYPE) + and not existing_ep.get(DEVICE_TYPE) + and existing_ep.get(CONF_USE_DEVICE_TYPE) + ): + return False + if existing_ep.get(DEVICE_TYPE) and not ep.get(DEVICE_TYPE) and use_type: + return False + if use_type: + existing_ep[CONF_USE_DEVICE_TYPE] = use_type + if ep.get(DEVICE_TYPE): + existing_ep[DEVICE_TYPE] = ep[DEVICE_TYPE] + existing_ep[CONF_CLUSTERS].extend(ep[CONF_CLUSTERS]) + return True + + +def validate_endpoints(ep_dict: dict[int, dict]) -> None: + for num, ep in ep_dict.items(): + types_dict = ep.get(CONF_USE_DEVICE_TYPE) + if not types_dict: + continue + if len(types_dict) == 1: + ep[DEVICE_TYPE] = list(types_dict.keys())[0] + del ep[CONF_USE_DEVICE_TYPE] + continue + types_list = [t[0] for t in types_dict.items() if t[1]] + if len(types_list) > 1: + raise cv.Invalid( + f"There is more than one component with endpoint: {num} and {CONF_USE_DEVICE_TYPE}: True" + ) + if not types_list: + raise cv.Invalid( + f"Multiple device types on endpoint: {num}. Set {CONF_USE_DEVICE_TYPE}: True on one component." + ) + ep[DEVICE_TYPE] = types_list[0] + del ep[CONF_USE_DEVICE_TYPE] + + +def create_ep(router: bool) -> None: + zb_data = CORE.data.setdefault(KEY_ZIGBEE, {}) + ep_dict: dict[int, dict] = zb_data.setdefault(KEY_ZIGBEE_EP, {}) + ep_list: list[dict] = zb_data.setdefault(KEY_ZIGBEE_EP_NO_NUM, []) + validate_endpoints(ep_dict) # create dummy endpoint if list is empty - if not ep_list: + if not ep_dict and not ep_list: ep_type = "CUSTOM_ATTR" if router: ep_type = "RANGE_EXTENDER" - ep_list = [ - { - DEVICE_TYPE: ep_type, - } - ] - # enumerate endpoints - for i, ep in enumerate(ep_list, 1): - ep[CONF_NUM] = i - if len(ep_list) > CONF_MAX_EP_NUMBER: - raise cv.Invalid( - f"Too many devices. Zigbee can define only {CONF_MAX_EP_NUMBER} endpoints." - ) - return ep_list + ep_dict[1] = {DEVICE_TYPE: ep_type} + if ep_list: + # merge endpoint with different clusters + ep_list_new: list[dict] = [] + for ep in ep_list: + added = False + for existing_ep in ep_list_new: + if merge_endpoints(existing_ep, ep, ep.get(CONF_USE_DEVICE_TYPE)): + added = True + break + if not added: + ep_list_new.append(ep) + + # Add endpoints with no number to the endpoint dict with a new number + eps = list(ep_dict.keys()) + for ep in ep_list_new: + ep_num = get_next_ep_num(eps) + ep_dict[ep_num] = ep + + # clear list so that it is not processed again + del zb_data[KEY_ZIGBEE_EP_NO_NUM] + + # Add default device type to endpoints that have none + for ep in ep_dict.values(): + if not ep.get(DEVICE_TYPE): + ep[DEVICE_TYPE] = "CUSTOM_ATTR" + + +def add_ep(ep: dict[str, Any], ep_num: int | None, use_type: bool | None) -> None: + zb_data = CORE.data.setdefault(KEY_ZIGBEE, {}) + if use_type is False: + ep.pop(DEVICE_TYPE, None) + if ep_num is None: + if use_type: + ep[CONF_USE_DEVICE_TYPE] = use_type + ep_list: list[dict] = zb_data.setdefault(KEY_ZIGBEE_EP_NO_NUM, []) + ep_list.append(ep) + else: + ep_dict: dict[int, dict] = zb_data.setdefault(KEY_ZIGBEE_EP, {}) + if ep_num in ep_dict: + # check if the existing endpoint has same clusters + existing_ep = ep_dict[ep_num] + if cl := compare_clusters( + existing_ep, + ep, + ): + raise cv.Invalid( + f"Endpoint {ep_num} has more than one cluster with cluster id {cl[0]} and role {cl[1]}." + ) + if ep.get(DEVICE_TYPE) or use_type: + types_dict = existing_ep.setdefault(CONF_USE_DEVICE_TYPE, {}) + if not types_dict.get(ep.get(DEVICE_TYPE)) or use_type: + types_dict[ep.get(DEVICE_TYPE)] = use_type + existing_ep[CONF_CLUSTERS].extend(ep[CONF_CLUSTERS]) + else: + if use_type or ep.get(DEVICE_TYPE): + ep[CONF_USE_DEVICE_TYPE] = {ep.get(DEVICE_TYPE): use_type} + ep_dict[ep_num] = ep diff --git a/esphome/components/zigbee/zigbee_esp32.cpp b/esphome/components/zigbee/zigbee_esp32.cpp index 03457312be..3e0f6cd745 100644 --- a/esphome/components/zigbee/zigbee_esp32.cpp +++ b/esphome/components/zigbee/zigbee_esp32.cpp @@ -53,11 +53,7 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) { switch (signal_type) { case EZB_ZDO_SIGNAL_SKIP_STARTUP: ESP_LOGD(TAG, "Zigbee stack initialized"); - if (ezb_bdb_is_factory_new()) { - global_zigbee->defer([]() { global_zigbee->setup_reporting(); }); - } else { - ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_INITIALIZATION); - } + ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_INITIALIZATION); break; case EZB_BDB_SIGNAL_DEVICE_FIRST_START: case EZB_BDB_SIGNAL_DEVICE_REBOOT: { @@ -133,12 +129,12 @@ static void zb_action_handler(ezb_zcl_core_action_callback_id_t callback_id, voi case EZB_ZCL_CORE_SET_ATTR_VALUE_CB_ID: zb_attribute_handler((ezb_zcl_set_attr_value_message_t *) message); break; -#ifdef ESPHOME_LOG_HAS_VERBOSE case EZB_ZCL_CORE_DEFAULT_RSP_CB_ID: { +#ifdef ESPHOME_LOG_HAS_VERBOSE ezb_zcl_cmd_default_rsp_message_t *default_rsp = (ezb_zcl_cmd_default_rsp_message_t *) message; ESP_LOGV(TAG, "Received ZCL Default Response: 0x%02x", default_rsp->in.status_code); - } break; #endif + } break; default: ESP_LOGD(TAG, "Receive Zigbee action(0x%04x) callback", static_cast(callback_id)); break; @@ -206,21 +202,30 @@ void ZigbeeComponent::update_basic_cluster_(ezb_af_ep_desc_t ep_desc) { ezb_af_endpoint_add_cluster_desc(ep_desc, cluster_desc); } -void ZigbeeComponent::setup_reporting() { - ESP_LOGD(TAG, "Setting up reporting for all attributes"); - esp_zigbee_lock_acquire(portMAX_DELAY); - for (auto &[_, attribute] : this->attributes_) { - attribute->setup_reporting(); +bool ZigbeeComponent::register_device() { + if (ezb_af_device_desc_register(this->dev_desc_) != EZB_ERR_NONE) { + ESP_LOGE(TAG, "Could not register the endpoint list"); + this->mark_failed(); + return false; } - ezb_bdb_start_top_level_commissioning(EZB_BDB_MODE_INITIALIZATION); - esp_zigbee_lock_release(); + return true; } static void ezb_task(void *pv_parameters) { + if (!global_zigbee->register_device()) { + vTaskDelete(NULL); + return; + } if (esp_zigbee_start(false) != ESP_OK) { ESP_LOGE(TAG, "Could not setup Zigbee"); + global_zigbee->mark_failed(); vTaskDelete(NULL); + return; // vTaskDelete(NULL) never returns, but keep intent explicit } + + // Increase priority to 5 to align with openthread or BLE + vTaskPrioritySet(NULL, 5); + esp_zigbee_launch_mainloop(); esp_zigbee_deinit(); @@ -274,12 +279,6 @@ void ZigbeeComponent::setup() { return; } - if (ezb_af_device_desc_register(this->dev_desc_) != EZB_ERR_NONE) { - ESP_LOGE(TAG, "Could not register the endpoint list"); - this->mark_failed(); - return; - } - ezb_zcl_core_action_handler_register(zb_action_handler); if (ezb_bdb_set_primary_channel_set(EZB_PRIMARY_CHANNEL_MASK) != ESP_OK) { @@ -298,7 +297,8 @@ void ZigbeeComponent::setup() { }; ezb_af_set_node_power_desc(&desc); - xTaskCreate(ezb_task, "Zigbee_main", 4096, NULL, 24, NULL); + // Start the Zigbee task with priority 1 to ensure main loop can still run even if Zigbee is busy + xTaskCreate(ezb_task, "Zigbee_main", 4096, NULL, 1, NULL); this->disable_loop(); // loop is only needed for processing events, so disable until we join a network } diff --git a/esphome/components/zigbee/zigbee_esp32.h b/esphome/components/zigbee/zigbee_esp32.h index 11289843a8..f4bafac294 100644 --- a/esphome/components/zigbee/zigbee_esp32.h +++ b/esphome/components/zigbee/zigbee_esp32.h @@ -42,7 +42,7 @@ class ZigbeeComponent final : public Component { void set_basic_cluster(const char *model, const char *manufacturer, uint8_t power_source); void add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role); void create_default_cluster(uint8_t endpoint_id, uint16_t device_id); - void setup_reporting(); + bool register_device(); template void add_attr(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, diff --git a/esphome/components/zigbee/zigbee_esp32.py b/esphome/components/zigbee/zigbee_esp32.py index f19bc97be7..116dce8cc5 100644 --- a/esphome/components/zigbee/zigbee_esp32.py +++ b/esphome/components/zigbee/zigbee_esp32.py @@ -35,9 +35,11 @@ from .const import ( ANALOG_INPUT_APPTYPE, BACNET_UNIT_NO_UNITS, BACNET_UNITS, + CONF_ENDPOINT, CONF_POWER_SOURCE, CONF_REPORT, CONF_ROUTER, + CONF_USE_DEVICE_TYPE, KEY_ZIGBEE, POWER_SOURCE, ZigbeeAttribute, @@ -45,18 +47,17 @@ from .const import ( from .const_esp32 import ( ATTR_TYPE, CLUSTER_ID, + CLUSTER_ROLE, CONF_ATTRIBUTE_ID, CONF_ATTRIBUTES, CONF_CLUSTERS, - CONF_NUM, DEVICE_ID, DEVICE_TYPE, - KEY_BS_EP, - KEY_SENSOR_EP, + KEY_ZIGBEE_EP, ROLE, SCALE, ) -from .zigbee_ep_esp32 import create_ep, ep_configs +from .zigbee_ep_esp32 import add_ep, create_ep, ep_configs _LOGGER = logging.getLogger(__name__) @@ -146,6 +147,7 @@ def final_validate_esp32(config: ConfigType) -> ConfigType: raise cv.Invalid( f"Partition '{partition}' in your custom partition table has wrong format. It should be: '{partition}, {types['type']}, {types['subtype']}, , {types['size']},'" ) + create_ep(config.get(CONF_ROUTER)) return config @@ -199,18 +201,14 @@ def validate_sensor_esp32(config: ConfigType) -> ConfigType: }, ) setup_attributes(config, ep[CONF_CLUSTERS]) - zb_data = CORE.data.setdefault(KEY_ZIGBEE, {}) - sensor_ep: list[dict] = zb_data.setdefault(KEY_SENSOR_EP, []) - sensor_ep.append(ep) + add_ep(ep, config.get(CONF_ENDPOINT), config.get(CONF_USE_DEVICE_TYPE)) return config def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType: ep = copy.deepcopy(ep_configs["binary_input"]) setup_attributes(config, ep[CONF_CLUSTERS]) - zb_data = CORE.data.setdefault(KEY_ZIGBEE, {}) - binary_sensor_ep: list[dict] = zb_data.setdefault(KEY_BS_EP, []) - binary_sensor_ep.append(ep) + add_ep(ep, config.get(CONF_ENDPOINT), config.get(CONF_USE_DEVICE_TYPE)) return config @@ -243,7 +241,7 @@ async def attributes_to_code( var.add_attr( ep_num, CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]), - cl[ROLE], + CLUSTER_ROLE[cl[ROLE]], attr[CONF_ATTRIBUTE_ID], attr.get(CONF_MAX_LENGTH, 0), attr[CONF_VALUE], @@ -255,7 +253,7 @@ async def attributes_to_code( var, ep_num, CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]), - cl[ROLE], + CLUSTER_ROLE[cl[ROLE]], attr[CONF_ATTRIBUTE_ID], ATTR_TYPE[attr[CONF_TYPE]], attr.get(SCALE, 1), @@ -276,7 +274,7 @@ async def attributes_to_code( async def esp32_to_code(config: ConfigType) -> "MockObj": add_idf_component( name="espressif/esp-zigbee-lib", - ref="2.0.2", + ref="2.0.3", ) # add sdkconfigs later so they can overwrite esp32 defaults @@ -287,9 +285,7 @@ async def esp32_to_code(config: ConfigType) -> "MockObj": # create endpoints zb_data = CORE.data.get(KEY_ZIGBEE, {}) - sensor_ep: list[dict] = zb_data.get(KEY_SENSOR_EP, []) - binary_sensor_ep: list[dict] = zb_data.get(KEY_BS_EP, []) - ep_list = create_ep(sensor_ep + binary_sensor_ep, config.get(CONF_ROUTER)) + ep_dict: dict[int, dict] = zb_data.get(KEY_ZIGBEE_EP, {}) # setup zigbee components var = cg.new_Pvariable(config[CONF_ID]) @@ -301,15 +297,15 @@ async def esp32_to_code(config: ConfigType) -> "MockObj": POWER_SOURCE[config[CONF_POWER_SOURCE]], ) ) - for ep in ep_list: - cg.add(var.create_default_cluster(ep[CONF_NUM], DEVICE_ID[ep[DEVICE_TYPE]])) + for ep_num, ep in ep_dict.items(): + cg.add(var.create_default_cluster(ep_num, DEVICE_ID[ep[DEVICE_TYPE]])) for cl in ep.get(CONF_CLUSTERS, []): cg.add( var.add_cluster( - ep[CONF_NUM], + ep_num, CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]), - cl[ROLE], + CLUSTER_ROLE[cl[ROLE]], ) ) - await attributes_to_code(var, ep[CONF_NUM], cl) + await attributes_to_code(var, ep_num, cl) return var diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index 81aad7dcb1..fedcb4a9c2 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -229,6 +229,7 @@ void ZigbeeComponent::dump_config() { " Wipe on boot: %s\n" " Device is joined to the network: %s\n" " Sleep time: %us\n" + " Radio sleep time: %us\n" " RX ON when idle: %s\n" " Current channel: %d\n" " Current page: %d\n" @@ -238,9 +239,10 @@ void ZigbeeComponent::dump_config() { " Short addr: 0x%04X\n" " Long pan id: 0x%s\n" " Short pan id: 0x%04X", - get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, YESNO(zb_get_rx_on_when_idle()), - zb_get_current_channel(), zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, - zb_get_short_address(), extended_pan_id_buf, zb_get_pan_id()); + get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, this->radio_sleep_time_, + YESNO(zb_get_rx_on_when_idle()), zb_get_current_channel(), zb_get_current_page(), + zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf, + zb_get_pan_id()); dump_reporting_(); } @@ -251,6 +253,13 @@ static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) { void ZigbeeComponent::force_report() { this->force_report_ = true; } +void ZigbeeComponent::add_radio_sleep_time_ms(uint32_t ms) { + this->radio_sleep_remainder_ += ms; + uint32_t seconds = this->radio_sleep_remainder_ / 1000; + this->radio_sleep_remainder_ -= seconds * 1000; + this->radio_sleep_time_ += seconds; +} + void ZigbeeComponent::loop() { if (this->force_report_) { this->force_report_ = false; @@ -327,6 +336,36 @@ zb_ret_t __wrap_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_re esphome::zigbee::global_zigbee->after_reporting_info(config_rep_req, attr_addr_info); return ret; } + +extern void __real_zb_trans_enter_sleep(void); +extern void __real_zb_trans_enter_receive(void); +extern zb_bool_t __real_zb_trans_transmit(zb_uint8_t wait_type, zb_time_t tx_at, zb_uint8_t *tx_buf, + zb_uint8_t current_channel); + +static uint32_t radio_sleep_start_ms = 0; + +static void stop_radio_sleep_timer() { + if (radio_sleep_start_ms) { + esphome::zigbee::global_zigbee->add_radio_sleep_time_ms(esphome::millis() - radio_sleep_start_ms); + } + radio_sleep_start_ms = 0; +} + +void __wrap_zb_trans_enter_sleep(void) { + __real_zb_trans_enter_sleep(); + radio_sleep_start_ms = esphome::millis(); +} + +void __wrap_zb_trans_enter_receive(void) { + stop_radio_sleep_timer(); + __real_zb_trans_enter_receive(); +} + +zb_bool_t __wrap_zb_trans_transmit(zb_uint8_t wait_type, zb_time_t tx_at, zb_uint8_t *tx_buf, + zb_uint8_t current_channel) { + stop_radio_sleep_timer(); + return __real_zb_trans_transmit(wait_type, tx_at, tx_buf, current_channel); +} // NOLINTEND(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp) } #endif diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index 3b4a465361..8528aebff8 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -81,6 +81,7 @@ class ZigbeeComponent final : public Component { void force_report(); void loop() override; void set_sleepy(bool sleepy) { this->sleepy_ = sleepy; } + void add_radio_sleep_time_ms(uint32_t ms); protected: static void zcl_device_cb(zb_bufid_t bufid); @@ -94,6 +95,8 @@ class ZigbeeComponent final : public Component { bool force_report_{false}; uint32_t sleep_time_{}; uint32_t sleep_remainder_{}; + uint32_t radio_sleep_time_{}; + uint32_t radio_sleep_remainder_{}; bool sleepy_{}; }; diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 1647fb28ae..f47cf6bd40 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -117,6 +117,14 @@ async def zephyr_to_code(config: ConfigType) -> "MockObj": cg.add_build_flag("-Wl,--wrap=zb_zcl_put_reporting_info_from_req") + # Wrap the transceiver sleep/receive/transmit calls to measure how long the + # radio is powered down. The span between a zb_trans_enter_sleep() and the + # following zb_trans_enter_receive() or zb_trans_transmit() is time the + # radio spent asleep. + cg.add_build_flag("-Wl,--wrap=zb_trans_enter_sleep") + cg.add_build_flag("-Wl,--wrap=zb_trans_enter_receive") + cg.add_build_flag("-Wl,--wrap=zb_trans_transmit") + if CONF_IEEE802154_VENDOR_OUI in config: zephyr_add_prj_conf("IEEE802154_VENDOR_OUI_ENABLE", True) random_number = config[CONF_IEEE802154_VENDOR_OUI] diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 8a24bd57d6..5f56861e6d 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -18,13 +18,22 @@ static const char *const TAG = "zwave_proxy"; static constexpr size_t ZWAVE_MAX_LOG_BYTES = 168; static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20; -// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...] +// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID(1 or 2)][...] +// We only read the home ID, so the node ID (1 byte in 8-bit mode, 2 bytes in 16-bit mode) and +// anything after it are not required to be present static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value -static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum +static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 7; // TYPE + CMD + HOME_ID(4) + checksum +static constexpr uint8_t ZWAVE_MIN_FRAME_LENGTH = 3; // TYPE + CMD + checksum (zero-payload frame) +static constexpr uint32_t ZWAVE_FRAME_TIMEOUT_MS = 1500; // Abandon a frame this long after its start (SOF) byte static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup static constexpr uint32_t RECONNECT_DELAY_MS = 500; // Delay between home ID query attempts after reconnect static constexpr uint8_t MAX_QUERY_RETRIES = 5; // Max attempts to query home ID after reconnect +static constexpr bool is_bootloader_menu_byte(uint8_t byte) { + // Bootloader menu output is printable ASCII plus CR/LF, ending with a NUL terminator + return byte == 0 || byte == '\r' || byte == '\n' || (byte >= 0x20 && byte <= 0x7E); +} + static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) { // Calculate Z-Wave frame checksum // XOR all bytes between SOF and checksum position (exclusive) @@ -74,6 +83,11 @@ bool ZWaveProxy::can_proceed() { const uint32_t now = App.get_loop_component_start_time(); if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) { ESP_LOGW(TAG, "Timeout reading Home ID during setup"); + // The modem may simply still be booting; keep querying from loop() using the same retry + // machinery as a reconnect. This adds no setup delay — clients are notified of the home ID + // via the HOME_ID_CHANGE message whenever it finally arrives. + this->reconnect_time_ = now; + this->query_retries_ = 0; return true; // Proceed anyway after timeout } @@ -98,7 +112,18 @@ void ZWaveProxy::loop() { } this->process_uart_(); - this->status_clear_warning(); + + // Abandon a stalled frame reception. The Z-Wave API specification requires a receiver to abort + // a data frame reception lasting more than 1500 ms after the SOF byte, without sending a NAK. + // Without this, the stale bytes would silently corrupt the next frame. Any SEND_* state was + // already resolved by response_handler_() above, so a state other than WAIT_START here always + // means we are mid-frame. + if (this->parsing_state_ != ZWAVE_PARSING_STATE_WAIT_START && + App.get_loop_component_start_time() - this->frame_start_time_ > ZWAVE_FRAME_TIMEOUT_MS) { + ESP_LOGW(TAG, "Timeout waiting for frame data; resetting parser"); + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + this->buffer_index_ = 0; + } } void ZWaveProxy::process_uart_slow_() { @@ -112,19 +137,24 @@ void ZWaveProxy::process_uart_slow_() { } if (this->parse_byte_(byte)) { // Check if this is a GET_NETWORK_IDS response frame - // Frame format: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...] + // Frame format: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID(1 or 2)][...] + // Bootloader output is excluded up front: a completed bootloader "frame" is menu text, so + // buffer_[1..3] would be meaningless (and possibly never written). Outside bootloader mode, + // the parser guarantees a completed frame starts with SOF, so buffer_[0] needs no check. // We verify: - // - buffer_[0]: Start of frame marker (0x01) - // - buffer_[1]: Length field must be >= 9 to contain all required data + // - buffer_[1]: Length field must be >= 7 so the frame contains the full home ID // - buffer_[2]: Command type (0x01 for response) // - buffer_[3]: Command ID (0x20 for GET_NETWORK_IDS) - if (this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS && this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE && - this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) { + if (!this->in_bootloader_ && this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && + this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE && this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS) { // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed // The frame parser has already validated the checksum and ensured all bytes are present if (this->set_home_id_(&this->buffer_[4])) { + char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)]; + ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size())); this->send_homeid_changed_msg_(); } + this->home_id_ready_ = true; } ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr)); if (this->api_connection_ != nullptr) { @@ -140,14 +170,19 @@ void ZWaveProxy::process_uart_slow_() { } } } while (this->available()); + // Reaching here means every read succeeded, so clear any earlier read-failure warning. + // (An early return on read failure skips this, leaving the warning visible until the + // next successful drain.) + this->status_clear_warning(); } void ZWaveProxy::dump_config() { char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)]; - ESP_LOGCONFIG(TAG, - "Z-Wave Proxy:\n" - " Home ID: %s", - format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size())); + ESP_LOGCONFIG( + TAG, + "Z-Wave Proxy:\n" + " Home ID: %s", + this->home_id_ready_ ? format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()) : "unknown"); } void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) { @@ -160,10 +195,20 @@ void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) { void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) { switch (type) { case api::enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: - if (this->api_connection_ != nullptr) { - ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + if (this->api_connection_ == api_connection) { + ESP_LOGV(TAG, "API connection is already subscribed"); return; } + if (this->api_connection_ != nullptr) { + // A living subscriber keeps exclusive access. Its connection may be dead without + // loop() having noticed yet (e.g. the client crashed and reconnected quickly); + // in that case let the new client take over instead of locking it out. + if (this->api_connection_->is_connection_setup()) { + ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + return; + } + ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription"); + } this->api_connection_ = api_connection; ESP_LOGV(TAG, "API connection is now subscribed"); break; @@ -222,6 +267,7 @@ void ZWaveProxy::retry_home_id_query_() { void ZWaveProxy::clear_home_id_() { static constexpr uint8_t ZERO_HOME_ID[ZWAVE_HOME_ID_SIZE] = {}; if (this->set_home_id_(ZERO_HOME_ID)) { + ESP_LOGV(TAG, "Home ID cleared"); this->send_homeid_changed_msg_(); } this->home_id_ready_ = false; @@ -237,13 +283,20 @@ bool ZWaveProxy::set_home_id_(const uint8_t *new_home_id) { return false; // No change } std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size()); - char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)]; - ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size())); - this->home_id_ready_ = true; return true; // Home ID was changed } -void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { +void ZWaveProxy::send_frame(api::APIConnection *api_connection, const uint8_t *data, size_t length) { + // Only the subscribed client may talk to the Z-Wave module; a frame from any other + // (authenticated but unsubscribed) client would interleave with the subscriber's traffic + if (api_connection != this->api_connection_) { + ESP_LOGW(TAG, "Ignoring frame from unsubscribed client"); + return; + } + this->send_frame_(data, length); +} + +void ZWaveProxy::send_frame_(const uint8_t *data, size_t length) { // Safety: validate pointer before any access if (data == nullptr) { ESP_LOGE(TAG, "Null data pointer"); @@ -289,7 +342,7 @@ void ZWaveProxy::send_simple_command_(const uint8_t command_id) { // Where LENGTH=0x03 (3 bytes: TYPE + CMD + CHECKSUM) uint8_t cmd[] = {0x01, 0x03, 0x00, command_id, 0x00}; cmd[4] = calculate_frame_checksum(cmd, sizeof(cmd)); - this->send_frame(cmd, sizeof(cmd)); + this->send_frame_(cmd, sizeof(cmd)); } bool ZWaveProxy::parse_byte_(uint8_t byte) { @@ -300,9 +353,12 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) { this->parse_start_(byte); break; case ZWAVE_PARSING_STATE_WAIT_LENGTH: - if (!byte) { + if (byte < ZWAVE_MIN_FRAME_LENGTH) { ESP_LOGW(TAG, "Invalid LENGTH: %u", byte); this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_NAK; + // Send the NAK now; otherwise any bytes already buffered behind this one would be + // silently discarded by the SEND_NAK case below until the next loop() iteration + this->response_handler_(); return false; } ESP_LOGVV(TAG, "Received LENGTH: %u", byte); @@ -319,7 +375,9 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) { case ZWAVE_PARSING_STATE_WAIT_COMMAND_ID: this->buffer_[this->buffer_index_++] = byte; ESP_LOGVV(TAG, "Received COMMAND ID: 0x%02X", byte); - this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_PAYLOAD; + // A zero-payload frame (LENGTH == 3) has its checksum immediately after the command ID + this->parsing_state_ = this->buffer_index_ >= this->end_frame_after_ ? ZWAVE_PARSING_STATE_WAIT_CHECKSUM + : ZWAVE_PARSING_STATE_WAIT_PAYLOAD; break; case ZWAVE_PARSING_STATE_WAIT_PAYLOAD: this->buffer_[this->buffer_index_++] = byte; @@ -347,12 +405,24 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) { break; } case ZWAVE_PARSING_STATE_READ_BL_MENU: - if (this->buffer_index_ >= this->buffer_.size()) { + // This state is tentative (see parse_start_): bootloader mode is committed only when a + // plausible menu — printable text ending in a NUL terminator — completes. A byte that + // cannot be menu text means the 0x0D that started this state was not a menu after all, + // so re-parse that byte as a frame start; it may be the SOF/ACK/NAK of real traffic. + if (this->buffer_index_ >= this->buffer_.size() || !is_bootloader_menu_byte(byte)) { this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + this->parse_start_(byte); break; } this->buffer_[this->buffer_index_++] = byte; if (!byte) { + if (!this->in_bootloader_) { + ESP_LOGD(TAG, "Entered bootloader mode"); + this->in_bootloader_ = true; + // Reset response deduplication: in bootloader mode, single-byte client writes (XMODEM + // ACK/NAK/CAN) are raw data and must never be suppressed as duplicate responses + this->last_response_ = 0; + } this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; frame_completed = true; } @@ -378,15 +448,16 @@ void ZWaveProxy::parse_start_(uint8_t byte) { ESP_LOGD(TAG, "Exited bootloader mode"); this->in_bootloader_ = false; } + this->frame_start_time_ = App.get_loop_component_start_time(); this->buffer_[this->buffer_index_++] = byte; this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_LENGTH; return; case ZWAVE_FRAME_TYPE_BL_MENU: ESP_LOGV(TAG, "Received BL_MENU"); - if (!this->in_bootloader_) { - ESP_LOGD(TAG, "Entered bootloader mode"); - this->in_bootloader_ = true; - } + // Read the menu tentatively: a stray 0x0D can equally appear in garbled data after the + // parser loses frame alignment, so bootloader mode is only committed once a plausible + // menu completes (see READ_BL_MENU handling in parse_byte_) + this->frame_start_time_ = App.get_loop_component_start_time(); this->buffer_[this->buffer_index_++] = byte; this->parsing_state_ = ZWAVE_PARSING_STATE_READ_BL_MENU; return; @@ -403,7 +474,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) { ESP_LOGV(TAG, "Received CAN"); break; default: - ESP_LOGW(TAG, "Unrecognized START: 0x%02X", byte); + ESP_LOGV(TAG, "Unrecognized START: 0x%02X", byte); return; } // Forward response (ACK/NAK/CAN) back to client for processing diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index ec52b15cd9..cb60139ef8 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -68,13 +68,16 @@ class ZWaveProxy final : public uart::UARTDevice, public Component { return encode_uint32(this->home_id_[0], this->home_id_[1], this->home_id_[2], this->home_id_[3]); } - void send_frame(const uint8_t *data, size_t length); + // Send a frame from an API client to the Z-Wave module. Frames from any connection other + // than the currently subscribed one are ignored. + void send_frame(api::APIConnection *api_connection, const uint8_t *data, size_t length); protected: - bool set_home_id_(const uint8_t *new_home_id); // Store a new home ID. Returns true if it changed. - void clear_home_id_(); // Clear home ID and notify API clients - void on_connection_changed_(bool connected); // Handle modem connect/disconnect transitions - void retry_home_id_query_(); // Retry home ID query after reconnect + void send_frame_(const uint8_t *data, size_t length); // Write a frame to the Z-Wave module + bool set_home_id_(const uint8_t *new_home_id); // Store a new home ID. Returns true if it changed. + void clear_home_id_(); // Clear home ID and notify API clients + void on_connection_changed_(bool connected); // Handle modem connect/disconnect transitions + void retry_home_id_query_(); // Retry home ID query after reconnect void send_homeid_changed_msg_(api::APIConnection *conn = nullptr); void send_simple_command_(uint8_t command_id); bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) @@ -114,6 +117,7 @@ class ZWaveProxy final : public uart::UARTDevice, public Component { api::APIConnection *api_connection_{nullptr}; // Current subscribed client uint32_t setup_time_{0}; // Time when setup() was called uint32_t reconnect_time_{0}; // Timestamp of reconnect detection (0 = no pending query) + uint32_t frame_start_time_{0}; // Timestamp of the current frame's start byte (reception timeout) // Small values (grouped by size to minimize padding) uint16_t buffer_index_{0}; // Index for populating the data buffer diff --git a/esphome/config.py b/esphome/config.py index fc8f46909f..976faed447 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -599,6 +599,18 @@ class LoadValidationStep(ConfigValidationStep): CORE.loaded_integrations.add(self.domain) # For platform components, normalize conf before creating MetadataValidationStep if component.is_platform_component: + # Legacy config migration: allow a platform component to rewrite a + # pre-platform-format top-level config (e.g. a bare list or legacy + # dict form) into the normalized list of `platform:` tagged entries. + # Removable deprecation shim hook; no-op for components that do not + # define LEGACY_CONFIG_MIGRATE. + if ( + (migrate := component.legacy_config_migrate) is not None + and self.conf + and not isinstance(self.conf, core.AutoLoad) + and (migrated := migrate(self.conf)) is not None + ): + result[self.domain] = self.conf = migrated if not self.conf: result[self.domain] = self.conf = [] elif not isinstance(self.conf, list): diff --git a/esphome/config_validation.py b/esphome/config_validation.py index b77e22a6fb..ff9170813c 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -71,7 +71,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_NRF52, - PLATFORM_RP2040, + PLATFORM_RP2, SCHEDULER_DONT_RUN, TYPE_GIT, TYPE_LOCAL, @@ -292,10 +292,14 @@ class Visibility(StrEnum): the same way. ESPHome itself ignores the value at runtime; consumers downstream of the schema dump act on it. - A field with no ``visibility`` set (the default) renders on the - editor's main form. The two values below are points along a - single axis of "how prominently to surface this": + Three points along a single axis of "how prominently to surface + this", from least to most hidden: + - ``UI`` — always render on the editor's main form. Use to + promote an ``Optional`` that would otherwise fall through to + the advanced disclosure (see the default rule below): the + "headline" config a user reaches for first (e.g. a sensor's + ``name`` or its primary pin/address). - ``ADVANCED`` — render under the editor's "advanced settings" disclosure. Use for fields whose default is right for ~all users (e.g. ``update_interval`` on time platforms — 15 min is @@ -307,25 +311,35 @@ class Visibility(StrEnum): tweaks can break boot). The YAML escape hatch stays available for the rare power-user override. - The single-axis shape encodes "yaml-only is strictly stronger - than advanced" at the type level — there's no way to ask for - both at once, and no way to set a contradictory state like - "advanced=False, yaml_only=True". + Default when unset (``visibility=None``): resolved by the + consumer, not encoded on the marker. A schema-aware editor + treats an ``Optional`` with no setting as ``ADVANCED`` (most + optional knobs have sensible defaults and would clutter the + form), and a ``Required`` with no setting as ``UI`` (a required + field needs the user's attention). Pass an explicit value to + override either default — most commonly ``UI`` to keep a + high-value ``Optional`` on the main form. + + The single-axis shape encodes the strictness ladder + (``UI`` < ``ADVANCED`` < ``YAML_ONLY``) at the type level — + there's no way to set a contradictory state. Per-field; the dumper walks recursively into nested schemas - and emits each field's setting independently. Cascading - semantics — "a stricter parent makes its descendants at-least - as strict" — belong on the consumer side: the schema marker - is faithfully what the field author wrote, and a consumer that - cares about effective visibility walks the parent chain and - takes the strictest setting. ``YAML_ONLY`` is strictly stronger - than ``ADVANCED``, which is strictly stronger than no setting. - Inner fields can declare their own visibility; an inner + and emits each field's setting independently, omitting the key + when unset so the dump stays compact and the per-field default + is the consumer's to apply. Cascading semantics — "a stricter + parent makes its descendants at-least as strict" — belong on the + consumer side: the schema marker is faithfully what the field + author wrote, and a consumer that cares about effective + visibility walks the parent chain and takes the strictest + setting. Inner fields can declare their own visibility; an inner ``YAML_ONLY`` under an ``ADVANCED`` parent stays ``YAML_ONLY``, - and the consumer's cascade keeps siblings under the parent at - ``ADVANCED`` regardless of their own (less-strict) setting. + and the consumer's cascade keeps a ``UI`` sibling under an + ``ADVANCED`` parent at ``ADVANCED`` regardless of its own + (less-strict) setting. """ + UI = "ui" ADVANCED = "advanced" YAML_ONLY = "yaml_only" @@ -347,6 +361,9 @@ class Optional(vol.Optional): See :class:`Visibility` for the ``visibility`` kwarg — a UI hint for schema-driven editors that doesn't affect validation. + Left unset, an ``Optional`` is treated as ``Visibility.ADVANCED`` + by schema-aware editors; pass ``Visibility.UI`` to keep it on the + main form. """ def __init__( @@ -369,9 +386,11 @@ class Required(vol.Required): See :class:`Visibility` for the ``visibility`` kwarg — a UI hint for schema-driven editors that doesn't affect validation. - Required fields rarely need it (a required field by definition - needs the user's attention) but the kwarg is exposed for - symmetry so consumers can apply uniform logic across key markers. + Required fields rarely need it: left unset, a ``Required`` is + treated as on the main form (``Visibility.UI``) by schema-aware + editors, since a required field needs the user's attention. The + kwarg is exposed for symmetry so consumers can apply uniform + logic across key markers. """ def __init__( @@ -403,12 +422,14 @@ class Version: @classmethod def parse(cls, value: str) -> Version: - match = re.match(r"^(\d+).(\d+).(\d+)[-.]?(\w*)$", value) + # The patch component is optional and defaults to 0, so "6.0" and + # "6.0-rc1" parse as 6.0.0 and 6.0.0-rc1. + match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?[-.]?(\w*)$", value) if match is None: raise ValueError(f"Not a valid version number {value}") major = int(match[1]) minor = int(match[2]) - patch = int(match[3]) + patch = int(match[3] or 0) extra = match[4] or "" return Version(major=major, minor=minor, patch=patch, extra=extra) @@ -859,7 +880,38 @@ def only_with_framework( only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) only_on_nrf52 = only_on(PLATFORM_NRF52) -only_on_rp2040 = only_on(PLATFORM_RP2040) +only_on_rp2 = only_on(PLATFORM_RP2) + +# CORE.data key for the "deprecation warning already fired this run" flag. +# Deduped via CORE.data (cleared between runs) to match the framework-alias +# pattern; one warning per `esphome config|compile|run` invocation is enough. +_ONLY_ON_RP2040_DEPRECATED_KEY = "_cv_only_on_rp2040_deprecated_warned" + + +def only_on_rp2040(obj): + """Deprecated — kept as a back-compat shim for external custom components. + + Pre-RP2350, this was the family check for the RP2 platform; with RP2350 + landing under the same target platform, the variant axis is now exposed + by the rp2 component itself. New code should use one of: + + * :func:`only_on_rp2` — family-level gate (matches the esp32 pattern; + same semantics as the pre-RP2350 ``only_on_rp2040``). + * ``rp2.only_on_variant(supported=[VARIANT_RP2040])`` — variant-level + gate, rejects RP2350 boards on the rp2 platform. + + Scheduled for removal in 2027.7.0. + """ + if not CORE.data.get(_ONLY_ON_RP2040_DEPRECATED_KEY): + _LOGGER.warning( + "cv.only_on_rp2040 is deprecated; use cv.only_on_rp2 for the " + "family gate, or rp2.only_on_variant(supported=[VARIANT_RP2040]) " + "for the variant gate. Removed in 2027.7.0." + ) + CORE.data[_ONLY_ON_RP2040_DEPRECATED_KEY] = True + return only_on_rp2(obj) + + only_with_arduino = only_with_framework(Framework.ARDUINO) @@ -1763,6 +1815,8 @@ def one_of(*values, **kwargs): - *int* (``bool``, default=False): Whether to convert the incoming values to integers. - *float* (``bool``, default=False): Whether to convert the incoming values to floats. - *space* (``str``, default=' '): What to convert spaces in the input string to. + - *underscore* (``str``, default='_'): What to convert underscores in the input string to. + - *hyphen* (``str``, default='-'): What to convert hyphens in the input string to. """ options = ", ".join(f"'{x}'" for x in values) lower = kwargs.pop("lower", False) @@ -1771,8 +1825,11 @@ def one_of(*values, **kwargs): to_int = kwargs.pop("int", False) to_float = kwargs.pop("float", False) space = kwargs.pop("space", " ") + underscore = kwargs.pop("underscore", "_") + hyphen = kwargs.pop("hyphen", "-") if kwargs: raise ValueError + separators = str.maketrans({" ": space, "_": underscore, "-": hyphen}) @schema_extractor("one_of") def validator(value): @@ -1781,7 +1838,7 @@ def one_of(*values, **kwargs): if string_: value = string(value) - value = value.replace(" ", space) + value = value.translate(separators) if to_int: value = int_(value) if to_float: @@ -1886,14 +1943,29 @@ def dimensions(value): return dimensions([match.group(1), match.group(2)]) +def _remap_bundle_path(value: str) -> Path | None: + """Resolve a path from the machine an extracted bundle was created on. + + An absolute path in a config compiled from an extracted bundle may point + at the machine the bundle was created on; the bundle ships the file at + its config-relative location instead. + """ + from esphome.bundle import remap_bundle_path + + return remap_bundle_path(value) + + def directory(value: object) -> Path: value = string(value) path = CORE.relative_config_path(value) if not path.exists(): - raise Invalid( - f"Could not find directory '{path}'. Please make sure it exists (full path: {path.resolve()})." - ) + remapped = _remap_bundle_path(value) + if remapped is None: + raise Invalid( + f"Could not find directory '{path}'. Please make sure it exists (full path: {path.resolve()})." + ) + path = remapped if not path.is_dir(): raise Invalid( f"Path '{path}' is not a directory (full path: {path.resolve()})." @@ -1906,9 +1978,12 @@ def file_(value: object) -> Path: path = CORE.relative_config_path(value) if not path.exists(): - raise Invalid( - f"Could not find file '{path}'. Please make sure it exists (full path: {path.resolve()})." - ) + remapped = _remap_bundle_path(value) + if remapped is None: + raise Invalid( + f"Could not find file '{path}'. Please make sure it exists (full path: {path.resolve()})." + ) + path = remapped if not path.is_file(): raise Invalid(f"Path '{path}' is not a file (full path: {path.resolve()}).") return path @@ -1990,7 +2065,24 @@ def _get_default_key(*args): class SplitDefault(Optional): - """Mark this key to have a split default for ESP8266/ESP32.""" + """Mark this key to have a split default per target platform / variant / framework. + + Defaults are passed as kwargs keyed on the platform identifier; the most + specific match wins. Lookup order (first hit wins): + + 1. ``__`` — e.g. ``esp32_c3_arduino``, + ``rp2_2040_arduino`` + 2. ``_`` — e.g. ``esp32_c3``, ``rp2_2040`` + 3. ``_`` — e.g. ``esp32_arduino``, + ``rp2_arduino`` + 4. ```` — e.g. ``esp32``, ``rp2`` + + For ESP32 the variant strips the ``ESP32`` prefix from + :data:`esp32.VARIANT_*` constants (``ESP32C3`` → ``c3``). For RP2 the + variant strips just ``RP`` (``RP2040`` → ``2040``, ``RP2350`` → ``2350``) + so kwargs read naturally — `rp2_2040=...` is the override for the + Pico / Pico W and `rp2_2350=...` is the override for the Pico 2. + """ def __init__(self, key, **kwargs): super().__init__(key) @@ -2012,6 +2104,22 @@ class SplitDefault(Optional): keys += _get_default_key(variant, framework) keys += _get_default_key(variant) keys += _get_default_key(framework) + elif CORE.is_rp2: + # Strip the "RP" prefix to leave the chip number, mirroring + # the ESP32 "platform stripped from variant" convention so + # kwargs stay short (``rp2_2040`` rather than ``rp2_rp2040``). + # Variant lookup is defensive: validators may run before the + # rp2 component's ``set_core_data`` (or in tests that wire a + # partial ``CORE.data``); in that case we just skip the + # variant-specific keys and fall through to the base + # platform/framework defaults. + raw_variant = CORE.data.get("rp2", {}).get("variant") + framework = CORE.target_framework + if raw_variant: + variant = raw_variant.removeprefix("RP").lower() + keys += _get_default_key(variant, framework) + keys += _get_default_key(variant) + keys += _get_default_key(framework) keys += _get_default_key() for key in keys: if self._defaults.get(key) is not None: @@ -2210,16 +2318,25 @@ MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema( } ) +# Per-entity MQTT plumbing — integration metadata, never a primary UI field. MQTT_COMPONENT_SCHEMA = Schema( { - Optional(CONF_QOS): All(requires_component("mqtt"), mqtt_qos), - Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean), - Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean), - Optional(CONF_SUBSCRIBE_QOS): All(requires_component("mqtt"), mqtt_qos), - Optional(CONF_STATE_TOPIC): All( + Optional(CONF_QOS, visibility=Visibility.ADVANCED): All( + requires_component("mqtt"), mqtt_qos + ), + Optional(CONF_RETAIN, visibility=Visibility.ADVANCED): All( + requires_component("mqtt"), boolean + ), + Optional(CONF_DISCOVERY, visibility=Visibility.ADVANCED): All( + requires_component("mqtt"), boolean + ), + Optional(CONF_SUBSCRIBE_QOS, visibility=Visibility.ADVANCED): All( + requires_component("mqtt"), mqtt_qos + ), + Optional(CONF_STATE_TOPIC, visibility=Visibility.ADVANCED): All( requires_component("mqtt"), templatable(publish_topic) ), - Optional(CONF_AVAILABILITY): All( + Optional(CONF_AVAILABILITY, visibility=Visibility.ADVANCED): All( requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA) ), } @@ -2227,10 +2344,12 @@ MQTT_COMPONENT_SCHEMA = Schema( MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( { - Optional(CONF_COMMAND_TOPIC): All( + Optional(CONF_COMMAND_TOPIC, visibility=Visibility.ADVANCED): All( requires_component("mqtt"), templatable(subscribe_topic) ), - Optional(CONF_COMMAND_RETAIN): All(requires_component("mqtt"), boolean), + Optional(CONF_COMMAND_RETAIN, visibility=Visibility.ADVANCED): All( + requires_component("mqtt"), boolean + ), } ) @@ -2305,12 +2424,16 @@ def string_no_slash(value): ENTITY_BASE_SCHEMA = Schema( { - Optional(CONF_NAME): _validate_entity_name, - Optional(CONF_INTERNAL): boolean, - Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean, - Optional(CONF_ICON): icon, - Optional(CONF_ENTITY_CATEGORY): entity_category, - Optional(CONF_DEVICE_ID): sub_device_id, + # The name is every entity's headline field — keep it on the + # main form rather than letting it fall through to advanced. + Optional(CONF_NAME, visibility=Visibility.UI): _validate_entity_name, + Optional(CONF_INTERNAL, visibility=Visibility.ADVANCED): boolean, + Optional( + CONF_DISABLED_BY_DEFAULT, default=False, visibility=Visibility.ADVANCED + ): boolean, + Optional(CONF_ICON, visibility=Visibility.ADVANCED): icon, + Optional(CONF_ENTITY_CATEGORY, visibility=Visibility.ADVANCED): entity_category, + Optional(CONF_DEVICE_ID, visibility=Visibility.ADVANCED): sub_device_id, } ) @@ -2392,11 +2515,16 @@ def git_ref(value): return value +# What `refresh: never` validates to; also used to recognize a disabled +# refresh when logging (see esphome/git.py) +SOURCE_REFRESH_NEVER = "365250d" + + def source_refresh(value: str): if value.lower() == "always": return source_refresh("0s") if value.lower() == "never": - return source_refresh("365250d") + return source_refresh(SOURCE_REFRESH_NEVER) return positive_time_period_seconds(value) @@ -2443,18 +2571,58 @@ def require_framework_version( extra_message=None, **kwargs, ): + """Constrain the configured framework version per target platform / variant. + + Kwargs are keyed by ``_`` (e.g. ``esp32_arduino``, + ``rp2_arduino``) with optional variant-specific overrides keyed by + ``__`` (e.g. ``esp32_c3_arduino``, + ``rp2_2040_arduino``, ``rp2_2350_arduino``). Variant overrides win when + the configured variant matches; otherwise the base platform key is used. + + Special cases: ``host`` (with host framework) and ``esp_idf`` (any ESP32 + on ESP-IDF) bypass variant lookup. + """ + def validator(value): core_data = CORE.data[KEY_CORE] framework = core_data[KEY_TARGET_FRAMEWORK] + keys_to_try: list[str] = [] if CORE.is_host and framework == "host": - key = "host" + keys_to_try.append("host") elif framework == "esp-idf": - key = "esp_idf" + keys_to_try.append("esp_idf") else: - key = CORE.target_platform + "_" + framework + # Try variant-specific key first (mirrors the SplitDefault + # precedence). ESP32 strips its platform prefix from variant + # constants; RP2 strips just ``RP`` to keep chip-number kwargs + # (``rp2_2040``, ``rp2_2350``). + if CORE.is_esp32: + from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant - if key not in kwargs: + # Guard against tests that wire CORE.data without an + # esp32 variant block; same defensive intent as the rp2 + # branch below. + try: + variant = get_esp32_variant().replace(VARIANT_ESP32, "").lower() + except (KeyError, AttributeError): + variant = "" + if variant: + keys_to_try.append(f"{CORE.target_platform}_{variant}_{framework}") + elif CORE.is_rp2: + # Defensive lookup — see the matching block in + # ``SplitDefault.default``: the rp2 component's + # ``set_core_data`` may not have populated + # ``CORE.data["rp2"]["variant"]`` yet (validators run + # during schema validation, before code-gen). + raw_variant = CORE.data.get("rp2", {}).get("variant") + if raw_variant: + variant = raw_variant.removeprefix("RP").lower() + keys_to_try.append(f"{CORE.target_platform}_{variant}_{framework}") + keys_to_try.append(f"{CORE.target_platform}_{framework}") + + key = next((k for k in keys_to_try if k in kwargs), None) + if key is None: msg = f"This feature is incompatible with {CORE.target_platform.upper()} using {framework} framework" if extra_message: msg += f". {extra_message}" @@ -2560,10 +2728,32 @@ SOURCE_SCHEMA = Any( ) -def rename_key(old_key, new_key): +def rename_key( + old_key, new_key, *, removed_in: str | None = None, component: str | None = None +): + """Rename a config key from ``old_key`` to ``new_key``. + + Specifying both keys is an error; otherwise only one of the two would + survive the rename and the other would be dropped silently. + + When ``removed_in`` is set, a deprecation warning is logged if the old key is + present. Pass ``component`` (the platform/component name) alongside + ``removed_in`` so the warning identifies where it originates. + """ + def validator(config: dict) -> dict: config = config.copy() if old_key in config: + has_at_most_one_key(old_key, new_key)(config) + if removed_in is not None: + prefix = f"[{component}] " if component else "" + _LOGGER.warning( + "%s'%s' is deprecated, use '%s'. Will be removed in %s", + prefix, + old_key, + new_key, + removed_in, + ) config[new_key] = config.pop(old_key) return config diff --git a/esphome/const.py b/esphome/const.py index 331eb5011d..a7306edeb0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.7.0-dev" +__version__ = "2026.8.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -33,7 +33,12 @@ class Platform(StrEnum): LIBRETINY_OLDSTYLE = "libretiny" LN882X = "ln882x" NRF52 = "nrf52" - RP2040 = "rp2040" + RP2 = "rp2" # canonical name for the RP2 family (RP2040, RP2350, …) + # Deprecated: use Platform.RP2 instead. Python enum aliasing makes this + # the same member as RP2 (same string value), so ``Platform.RP2040`` and + # ``Platform.RP2`` remain interchangeable for external custom components. + # Scheduled for removal in 2027.7.0. + RP2040 = "rp2" RTL87XX = "rtl87xx" @@ -86,6 +91,9 @@ class PlatformFramework(Enum): # Arduino framework platforms ESP8266_ARDUINO = (Platform.ESP8266, Framework.ARDUINO) + RP2_ARDUINO = (Platform.RP2, Framework.ARDUINO) + # Deprecated: use PlatformFramework.RP2_ARDUINO instead. Kept as an + # alias for backwards compatibility; scheduled for removal in 2027.7.0. RP2040_ARDUINO = (Platform.RP2040, Framework.ARDUINO) BK72XX_ARDUINO = (Platform.BK72XX, Framework.ARDUINO) RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) @@ -106,6 +114,9 @@ PLATFORM_HOST = Platform.HOST PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE PLATFORM_LN882X = Platform.LN882X PLATFORM_NRF52 = Platform.NRF52 +PLATFORM_RP2 = Platform.RP2 +# Deprecated: use PLATFORM_RP2 instead. Kept as a back-compat alias; +# scheduled for removal in 2027.7.0. PLATFORM_RP2040 = Platform.RP2040 PLATFORM_RTL87XX = Platform.RTL87XX @@ -976,6 +987,7 @@ CONF_STEP_PIN = "step_pin" CONF_STILL_THRESHOLD = "still_threshold" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" +CONF_STORAGE = "storage" CONF_STORE_BASELINE = "store_baseline" CONF_SUBNET = "subnet" CONF_SUBSCRIBE_QOS = "subscribe_qos" @@ -1236,6 +1248,7 @@ UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" +UNIT_KILOPASCAL = "kPa" UNIT_KILOVOLT_AMPS = "kVA" UNIT_KILOVOLT_AMPS_HOURS = "kVAh" UNIT_KILOVOLT_AMPS_REACTIVE = "kvar" @@ -1243,6 +1256,7 @@ UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" UNIT_LITRE = "L" +UNIT_LITRE_PER_HOUR = "L/h" UNIT_LITRE_PER_SECOND = "L/s" UNIT_LUX = "lx" UNIT_MEGAJOULE = "MJ" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 21ff7ef07c..bf637d4c1f 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -8,6 +8,7 @@ import re from typing import TYPE_CHECKING, Any from esphome.const import ( + CONF_BUILD_PATH, CONF_COMMENT, CONF_ESPHOME, CONF_ETHERNET, @@ -25,7 +26,7 @@ from esphome.const import ( PLATFORM_HOST, PLATFORM_LN882X, PLATFORM_NRF52, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_RTL87XX, Toolchain, ) @@ -52,6 +53,11 @@ _LOGGER = logging.getLogger(__name__) # Key for tracking controller count in CORE.data for ControllerRegistry StaticVector sizing KEY_CONTROLLER_REGISTRY_COUNT = "controller_registry_count" +# CORE.data key for the "is_rp2040 deprecation warning already fired this +# run" flag. Mirrors the ``cv.only_on_rp2040`` dedupe pattern; cleared +# between runs so each fresh invocation warns once. +_IS_RP2040_DEPRECATED_KEY = "_core_is_rp2040_deprecated_warned" + class EsphomeError(Exception): """General ESPHome exception occurred.""" @@ -569,6 +575,9 @@ class EsphomeCore: self.build_path: Path | None = None # The validated configuration, this is None until the config has been validated self.config: ConfigType | None = None + # The raw configuration as read from YAML (after packages/substitutions), + # available during validation before the config is fully validated + self.raw_config: ConfigType | None = None # YAML frontmatter loaded from user YAML files. Frontmatter is a leading # YAML document separated by `---` from the actual configuration. It is # ignored by config validation and code generation, but kept here so it @@ -591,6 +600,9 @@ class EsphomeCore: self.platformio_libraries: dict[str, Library] = {} # A set of build flags to set in the platformio project self.build_flags: set[str] = set() + # A set of build flags that apply to C++ compiles only (CXXFLAGS / + # CXX_COMPILE_OPTIONS), for flags GCC rejects or warns about on C + self.cxx_build_flags: set[str] = set() # A set of build unflags to set in the platformio project self.build_unflags: set[str] = set() # The C++ language standard for the build (e.g. "gnu++20"), set via cg.set_cpp_standard() @@ -642,6 +654,7 @@ class EsphomeCore: self.config_path = None self.build_path = None self.config = None + self.raw_config = None self.frontmatter = {} self.event_loop = _FakeEventLoop() self.task_counter = 0 @@ -650,6 +663,7 @@ class EsphomeCore: self.global_statements = [] self.platformio_libraries = {} self.build_flags = set() + self.cxx_build_flags = set() self.build_unflags = set() self.cpp_standard = None self.defines = set() @@ -718,12 +732,28 @@ class EsphomeCore: The hash is computed lazily and cached for performance. Uses sort_keys=True to ensure deterministic ordering. + + The hash must be reproducible across machines so the device builder + can compare a locally computed hash against the one a device + advertises. Machine-local data is kept out of the input: build_path + (which embeds ESPHOME_BUILD_PATH and OS path separators) is excluded, + and Path values are dumped relative to the config directory. """ if self._config_hash is None: from esphome import yaml_util from esphome.helpers import fnv1a_32bit_hash - config_str = yaml_util.dump(self.config, show_secrets=True, sort_keys=True) + config = dict(self.config) + if (esphome_conf := config.get(CONF_ESPHOME)) is not None: + esphome_conf = dict(esphome_conf) + esphome_conf.pop(CONF_BUILD_PATH, None) + config[CONF_ESPHOME] = esphome_conf + config_str = yaml_util.dump( + config, + show_secrets=True, + sort_keys=True, + relative_to=self.config_dir if self.config_path is not None else None, + ) self._config_hash = fnv1a_32bit_hash(config_str) return self._config_hash @@ -826,9 +856,38 @@ class EsphomeCore: def is_esp32(self): return self.target_platform == PLATFORM_ESP32 + @property + def is_rp2(self): + """Return True if the target platform is the RP2 chip family. + + Canonical umbrella check covering RP2040, RP2350, and any future + RP2-series chip. Mirrors :attr:`is_esp32` for the ESP32 family. + For variant-specific gating (RP2040 vs RP2350), use + ``rp2.get_rp2040_variant()`` or ``rp2.only_on_variant(...)`` from + the rp2 component — variant detection doesn't belong on ``CORE``. + """ + return self.target_platform == PLATFORM_RP2 + @property def is_rp2040(self): - return self.target_platform == PLATFORM_RP2040 + """Deprecated: use :attr:`is_rp2` for the family check, or + ``rp2.get_rp2040_variant() == rp2.VARIANT_RP2040`` for the + variant-specific check. Kept as an alias since pre-RP2350 + callers used it as a family check, identical to ``is_rp2``. + + Scheduled for removal in 2027.7.0. Logs a one-shot deprecation + warning per run (deduped via ``self.data`` so repeated reads in + the same invocation don't spam) to match the parallel + ``cv.only_on_rp2040`` shim. + """ + if not self.data.get(_IS_RP2040_DEPRECATED_KEY): + _LOGGER.warning( + "CORE.is_rp2040 is deprecated; use CORE.is_rp2 for the family " + "gate, or rp2.get_rp2040_variant() == rp2.VARIANT_RP2040 for " + "the variant-specific check. Removed in 2027.7.0." + ) + self.data[_IS_RP2040_DEPRECATED_KEY] = True + return self.is_rp2 @property def is_bk72xx(self): @@ -957,6 +1016,11 @@ class EsphomeCore: _LOGGER.debug("Adding build flag: %s", build_flag) return build_flag + def add_cxx_build_flag(self, build_flag: str) -> str: + self.cxx_build_flags.add(build_flag) + _LOGGER.debug("Adding C++ build flag: %s", build_flag) + return build_flag + def add_build_unflag(self, build_unflag: str) -> None: if self.using_toolchain_esp_idf: # The native ESP-IDF build generator does not consume build_unflags diff --git a/esphome/core/application.h b/esphome/core/application.h index 76af514511..a12cdc4ac8 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -612,6 +612,8 @@ class LoopBlockingGuard { uint32_t blocking_time = curr_time - App.get_loop_component_start_time(); if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { warn_blocking(blocking_time); + // Exclude synchronous warning-log time from the next operation. + curr_time = MillisInternal::get(); } #endif return curr_time; diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index cf8b05a300..276b8aa972 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -201,7 +201,7 @@ template class DelayAction : public Action { /* component= */ nullptr, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::SELF_POINTER, /* static_name= */ reinterpret_cast(this), /* hash_or_id= */ 0, this->delay_.value(), [this]() { this->play_next_(); }, - /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1, + /* skip_cancel= */ this->num_running_ > 1, // Record the owning script (if any) so the blocking warning can name it; propagates across // chained delays via the scheduler. /* source= */ App.get_current_source()); @@ -215,7 +215,7 @@ template class DelayAction : public Action { /* component= */ nullptr, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::SELF_POINTER, /* static_name= */ reinterpret_cast(this), /* hash_or_id= */ 0, this->delay_.value(x...), std::move(f), - /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1, + /* skip_cancel= */ this->num_running_ > 1, // See the no-argument branch above: record the owning script for log attribution. /* source= */ App.get_current_source()); } @@ -502,6 +502,9 @@ template class WaitUntilAction : public Action, public Co void stop() override { this->var_queue_.clear(); + // Tell any process_queue_() call further down the stack that the items it is + // still holding were cancelled + this->stop_generation_++; this->disable_loop(); } @@ -511,33 +514,57 @@ template class WaitUntilAction : public Action, public Co } protected: + using QueueItem = std::tuple, std::tuple>; + // Helper: Process queue, triggering completed items and removing them // Returns true if queue still has pending items bool process_queue_(uint32_t now) { - // Process each queued wait_until and remove completed ones - this->var_queue_.remove_if([&](auto &queued) { - auto start = std::get(queued); - auto timeout = std::get>(queued); - auto &var = std::get>(queued); + // Completed items run the rest of the action chain synchronously, and that chain + // can re-enter this same action (e.g. a script with mode: restart that executes + // itself) and add to or clear var_queue_. Iterating the member list directly would + // then corrupt it, so move it aside and iterate a local list instead. + std::list queue; + queue.swap(this->var_queue_); + std::list pending; + while (!queue.empty()) { + auto it = queue.begin(); + auto start = std::get(*it); + auto timeout = std::get>(*it); // Check if timeout has expired auto expired = timeout && (now - start) >= *timeout; // Keep waiting if not expired and condition not met - if (!expired && !this->condition_->check_tuple(var)) { - return false; + if (!expired && !this->condition_->check_tuple(std::get>(*it))) { + pending.splice(pending.end(), queue, it); + continue; } - // Condition met or timed out - trigger next action - this->play_next_tuple_(var); - return true; - }); + // Condition met or timed out - trigger the next action. Keep the item in a local + // holder so its arguments stay valid while the chain runs, without any nested + // process_queue_() call being able to see (and fire) it again. + std::list completed; + completed.splice(completed.begin(), queue, it); + uint8_t generation = this->stop_generation_; + this->play_next_tuple_(std::get>(completed.front())); + if (generation != this->stop_generation_) { + // stop() ran inside the chain - the items still held locally were cancelled + pending.clear(); + break; + } + } + + // Re-entrant continuations may have enqueued new waits into var_queue_; put the + // older still-waiting items back in front of them to keep FIFO firing order + this->var_queue_.splice(this->var_queue_.begin(), pending); return !this->var_queue_.empty(); } Condition *condition_; - std::list, std::tuple>> var_queue_{}; + std::list var_queue_{}; + // Bumped by stop() so process_queue_() can detect a stop from inside play_next_tuple_() + uint8_t stop_generation_{0}; }; template class UpdateComponentAction : public Action { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 281d7aaecd..e5fbb8ba07 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -93,36 +93,6 @@ bool Component::cancel_interval(const char *name) { // NOLINT return App.scheduler.cancel_interval(this, name); } -void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function &&f, float backoff_increase_factor) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); -#pragma GCC diagnostic pop -} - -void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function &&f, float backoff_increase_factor) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); -#pragma GCC diagnostic pop -} - -bool Component::cancel_retry(const std::string &name) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return App.scheduler.cancel_retry(this, name); -#pragma GCC diagnostic pop -} - -bool Component::cancel_retry(const char *name) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return App.scheduler.cancel_retry(this, name); -#pragma GCC diagnostic pop -} - void Component::set_timeout(const char *name, uint32_t timeout, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, timeout, std::move(f)); } @@ -156,21 +126,6 @@ void Component::set_interval(InternalSchedulerID id, uint32_t interval, std::fun bool Component::cancel_interval(InternalSchedulerID id) { return App.scheduler.cancel_interval(this, id); } -void Component::set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, - std::function &&f, float backoff_increase_factor) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - App.scheduler.set_retry(this, id, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); -#pragma GCC diagnostic pop -} - -bool Component::cancel_retry(uint32_t id) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return App.scheduler.cancel_retry(this, id); -#pragma GCC diagnostic pop -} - void Component::call_setup() { this->setup(); } void Component::call_dump_config_() { this->dump_config(); @@ -307,13 +262,6 @@ void Component::set_timeout(uint32_t timeout, std::function &&f) { // N void Component::set_interval(uint32_t interval, std::function &&f) { // NOLINT App.scheduler.set_interval(this, static_cast(nullptr), interval, std::move(f)); } -void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, - float backoff_increase_factor) { // NOLINT -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); -#pragma GCC diagnostic pop -} bool Component::is_ready() const { // Bitmask check: valid states are SETUP(1), LOOP(2), LOOP_DONE(4) // (1 << state) & 0b10110 checks membership in one instruction diff --git a/esphome/core/component.h b/esphome/core/component.h index 70a051ca0b..ecaf863ecf 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -96,8 +96,6 @@ inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; // decide whether to propagate clears to App.app_state_. Never set on a // Component's component_state_. inline constexpr uint8_t APP_STATE_SETUP_COMPLETE = 0x40; -// Remove before 2026.8.0 -enum class RetryResult { DONE, RETRY }; inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds (1cs = 10ms) @@ -410,41 +408,6 @@ class Component { bool cancel_interval(uint32_t id); // NOLINT bool cancel_interval(InternalSchedulerID id); // NOLINT - /// @deprecated set_retry is deprecated. Use set_timeout or set_interval instead. Removed in 2026.8.0. - // Remove before 2026.8.0 - ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", - "2026.2.0") - void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT - std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT - - // Remove before 2026.8.0 - ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", - "2026.2.0") - void set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT - std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT - - // Remove before 2026.8.0 - ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", - "2026.2.0") - void set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT - std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT - - // Remove before 2026.8.0 - ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", - "2026.2.0") - void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, // NOLINT - float backoff_increase_factor = 1.0f); // NOLINT - - // Remove before 2026.8.0 - ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") - bool cancel_retry(const std::string &name); // NOLINT - // Remove before 2026.8.0 - ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") - bool cancel_retry(const char *name); // NOLINT - // Remove before 2026.8.0 - ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") - bool cancel_retry(uint32_t id); // NOLINT - /** Set a timeout function with a const char* name. * * Similar to javascript's setTimeout(). Empty name means no cancelling possible. diff --git a/esphome/core/config.py b/esphome/core/config.py index 0670fde0ff..6b24a55487 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -284,14 +284,24 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_COMMENT): cv.All( cv.string, cv.ByteLength(max=COMMENT_MAX_LEN) ), - cv.Required(CONF_BUILD_PATH): cv.string, - cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( + cv.Required(CONF_BUILD_PATH, visibility=cv.Visibility.YAML_ONLY): cv.string, + cv.Optional( + CONF_PLATFORMIO_OPTIONS, + default={}, + visibility=cv.Visibility.YAML_ONLY, + ): cv.Schema( { cv.string_strict: cv.Any([cv.string], cv.string), } ), - cv.Optional(CONF_BUILD_FLAGS, default=[]): cv.ensure_list(cv.string_strict), - cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema( + cv.Optional( + CONF_BUILD_FLAGS, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list(cv.string_strict), + cv.Optional( + CONF_ENVIRONMENT_VARIABLES, + default={}, + visibility=cv.Visibility.YAML_ONLY, + ): cv.Schema( { cv.string_strict: cv.string, } @@ -313,12 +323,20 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoopTrigger), } ), - cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include), - cv.Optional(CONF_INCLUDES_C, default=[]): cv.ensure_list(valid_include), - cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict), + cv.Optional( + CONF_INCLUDES, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list(valid_include), + cv.Optional( + CONF_INCLUDES_C, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list(valid_include), + cv.Optional( + CONF_LIBRARIES, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list(cv.string_strict), cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean, cv.Optional(CONF_MERGE_WARNINGS, default=True): cv.boolean, - cv.Optional(CONF_DEBUG_SCHEDULER, default=False): cv.boolean, + cv.Optional( + CONF_DEBUG_SCHEDULER, default=False, visibility=cv.Visibility.YAML_ONLY + ): cv.boolean, cv.Optional(CONF_PROJECT): cv.Schema( { cv.Required(CONF_NAME): cv.All( @@ -338,11 +356,15 @@ CONFIG_SCHEMA = cv.All( ), } ), - cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All( - cv.version_number, cv.validate_esphome_version - ), cv.Optional( - CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default + CONF_MIN_VERSION, + default=ESPHOME_VERSION, + visibility=cv.Visibility.ADVANCED, + ): cv.All(cv.version_number, cv.validate_esphome_version), + cv.Optional( + CONF_COMPILE_PROCESS_LIMIT, + default=_compile_process_limit_default, + visibility=cv.Visibility.ADVANCED, ): cv.int_range(min=1, max=get_usable_cpu_count()), cv.Optional(CONF_AREAS, default=[]): cv.ensure_list(AREA_SCHEMA), cv.Optional(CONF_DEVICES, default=[]): cv.ensure_list(DEVICE_SCHEMA), @@ -723,6 +745,15 @@ async def to_code(config: ConfigType) -> None: cg.add_build_flag("-Wno-unused-variable") cg.add_build_flag("-Wno-unused-but-set-variable") cg.add_build_flag("-Wno-sign-compare") + # C++20 deprecated ++/--, compound assignment, and chained assignment on + # volatile lvalues; GCC warns via -Wvolatile, on by default at gnu++20. + # C++23 (P2327R1) removed the deprecation for compound assignment, so the + # warning flags patterns that are valid again under newer standards. + # C++-only flag: GCC warns when it is passed on a C compile, hence + # add_cxx_build_flag. Skipped for host builds, where the compiler may be + # clang, which does not know this GCC option. + if not CORE.is_host: + cg.add_cxx_build_flag("-Wno-volatile") if config[CONF_DEBUG_SCHEDULER]: cg.add_define("ESPHOME_DEBUG_SCHEDULER") @@ -859,8 +890,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( "wake/wake_esp8266.cpp": { PlatformFramework.ESP8266_ARDUINO, }, - "wake/wake_rp2040.cpp": { - PlatformFramework.RP2040_ARDUINO, + "wake/wake_rp2.cpp": { + PlatformFramework.RP2_ARDUINO, }, "wake/wake_host.cpp": { PlatformFramework.HOST_NATIVE, diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1c0138f9d1..65726bdb1d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -19,13 +19,13 @@ // Threading model for static analysis. Match what the real codegen picks per // platform (see esphome/components//__init__.py ThreadModel.*): -// USE_ESP8266 / USE_RP2040 / USE_NRF52 → SINGLE +// USE_ESP8266 / USE_RP2 / USE_NRF52 → SINGLE // USE_BK72XX (ARMv5TE, no LDREX/STREX) → MULTI_NO_ATOMICS // everything else (ESP32, host, RTL87XX, LN882X) → MULTI_ATOMICS // Without this the clang-tidy envs end up with USE_ // + MULTI_ATOMICS simultaneously, a combination that can never occur in a // real build. -#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_NRF52) +#if defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_NRF52) #define ESPHOME_THREAD_SINGLE #elif defined(USE_BK72XX) #define ESPHOME_THREAD_MULTI_NO_ATOMICS @@ -44,6 +44,7 @@ #define USE_AREAS #define USE_BINARY_SENSOR #define USE_BINARY_SENSOR_FILTER +#define USE_BLE_DEVICE_IRK #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE @@ -56,6 +57,7 @@ #define USE_DATETIME_TIME #define USE_DEBUG #define USE_DEEP_SLEEP +#define USE_DEEP_SLEEP_ON_WAKE #define USE_DEVICES #define USE_DISPLAY #define USE_ENTITY_DEVICE_CLASS @@ -89,6 +91,7 @@ #define USE_LOGGER_LEVEL_LISTENERS #define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL +#define USE_LVGL_ANIMATION #define USE_LVGL_ANIMIMG #define USE_LVGL_ARC #define USE_LVGL_BINARY_SENSOR @@ -135,6 +138,7 @@ #define USE_MEDIA_PLAYER #define USE_MEDIA_SOURCE #define USE_NETWORK +#define USE_NETWORK_PRIMARY_INTERFACE_WIFI #define USE_NEXTION_COMMAND_SPACING #define USE_NEXTION_CONF_START_UP_PAGE #define USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START @@ -148,11 +152,14 @@ #define USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR #define USE_NEXTION_WAVEFORM #define USE_NUMBER +#define USE_OTA_STATE_LISTENER #define USE_OUTPUT #define USE_OUTPUT_FLOAT_POWER_SCALING #define USE_POWER_SUPPLY #define USE_PREFERENCES_SYNC_EVERY_LOOP +#define USE_PROVISIONING #define USE_QR_CODE +#define USE_SAFE_MODE_BOOT_IS_GOOD_ON_SHUTDOWN #define USE_SAFE_MODE_CALLBACK #define ESPHOME_SAFE_MODE_CALLBACK_COUNT 1 #define USE_SELECT @@ -202,8 +209,11 @@ #define MAX_API_CONNECTIONS 6 #define USE_MD5 #define USE_SHA256 +#ifndef USE_RP2 // no MQTT backend or esp_wireguard library on RP2 #define USE_MQTT #define USE_MQTT_COVER_JSON +#define USE_WIREGUARD +#endif #define USE_RTTTL_FINISHED_PLAYBACK_CALLBACK #define USE_RUNTIME_IMAGE_BMP #define USE_RUNTIME_IMAGE_PNG @@ -211,13 +221,11 @@ #define USE_RUNTIME_STATS #define USE_OTA #define USE_OTA_PASSWORD -#define USE_OTA_STATE_LISTENER #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE #define USE_WIFI #define USE_WIFI_AP #define USE_WIFI_MANUAL_IP -#define USE_WIREGUARD #endif // Arduino-specific feature flags @@ -227,7 +235,7 @@ #endif // Platforms with native 64-bit time sources (no rollover tracking needed) -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2) #define USE_NATIVE_64BIT_TIME #endif @@ -239,13 +247,18 @@ #define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #define USE_OTA_ROLLBACK #define USE_OTA_SIGNED_VERIFICATION +#define USE_OTA_DOWNGRADE_PROTECTION #define USE_ESP32_MIN_CHIP_REVISION_SET +#define USE_ESP32_RTC_PREFERENCES #define USE_ESP32_SRAM1_AS_IRAM +#define USE_ESPNOW +#define USE_ESPNOW_MAX_PAYLOAD_SIZE 1470 #define USE_BLUETOOTH_PROXY #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 #define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16 #define USE_CAPTIVE_PORTAL +#define USE_WIFI_SCAN_RESULTS_LOCK #define USE_ESP32_BLE #define USE_ESP32_BLE_MAX_CONNECTIONS 3 #define USE_ESP32_BLE_CLIENT @@ -262,6 +275,7 @@ #define USE_ESP32_BLE_SERVER_ON_DISCONNECT #define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1 #define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1 +#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 #define ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT 2 #define ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1 @@ -292,14 +306,17 @@ #define USE_VOICE_ASSISTANT #define USE_WEBSERVER #define USE_WEBSERVER_AUTH +#define USE_WEBSERVER_AUTH_DIGEST #define USE_WEBSERVER_OTA #define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WEBSERVER_GZIP #define USE_WEBSERVER_SORTING +#define USE_WEBSERVER_ALLOWED_ORIGINS #define WEB_SERVER_DEFAULT_HEADERS_COUNT 1 #define USE_CAPTIVE_PORTAL_GZIP #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT +#define USE_WIFI_FAST_CONNECT_IN_FLASH #define USE_WIFI_PHY_MODE #define USE_WIFI_IP_STATE_LISTENERS #define USE_WIFI_SCAN_RESULTS_LISTENERS @@ -378,6 +395,7 @@ #define USE_ESP8266_CRASH_HANDLER #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2) #define USE_CAPTIVE_PORTAL +#define USE_WIFI_SCAN_RESULTS_LOCK #define USE_ESP8266_LOGGER_SERIAL #define USE_ESP8266_LOGGER_SERIAL1 #define USE_ESP8266_PREFERENCES_FLASH @@ -400,12 +418,16 @@ #define USE_WEBSERVER #define USE_WEBSERVER_AUTH +#define USE_WEBSERVER_AUTH_DIGEST #define USE_WEBSERVER_PORT 80 // NOLINT #endif -#ifdef USE_RP2040 +// USE_RP2 is the canonical platform define for the RP2 chip family. The +// rp2/__init__.py codegen also defines USE_RP2040 as a back-compat alias +// for external custom components that may still test for it. +#ifdef USE_RP2 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) -#define USE_RP2040_CRASH_HANDLER +#define USE_RP2_CRASH_HANDLER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC @@ -419,14 +441,23 @@ #ifndef USE_ETHERNET_SPI #define USE_ETHERNET_SPI #endif +#define USE_ETHERNET_W5500 +#define USE_WIFI_IP_STATE_LISTENERS +#define ESPHOME_WIFI_IP_STATE_LISTENERS 2 +#define USE_ETHERNET_IP_STATE_LISTENERS +#define ESPHOME_ETHERNET_IP_STATE_LISTENERS 2 #endif #ifdef USE_LIBRETINY +#define USE_BK72XX_BLE +#define USE_LN882H_BLE #define USE_CAPTIVE_PORTAL +#define USE_WIFI_SCAN_RESULTS_LOCK #define USE_SOCKET_IMPL_LWIP_SOCKETS #define USE_LWIP_FAST_SELECT #define USE_WEBSERVER #define USE_WEBSERVER_AUTH +#define USE_WEBSERVER_AUTH_DIGEST #define USE_WEBSERVER_PORT 80 // NOLINT #define USE_ESPHOME_TASK_LOG_BUFFER #define ESPHOME_TASK_LOG_BUFFER_SIZE 768 diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h index ee8e81225a..55c9254327 100644 --- a/esphome/core/event_pool.h +++ b/esphome/core/event_pool.h @@ -1,6 +1,6 @@ #pragma once -#if defined(USE_ESP32) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_ZEPHYR) || defined(USE_LIBRETINY) #include #include @@ -86,4 +86,4 @@ template class EventPool { } // namespace esphome -#endif // defined(USE_ESP32) +#endif // defined(USE_ESP32) || defined(USE_ZEPHYR) || defined(USE_LIBRETINY) diff --git a/esphome/core/hal.h b/esphome/core/hal.h index b44a422836..4c5a19c6d1 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -19,8 +19,8 @@ #include "esphome/components/esp8266/hal.h" #elif defined(USE_LIBRETINY) #include "esphome/components/libretiny/hal.h" -#elif defined(USE_RP2040) -#include "esphome/components/rp2040/hal.h" +#elif defined(USE_RP2) +#include "esphome/components/rp2/hal.h" #elif defined(USE_HOST) #include "esphome/components/host/hal.h" #elif defined(USE_ZEPHYR) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index a7b63643a4..c8cf85d7d6 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -335,6 +335,72 @@ char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_ return format_hex_internal(buffer, buffer_size, data, length, 0, 'a'); } +const char *json_escape_into_buffer(std::span buf, StringRef value, bool short_control_escapes) { + if (buf.empty()) + return ""; + // Reserve one byte for the null terminator. + const size_t limit = buf.size() - 1; + size_t pos = 0; + for (char ch : value) { + auto c = static_cast(ch); + // Every short form is a backslash followed by a single character, so only that character is needed here. Keeping + // it a char rather than a string avoids putting the sequences in read only data, which is RAM on the ESP8266. + char escape = '\0'; + switch (c) { + case '"': + escape = '"'; + break; + case '\\': + escape = '\\'; + break; + case '\n': + escape = 'n'; + break; + case '\r': + escape = 'r'; + break; + case '\t': + escape = 't'; + break; + case '\b': + escape = 'b'; + break; + case '\f': + escape = 'f'; + break; + default: + break; + } + // " and \ are always written as two characters, but the control characters fall through to \u00XX when the + // caller did not ask for the short forms. + if (!short_control_escapes && c < 0x20) + escape = '\0'; + if (escape != '\0') { + if (pos + 2 > limit) + break; + buf[pos++] = '\\'; + buf[pos++] = escape; + } else if (c < 0x20) { + // Remaining control characters have no short form and must be written as \u00XX. The value is below 0x20, so + // the two high hex digits are always zero. + if (pos + JSON_ESCAPE_MAX_EXPANSION > limit) + break; + buf[pos++] = '\\'; + buf[pos++] = 'u'; + buf[pos++] = '0'; + buf[pos++] = '0'; + buf[pos++] = format_hex_char(static_cast(c >> 4)); + buf[pos++] = format_hex_char(static_cast(c & 0x0F)); + } else { + if (pos + 1 > limit) + break; + buf[pos++] = static_cast(c); + } + } + buf[pos] = '\0'; + return buf.data(); +} + // format_hex (std::string returning overloads) moved to alloc_helpers.cpp char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 07bcb7a74f..7940df8780 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -33,7 +33,7 @@ #include #endif -#ifdef USE_RP2040 +#ifdef USE_RP2 #include #endif @@ -184,8 +184,10 @@ template class SmallInlineBuffer { SmallInlineBuffer(const SmallInlineBuffer &) = delete; SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete; - /// Set buffer contents, allocating heap if needed - void set(const uint8_t *src, size_t size) { + /// Resize to `size` bytes of (uninitialized) storage and return a writable pointer to fill. + /// Allocates heap only when `size` exceeds the inline capacity. Use this when the contents are + /// built in place (e.g. assembling a frame and appending a checksum) to avoid a staging copy. + uint8_t *init(size_t size) { // Free existing heap allocation if switching from heap to inline or different heap size if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) { delete[] this->heap_; @@ -196,9 +198,12 @@ template class SmallInlineBuffer { this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory) } this->len_ = size; - memcpy(this->data(), src, size); + return this->data(); } + /// Set buffer contents, allocating heap if needed + void set(const uint8_t *src, size_t size) { memcpy(this->init(size), src, size); } + uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; } const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; } size_t size() const { return this->len_; } @@ -683,6 +688,15 @@ template class FixedVector { T &back() { return data_[size_ - 1]; } const T &back() const { return data_[size_ - 1]; } + /// Remove the last element in place (no reallocation, keeps capacity) + /// Caller must ensure vector is not empty (size() > 0) + void pop_back() { + if constexpr (!std::is_trivially_destructible::value) { + data_[size_ - 1].~T(); + } + size_--; + } + size_t size() const { return size_; } bool empty() const { return size_ == 0; } size_t capacity() const { return capacity_; } @@ -1254,6 +1268,22 @@ ESPHOME_ALWAYS_INLINE inline char format_hex_char(uint8_t v) { return format_hex /// Convert a nibble (0-15) to uppercase hex char (used for pretty printing) ESPHOME_ALWAYS_INLINE inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } +/// Largest number of output bytes a single input byte can expand to when JSON escaped (a \u00XX sequence). +static constexpr size_t JSON_ESCAPE_MAX_EXPANSION = 6; + +/// Copy value into buf, escaping the characters that cannot appear raw inside a JSON string literal. +/// +/// Escapes " and \ along with the control characters below 0x20. Bytes >= 0x20 are copied verbatim, so text +/// containing valid UTF-8 survives intact. The result is always null terminated; anything that would not fit is +/// dropped rather than written partially. Returns buf so the call can be used directly as an argument. +/// +/// With short_control_escapes the five control characters JSON gives a short form get it (\n \r \t \b \f) and the +/// rest become \u00XX. Pass false to write every control character as \u00XX, which some consumers expect. +/// +/// To size buf so that no input is ever dropped, allow JSON_ESCAPE_MAX_EXPANSION bytes per input byte plus one for +/// the null terminator. +const char *json_escape_into_buffer(std::span buf, StringRef value, bool short_control_escapes = true); + /// Write int8 value to buffer without modulo operations. /// Buffer must have at least 4 bytes free. Returns pointer past last char written. inline char *int8_to_str(char *buf, int8_t val) { @@ -1611,6 +1641,12 @@ constexpr float celsius_to_fahrenheit(float value) { return value * 1.8f + 32.0f /// Convert degrees Fahrenheit to degrees Celsius. constexpr float fahrenheit_to_celsius(float value) { return (value - 32.0f) / 1.8f; } +enum class TemperatureUnit : uint8_t { + CELSIUS = 0, + FAHRENHEIT = 1, + KELVIN = 2, +}; + ///@} /// @name Utilities @@ -1886,7 +1922,7 @@ class Mutex { Mutex(const Mutex &) = delete; Mutex &operator=(const Mutex &) = delete; -#if defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_RP2) // Single-threaded platforms: inline no-ops so the compiler eliminates all call overhead. Mutex() = default; ~Mutex() = default; @@ -1955,7 +1991,7 @@ class InterruptLock { ~InterruptLock(); protected: -#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) +#if defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_ZEPHYR) uint32_t state_; #endif }; @@ -1973,7 +2009,7 @@ class LwIPLock { LwIPLock(const LwIPLock &) = delete; LwIPLock &operator=(const LwIPLock &) = delete; -#if defined(USE_ESP32) || defined(USE_RP2040) +#if defined(USE_ESP32) || defined(USE_RP2) // Platforms with potential lwIP core locking — out-of-line implementations in helpers.cpp LwIPLock(); ~LwIPLock(); @@ -2123,7 +2159,7 @@ template class RAMAllocator { auto max_external = this->flags_ & ALLOC_EXTERNAL ? heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM) : 0; return max_internal + max_external; -#elif defined(USE_RP2040) +#elif defined(USE_RP2) return ::rp2040.getFreeHeap(); #elif defined(USE_LIBRETINY) return lt_heap_get_free(); diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 316186ea54..ce54231137 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -1,5 +1,7 @@ #pragma once +#include "esphome/core/defines.h" + #include #include @@ -26,6 +28,54 @@ namespace esphome { +namespace lockfree_internal { +#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS +// Platforms whose cores lack atomic read-modify-write instructions (currently +// the ARMv5TE BK72xx SoCs — no LDREX/STREX, no libatomic; other LibreTiny +// chips such as LN882x/RTL87xx are ARMv7-M and keep std::atomic). For this +// queue's SPSC contract RMW atomics are not needed: aligned 8/16-bit loads and +// stores are single instructions on these cores, so torn reads cannot occur, +// and on a single in-order core a compiler barrier supplies all the +// acquire/release ordering the algorithm requires. Each index has exactly one +// writer (head_: consumer, tail_: producer). The dropped counter's +// increment/exchange pair is not atomic here — a concurrent reset can lose +// counts — which is acceptable for a diagnostic drop counter. +#define ESPHOME_LFQ_COMPILER_BARRIER() __asm__ __volatile__("" ::: "memory") +template class PlainAtomic { + public: + PlainAtomic() = default; + constexpr PlainAtomic(T value) : value_(value) {} + T load(std::memory_order order = std::memory_order_seq_cst) const { + T value = value_; + if (order != std::memory_order_relaxed) + ESPHOME_LFQ_COMPILER_BARRIER(); // acquire: later reads may not hoist above this load + return value; + } + void store(T value, std::memory_order order = std::memory_order_seq_cst) { + if (order != std::memory_order_relaxed) + ESPHOME_LFQ_COMPILER_BARRIER(); // release: earlier writes may not sink below this store + value_ = value; + } + T fetch_add(T amount, std::memory_order /*order*/ = std::memory_order_seq_cst) { + T value = value_; + value_ = value + amount; + return value; + } + T exchange(T desired, std::memory_order /*order*/ = std::memory_order_seq_cst) { + T value = value_; + value_ = desired; + return value; + } + + private: + volatile T value_{0}; +}; +template using AtomicIndex = PlainAtomic; +#else +template using AtomicIndex = std::atomic; +#endif +} // namespace lockfree_internal + // Base lock-free queue without task notification template class LockFreeQueue { public: @@ -126,13 +176,13 @@ template class LockFreeQueue { protected: T *buffer_[SIZE]{}; // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) - std::atomic dropped_count_; // 65535 max - more than enough for drop tracking + lockfree_internal::AtomicIndex dropped_count_; // 65535 max - more than enough for drop tracking // Atomic: written by consumer (pop), read by producer (push) to check if full // Using uint8_t limits queue size to 255 elements but saves memory and ensures // atomic operations are efficient on all platforms - std::atomic head_; + lockfree_internal::AtomicIndex head_; // Atomic: written by producer (push), read by consumer (pop) to check if empty - std::atomic tail_; + lockfree_internal::AtomicIndex tail_; }; #ifdef USE_ESP32 diff --git a/esphome/core/preference_backend.h b/esphome/core/preference_backend.h index 431de205af..34bf84409d 100644 --- a/esphome/core/preference_backend.h +++ b/esphome/core/preference_backend.h @@ -12,8 +12,8 @@ #include "esphome/components/esp32/preference_backend.h" #elif defined(USE_ESP8266) #include "esphome/components/esp8266/preference_backend.h" -#elif defined(USE_RP2040) -#include "esphome/components/rp2040/preference_backend.h" +#elif defined(USE_RP2) +#include "esphome/components/rp2/preference_backend.h" #elif defined(USE_LIBRETINY) #include "esphome/components/libretiny/preference_backend.h" #elif defined(USE_HOST) @@ -24,7 +24,7 @@ namespace esphome { -#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \ +#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2) && !defined(USE_LIBRETINY) && \ !defined(USE_HOST) && !(defined(USE_ZEPHYR) && defined(CONFIG_SETTINGS)) // Stub for static analysis when no platform is defined. struct PreferenceBackend { diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h index 64a0a927e6..1efce5af51 100644 --- a/esphome/core/preferences.h +++ b/esphome/core/preferences.h @@ -9,8 +9,8 @@ #include "esphome/components/esp32/preferences.h" #elif defined(USE_ESP8266) #include "esphome/components/esp8266/preferences.h" -#elif defined(USE_RP2040) -#include "esphome/components/rp2040/preferences.h" +#elif defined(USE_RP2) +#include "esphome/components/rp2/preferences.h" #elif defined(USE_LIBRETINY) #include "esphome/components/libretiny/preferences.h" #elif defined(USE_HOST) diff --git a/esphome/core/preferences_rtc.h b/esphome/core/preferences_rtc.h new file mode 100644 index 0000000000..30b004f994 --- /dev/null +++ b/esphome/core/preferences_rtc.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +namespace esphome { + +// Shared storage format for word-addressable preference backends. +// +// Several platforms persist preferences as a buffer of 32-bit words followed by a +// single checksum word, seeded with the preference's `type` (its hashed key). This +// format is used for RTC user memory (ESP8266, ESP32) and for the ESP8266 +// flash-emulation buffer. The helpers here are platform independent; each backend +// supplies its own word read/write primitives and offset allocation. + +/// Round a byte count up to whole 32-bit words. +inline size_t rtc_pref_bytes_to_words(size_t bytes) { return (bytes + 3) / 4; } + +/// Compute the integrity checksum over [first, last), seeded with `type`. +/// Iterates over 32-bit words; the result is stored as the trailing word of a record. +/// (Not a true CRC -- it XORs each word after a Fibonacci-hash multiply -- but the +/// algorithm is kept as-is for compatibility with records written by old firmware.) +template uint32_t rtc_pref_calculate_checksum(It first, It last, uint32_t type) { + uint32_t checksum = type; + while (first != last) { + // UINT32_C keeps the multiply wrapping at 32 bits regardless of the width of + // unsigned long, so 64-bit host builds compute the same value as the devices. + checksum ^= (*first++ * UINT32_C(2654435769)) >> 1; + } + return checksum; +} + +/// Encode `len` data bytes into `buffer` (length_words data words + 1 trailing checksum word). +/// `buffer` must have capacity for at least `length_words + 1` words. Trailing padding in +/// the final data word is zeroed so the checksum is deterministic. +inline void rtc_pref_encode(uint32_t *buffer, uint32_t type, uint8_t length_words, const uint8_t *data, size_t len) { + memset(buffer, 0, (static_cast(length_words) + 1) * sizeof(uint32_t)); + memcpy(buffer, data, len); + buffer[length_words] = rtc_pref_calculate_checksum(buffer, buffer + length_words, type); +} + +/// Verify the checksum of a record held in `buffer` (length_words data words + 1 checksum +/// word) and, on success, copy `len` bytes out to `data`. Returns false on checksum mismatch +/// (e.g. the record was never written or RTC memory holds power-on garbage). +inline bool rtc_pref_decode(const uint32_t *buffer, uint32_t type, uint8_t length_words, uint8_t *data, size_t len) { + if (buffer[length_words] != rtc_pref_calculate_checksum(buffer, buffer + length_words, type)) { + return false; + } + memcpy(data, buffer, len); + return true; +} + +} // namespace esphome diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 8449cba5e8..e9c5bf2c04 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -110,35 +110,16 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { return static_cast((static_cast(random_uint32()) * max_offset) >> 32); } -// Check if a retry was already cancelled in items_ or to_add_ -// Extracted from set_timer_common_ to reduce code size - retry path is cold and deprecated -// Remove before 2026.8.0 along with all retry code -bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, - uint32_t hash_or_id) { - for (auto *container : {&this->items_, &this->to_add_}) { - for (auto *item : *container) { - if (item != nullptr && this->is_item_removed_locked_(item) && - this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, - /* match_retry= */ true, /* skip_removed= */ false)) { - return true; - } - } - } - return false; -} - // Common implementation for both timeout and interval // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name, uint32_t hash_or_id, uint32_t delay, - std::function &&func, bool is_retry, bool skip_cancel, - const LogString *source) { + std::function &&func, bool skip_cancel, const LogString *source) { if (delay == SCHEDULER_DONT_RUN) { // Still need to cancel existing timer if we have a name/id if (!skip_cancel) { LockGuard guard{this->lock_}; - this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false, - /* find_first= */ true); + this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* find_first= */ true); } return; } @@ -156,23 +137,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type delay = 1; } - // Take lock early to protect scheduler_item_pool_head_ access and retry-cancelled check + // Take lock early to protect scheduler_item_pool_head_ access LockGuard guard{this->lock_}; - // For retries, check if there's a cancelled timeout first - before allocating an item. - // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name - // Skip check for defer (delay=0) - deferred retries bypass the cancellation check - if (is_retry && delay != 0 && (name_type != NameType::STATIC_STRING || static_name != nullptr) && - type == SchedulerItem::TIMEOUT && - this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) { -#ifdef ESPHOME_DEBUG_SCHEDULER - SchedulerNameLog skip_name_log; - ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", - skip_name_log.format(name_type, static_name, hash_or_id)); -#endif - return; - } - // Create and populate the scheduler item SchedulerItem *item = this->get_item_from_pool_locked_(); // SELF_POINTER items store the source name (owning script) in the union slot instead of a component. @@ -192,7 +159,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type new (&item->callback) std::function(std::move(func)); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use this->set_item_removed_(item, false); - item->is_retry = is_retry; // Determine target container: defer_queue_ for deferred items, to_add_ for everything else. // Using a pointer lets both paths share the cancel + push_back epilogue. @@ -234,8 +200,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Common epilogue: atomic cancel-and-add (unless skip_cancel is true or anonymous) // Anonymous items (STATIC_STRING with nullptr) can never match anything, so skip the scan. if (!skip_cancel && (name_type != NameType::STATIC_STRING || static_name != nullptr)) { - this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false, - /* find_first= */ true); + this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* find_first= */ true); } target->push_back(item); if (target == &this->to_add_) { @@ -301,125 +266,6 @@ bool HOT Scheduler::cancel_interval(const void *self) { SchedulerItem::INTERVAL); } -// Suppress deprecation warnings for RetryResult usage in the still-present (but deprecated) retry implementation. -// Remove before 2026.8.0 along with all retry code. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - -struct RetryArgs { - // Ordered to minimize padding on 32-bit systems - std::function func; - Component *component; - Scheduler *scheduler; - // Union for name storage - only one is used based on name_type - union { - const char *static_name; // For STATIC_STRING - uint32_t hash_or_id; // For HASHED_STRING or NUMERIC_ID - } name_; - uint32_t current_interval; - float backoff_increase_factor; - Scheduler::NameType name_type; // Discriminator for name_ union - uint8_t retry_countdown; -}; - -void retry_handler(const std::shared_ptr &args) { - RetryResult const retry_result = args->func(--args->retry_countdown); - if (retry_result == RetryResult::DONE || args->retry_countdown <= 0) - return; - // second execution of `func` happens after `initial_wait_time` - // args->name_ is owned by the shared_ptr - // which is captured in the lambda and outlives the SchedulerItem - const char *static_name = (args->name_type == Scheduler::NameType::STATIC_STRING) ? args->name_.static_name : nullptr; - uint32_t hash_or_id = (args->name_type != Scheduler::NameType::STATIC_STRING) ? args->name_.hash_or_id : 0; - args->scheduler->set_timer_common_( - args->component, Scheduler::SchedulerItem::TIMEOUT, args->name_type, static_name, hash_or_id, - args->current_interval, [args]() { retry_handler(args); }, - /* is_retry= */ true); - // backoff_increase_factor applied to third & later executions - args->current_interval *= args->backoff_increase_factor; -} - -void HOT Scheduler::set_retry_common_(Component *component, NameType name_type, const char *static_name, - uint32_t hash_or_id, uint32_t initial_wait_time, uint8_t max_attempts, - std::function func, float backoff_increase_factor) { - this->cancel_retry_(component, name_type, static_name, hash_or_id); - - if (initial_wait_time == SCHEDULER_DONT_RUN) - return; - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - { - SchedulerNameLog name_log; - ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%0.1f)", - name_log.format(name_type, static_name, hash_or_id), initial_wait_time, max_attempts, - backoff_increase_factor); - } -#endif - - if (backoff_increase_factor < 0.0001f) { - ESP_LOGE(TAG, "set_retry: backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor, - (name_type == NameType::STATIC_STRING && static_name) ? static_name : ""); - backoff_increase_factor = 1; - } - - auto args = std::make_shared(); - args->func = std::move(func); - args->component = component; - args->scheduler = this; - args->name_type = name_type; - if (name_type == NameType::STATIC_STRING) { - args->name_.static_name = static_name; - } else { - args->name_.hash_or_id = hash_or_id; - } - args->current_interval = initial_wait_time; - args->backoff_increase_factor = backoff_increase_factor; - args->retry_countdown = max_attempts; - - // First execution of `func` immediately - use set_timer_common_ with is_retry=true - this->set_timer_common_( - component, SchedulerItem::TIMEOUT, name_type, static_name, hash_or_id, 0, [args]() { retry_handler(args); }, - /* is_retry= */ true); -} - -void HOT Scheduler::set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function func, float backoff_increase_factor) { - this->set_retry_common_(component, NameType::STATIC_STRING, name, 0, initial_wait_time, max_attempts, std::move(func), - backoff_increase_factor); -} - -bool HOT Scheduler::cancel_retry_(Component *component, NameType name_type, const char *static_name, - uint32_t hash_or_id) { - return this->cancel_item_(component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, - /* match_retry= */ true); -} -bool HOT Scheduler::cancel_retry(Component *component, const char *name) { - return this->cancel_retry_(component, NameType::STATIC_STRING, name, 0); -} - -void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, - uint8_t max_attempts, std::function func, - float backoff_increase_factor) { - this->set_retry_common_(component, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), initial_wait_time, - max_attempts, std::move(func), backoff_increase_factor); -} - -bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) { - return this->cancel_retry_(component, NameType::HASHED_STRING, nullptr, fnv1a_hash(name)); -} - -void HOT Scheduler::set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, - std::function func, float backoff_increase_factor) { - this->set_retry_common_(component, NameType::NUMERIC_ID, nullptr, id, initial_wait_time, max_attempts, - std::move(func), backoff_increase_factor); -} - -bool HOT Scheduler::cancel_retry(Component *component, uint32_t id) { - return this->cancel_retry_(component, NameType::NUMERIC_ID, nullptr, id); -} - -#pragma GCC diagnostic pop // End suppression of deprecated RetryResult warnings - optional HOT Scheduler::next_schedule_in(uint32_t now) { // IMPORTANT: This method should only be called from the main thread (loop task). // Accesses items_[0] and the fast-path empty checks without holding a lock, which @@ -806,11 +652,11 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { // Common implementation for cancel operations - handles locking bool HOT Scheduler::cancel_item_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry) { + SchedulerItem::Type type) { LockGuard guard{this->lock_}; // Public cancel path uses default find_first=false to cancel ALL matches because // DelayAction parallel mode (skip_cancel=true) can create multiple items with the same key. - return this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, match_retry); + return this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } // Helper to cancel matching items - must be called with lock held. @@ -822,11 +668,10 @@ bool HOT Scheduler::cancel_item_(Component *component, NameType name_type, const size_t Scheduler::mark_matching_items_removed_slow_locked_(std::vector &container, Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry, - bool find_first) { + SchedulerItem::Type type, bool find_first) { size_t count = 0; for (auto *item : container) { - if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { + if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type)) { this->set_item_removed_(item, true); if (find_first) return 1; @@ -837,8 +682,7 @@ size_t Scheduler::mark_matching_items_removed_slow_locked_(std::vectormark_matching_items_removed_locked_(this->defer_queue_, component, name_type, static_name, - hash_or_id, type, match_retry, find_first); + hash_or_id, type, find_first); if (find_first && total_cancelled > 0) return true; } @@ -863,7 +707,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type // Only the main loop in call() should recycle items after execution completes. { size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name, - hash_or_id, type, match_retry, find_first); + hash_or_id, type, find_first); total_cancelled += heap_cancelled; this->to_remove_add_locked_(heap_cancelled); if (find_first && total_cancelled > 0) @@ -872,7 +716,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type // Cancel items in to_add_ total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_type, static_name, - hash_or_id, type, match_retry, find_first); + hash_or_id, type, find_first); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index c7743e5b2a..8ef3499a11 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -16,14 +16,8 @@ namespace esphome { class Component; -struct RetryArgs; - -// Forward declaration of retry_handler - needs to be non-static for friend declaration -void retry_handler(const std::shared_ptr &args); class Scheduler { - // Allow retry_handler to access protected members for internal retry mechanism - friend void ::esphome::retry_handler(const std::shared_ptr &args); // Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays. // This is needed to fix issue #10264 where parallel scripts with delays interfere with each other. // We use friend instead of a public API because skip_cancel is dangerous - it can cause delays @@ -79,32 +73,6 @@ class Scheduler { SchedulerItem::INTERVAL); } - // Remove before 2026.8.0 - ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", - "2026.2.0") - void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function func, float backoff_increase_factor = 1.0f); - // Remove before 2026.8.0 - ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", - "2026.2.0") - void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function func, float backoff_increase_factor = 1.0f); - // Remove before 2026.8.0 - ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.", - "2026.2.0") - void set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, - std::function func, float backoff_increase_factor = 1.0f); - - // Remove before 2026.8.0 - ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") - bool cancel_retry(Component *component, const std::string &name); - // Remove before 2026.8.0 - ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") - bool cancel_retry(Component *component, const char *name); - // Remove before 2026.8.0 - ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") - bool cancel_retry(Component *component, uint32_t id); - /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) uint64_t millis_64() { return esphome::millis_64(); } @@ -202,19 +170,17 @@ class Scheduler { // std::atomic inlines correctly on all platforms. std::atomic remove{0}; - // Bit-packed fields (5 bits used, 3 bits padding in 1 byte) + // Bit-packed fields (4 bits used, 4 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; NameType name_type_ : 3; // Discriminator for name_ union (0–4, see NameType enum) - bool is_retry : 1; // True if this is a retry timeout - // 3 bits padding + // 4 bits padding #else // Single-threaded or multi-threaded without atomics: can pack all fields together - // Bit-packed fields (6 bits used, 2 bits padding in 1 byte) + // Bit-packed fields (5 bits used, 3 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; bool remove : 1; NameType name_type_ : 3; // Discriminator for name_ union (0–4, see NameType enum) - bool is_retry : 1; // True if this is a retry timeout - // 2 bits padding + // 3 bits padding #endif // Constructor @@ -226,13 +192,11 @@ class Scheduler { #ifdef ESPHOME_THREAD_MULTI_ATOMICS // remove is initialized in the member declaration type(TIMEOUT), - name_type_(NameType::STATIC_STRING), - is_retry(false) { + name_type_(NameType::STATIC_STRING) { #else type(TIMEOUT), remove(false), - name_type_(NameType::STATIC_STRING), - is_retry(false) { + name_type_(NameType::STATIC_STRING) { #endif name_.static_name = nullptr; } @@ -306,19 +270,8 @@ class Scheduler { // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id // `source` is stored (in the union slot) only for SELF_POINTER items; ignored otherwise. void set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name, - uint32_t hash_or_id, uint32_t delay, std::function &&func, bool is_retry = false, - bool skip_cancel = false, const LogString *source = nullptr); - - // Common implementation for retry - Remove before 2026.8.0 - // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - void set_retry_common_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, - uint32_t initial_wait_time, uint8_t max_attempts, std::function func, - float backoff_increase_factor); -#pragma GCC diagnostic pop - // Common implementation for cancel_retry - bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); + uint32_t hash_or_id, uint32_t delay, std::function &&func, bool skip_cancel = false, + const LogString *source = nullptr); // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. // On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040 — see @@ -374,11 +327,11 @@ class Scheduler { // mode where skip_cancel=true allows multiple items with the same key). // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id bool cancel_item_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry = false, bool find_first = false); + SchedulerItem::Type type, bool find_first = false); // Common implementation for cancel operations - handles locking bool cancel_item_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry = false); + SchedulerItem::Type type); // Helper to check if two static string names match inline bool HOT names_match_static_(const char *name1, const char *name2) const { @@ -394,7 +347,7 @@ class Scheduler { // IMPORTANT: Must be called with scheduler lock held inline bool HOT matches_item_locked_(SchedulerItem *item, Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, - bool match_retry, bool skip_removed = true) const { + bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded // platforms, items can be nulled in defer_queue_ during processing. // Fixes: https://github.com/esphome/esphome/issues/11940 @@ -403,7 +356,7 @@ class Scheduler { // get_component() is nullptr for SELF_POINTER items (their cancels pass nullptr too), so they // match by the `this` key alone. if (item->get_component() != component || item->type != type || - (skip_removed && this->is_item_removed_locked_(item)) || (match_retry && !item->is_retry)) { + (skip_removed && this->is_item_removed_locked_(item))) { return false; } // Name type must match @@ -448,13 +401,6 @@ class Scheduler { // IMPORTANT: Must not be inlined - called only for intervals, keeping it out of the hot path saves flash. uint32_t __attribute__((noinline)) calculate_interval_offset_(uint32_t delay); - // Helper to check if a retry was already cancelled - extracted to reduce code size of set_timer_common_ - // Remove before 2026.8.0 along with all retry code. - // IMPORTANT: Must not be inlined - retry path is cold and deprecated. - // IMPORTANT: Caller must hold the scheduler lock before calling this function. - bool __attribute__((noinline)) - is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); - #ifdef ESPHOME_DEBUG_SCHEDULER // Helper for debug logging in set_timer_common_ - extracted to reduce code size void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id, @@ -556,19 +502,21 @@ class Scheduler { // Inlined: the fast path (empty container) avoids calling the out-of-line scan. inline size_t HOT mark_matching_items_removed_locked_(std::vector &container, Component *component, NameType name_type, const char *static_name, - uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, + uint32_t hash_or_id, SchedulerItem::Type type, bool find_first = false) { if (container.empty()) return 0; return this->mark_matching_items_removed_slow_locked_(container, component, name_type, static_name, hash_or_id, - type, match_retry, find_first); + type, find_first); } // Out-of-line slow path for mark_matching_items_removed_locked_ when container is non-empty. // IMPORTANT: Must be called with scheduler lock held - __attribute__((noinline)) size_t mark_matching_items_removed_slow_locked_( - std::vector &container, Component *component, NameType name_type, const char *static_name, - uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool find_first); + __attribute__((noinline)) size_t mark_matching_items_removed_slow_locked_(std::vector &container, + Component *component, NameType name_type, + const char *static_name, + uint32_t hash_or_id, + SchedulerItem::Type type, bool find_first); Mutex lock_; std::vector items_; diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 5a5d27ceff..a48e52fb73 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -18,7 +18,7 @@ namespace esphome { // === Wake flag for ESP8266/RP2040 === -#if defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_RP2) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern volatile bool g_main_loop_woke; #endif @@ -65,8 +65,8 @@ __attribute__((always_inline)) inline bool wake_request_take() { #include "esphome/core/wake/wake_freertos.h" #elif defined(USE_ESP8266) #include "esphome/core/wake/wake_esp8266.h" -#elif defined(USE_RP2040) -#include "esphome/core/wake/wake_rp2040.h" +#elif defined(USE_RP2) +#include "esphome/core/wake/wake_rp2.h" #elif defined(USE_HOST) #include "esphome/core/wake/wake_host.h" #elif defined(USE_ZEPHYR) diff --git a/esphome/core/wake/wake_rp2040.cpp b/esphome/core/wake/wake_rp2.cpp similarity index 88% rename from esphome/core/wake/wake_rp2040.cpp rename to esphome/core/wake/wake_rp2.cpp index bdcbb1ad00..ac1deba726 100644 --- a/esphome/core/wake/wake_rp2040.cpp +++ b/esphome/core/wake/wake_rp2.cpp @@ -1,6 +1,6 @@ #include "esphome/core/defines.h" -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/hal.h" #include "esphome/core/wake.h" @@ -20,7 +20,7 @@ volatile bool g_main_loop_woke = false; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static volatile bool s_delay_expired = false; -static int64_t alarm_callback_(alarm_id_t id, void *user_data) { +static int64_t alarm_callback(alarm_id_t id, void *user_data) { (void) id; (void) user_data; s_delay_expired = true; @@ -43,7 +43,7 @@ void wakeable_delay(uint32_t ms) { return; } s_delay_expired = false; - alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true); + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback, nullptr, true); if (alarm <= 0) { delay(ms); return; @@ -59,4 +59,4 @@ void wakeable_delay(uint32_t ms) { } // namespace esphome -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/core/wake/wake_rp2040.h b/esphome/core/wake/wake_rp2.h similarity index 88% rename from esphome/core/wake/wake_rp2040.h rename to esphome/core/wake/wake_rp2.h index ea1242f535..715e5aca0c 100644 --- a/esphome/core/wake/wake_rp2040.h +++ b/esphome/core/wake/wake_rp2.h @@ -2,7 +2,7 @@ #include "esphome/core/defines.h" -#ifdef USE_RP2040 +#ifdef USE_RP2 #include "esphome/core/hal.h" @@ -21,11 +21,11 @@ inline void wake_loop_any_context() { inline void wake_loop_threadsafe() { wake_loop_any_context(); } -/// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake_rp2040.cpp. +/// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake_rp2.cpp. namespace internal { void wakeable_delay(uint32_t ms); } // namespace internal } // namespace esphome -#endif // USE_RP2040 +#endif // USE_RP2 diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 582b8fc74d..6bcf4eed77 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -699,6 +699,15 @@ def add_build_flag(build_flag: str): CORE.add_build_flag(build_flag) +def add_cxx_build_flag(build_flag: str) -> None: + """Add a global build flag that applies to C++ compiles only. + + Use for flags GCC rejects or warns about when passed on C compiles + (e.g. ``-Wno-volatile``). + """ + CORE.add_cxx_build_flag(build_flag) + + def add_build_unflag(build_unflag: str) -> None: """Add a global build unflag to the compiler flags.""" CORE.add_build_unflag(build_unflag) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index b035e28a7a..b2338e5bc1 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -151,6 +151,17 @@ async def gpio_pin_expression(conf): return await coroutine(pins.PIN_SCHEMA_REGISTRY[CORE.target_platform][0])(conf) +def set_setup_priority(var, priority: float) -> None: + """Emit a setup-priority override for the given component. + + Pairs the ``set_setup_priority()`` call with the ``USE_SETUP_PRIORITY_OVERRIDE`` + define that compiles in the core override support, so callers cannot emit one + without the other. + """ + add_define("USE_SETUP_PRIORITY_OVERRIDE") + add(var.set_setup_priority(priority)) + + async def register_component(var, config): """Register the given obj as a component. @@ -168,8 +179,7 @@ async def register_component(var, config): ) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: - add_define("USE_SETUP_PRIORITY_OVERRIDE") - add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) + set_setup_priority(var, config[CONF_SETUP_PRIORITY]) if CONF_UPDATE_INTERVAL in config: add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) diff --git a/esphome/espidf/component.py b/esphome/espidf/component.py index e9ec170a5e..cad5bbf665 100644 --- a/esphome/espidf/component.py +++ b/esphome/espidf/component.py @@ -53,9 +53,10 @@ def _apply_extra_script(component: IDFComponent) -> None: if not script_path.is_relative_to(library_root) or not script_path.is_file(): return from esphome.components.esp32 import get_esp32_variant + from esphome.components.esp32.const import variant_to_idf_target from esphome.espidf.extra_script import captured_as_build_flags, run_extra_script - idf_target = get_esp32_variant().lower().replace("-", "") + idf_target = variant_to_idf_target(get_esp32_variant()) result = run_extra_script( script_path, library_dir=component.path, idf_target=idf_target ) @@ -83,11 +84,23 @@ def generate_cmakelists_txt(component: IDFComponent) -> str: Returns: str: The complete CMakeLists.txt content as a string """ + # Late import: this module loads with the esp32 platform on every + # validate/compile, but shlex is only needed when generating component + # CMakeLists. + import shlex def escape_entry(p: PathType) -> str: # In CMakeLists.txt, backslashes need to be escaped return f'"{str(p)}"'.replace("\\", "\\\\") + def escape_path(p: PathType) -> str: + # CMake uses forward slashes for paths on every platform and treats + # backslashes as escape characters. On Windows os.path.relpath yields + # backslash paths, which break CMake's list re-parsing (e.g. "\b" in + # "src\backend" is an invalid character escape). Emit forward slashes, + # which Windows accepts too, so the generated CMakeLists is portable. + return f'"{str(p).replace(os.sep, "/")}"' + # Extract the values build_src_dir = component.data.get("build", {}).get("srcDir", None) if not build_src_dir: @@ -105,6 +118,23 @@ def generate_cmakelists_txt(component: IDFComponent) -> str: build_flags = ensure_list( component.data.get("build", {}).get("flags", DEFAULT_BUILD_FLAGS) ) + # PlatformIO shell-lexes each build.flags entry, so one entry can carry a + # flag and its argument (e.g. "-include cp_custom_alloc.h"). Split the + # same way; emitting such an entry as a single quoted compile option + # hands the compiler one argv with an embedded space. + build_flags = [token for entry in build_flags for token in shlex.split(entry)] + # Re-glue bare -I/-L/-l tokens to their argument ("-I foo" -> "-Ifoo") so + # the prefix classifiers below still route them to INCLUDE_DIRS and the + # link handling. + tokens, build_flags = build_flags, [] + i = 0 + while i < len(tokens): + if tokens[i] in ("-I", "-L", "-l") and i + 1 < len(tokens): + build_flags.append(tokens[i] + tokens[i + 1]) + i += 2 + else: + build_flags.append(tokens[i]) + i += 1 # List all sources files build_src_files = collect_filtered_files( @@ -152,10 +182,10 @@ def generate_cmakelists_txt(component: IDFComponent) -> str: # Generate the component content = "idf_component_register(\n" if build_src_files: - str_srcs = " ".join([escape_entry(p) for p in sorted(build_src_files)]) + str_srcs = " ".join([escape_path(p) for p in sorted(build_src_files)]) content += f" SRCS {str_srcs}\n" if build_include_dirs: - str_include_dirs = " ".join([escape_entry(p) for p in build_include_dirs]) + str_include_dirs = " ".join([escape_path(p) for p in build_include_dirs]) content += f" INCLUDE_DIRS {str_include_dirs}\n" # Project-managed and built-in component lists are set per-project # via idf_build_set_property in the top-level CMakeLists; expanded @@ -190,7 +220,7 @@ def generate_cmakelists_txt(component: IDFComponent) -> str: if link_directories: content += "target_link_directories(${COMPONENT_LIB} INTERFACE\n" for link_directory in link_directories: - str_build_flag = escape_entry(link_directory) + str_build_flag = escape_path(link_directory) content += f" {str_build_flag}\n" content += ")\n" diff --git a/esphome/espidf/extra_script.py b/esphome/espidf/extra_script.py index 4d06fb842a..487fef7cc1 100644 --- a/esphome/espidf/extra_script.py +++ b/esphome/espidf/extra_script.py @@ -107,7 +107,7 @@ def run_extra_script( script shouldn't block the build. """ env = _FakeSConsEnv(board_mcu=idf_target, pio_env=f"esphome_{idf_target}") - code = compile(script_path.read_text(), str(script_path), "exec") + code = compile(script_path.read_text(encoding="utf-8"), str(script_path), "exec") old_cwd = Path.cwd() try: os.chdir(library_dir) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 810a63476f..0ca7a9d14b 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -1,5 +1,7 @@ """ESP-IDF framework tools for ESPHome.""" +from collections.abc import Callable +from ctypes.util import find_library import json import logging import os @@ -7,7 +9,7 @@ from pathlib import Path import platform import re import shutil -import tempfile +from typing import Any, NoReturn import platformdirs @@ -18,6 +20,7 @@ from esphome.framework_helpers import ( archive_extract_all, create_venv, download_from_mirrors, + download_with_resume, get_python_env_executable_path, get_system_python_path, rmdir, @@ -46,6 +49,10 @@ STAMP_SCHEMA_VERSION = "0" ESPHOME_IDF_DEFAULT_TARGETS = str_to_lst_of_str( os.environ.get("ESPHOME_IDF_DEFAULT_TARGETS", "all") ) +# An explicitly set ESPHOME_IDF_DEFAULT_TARGETS overrides the per-variant +# targets a caller requests, so a builder image can still pre-warm every +# target with one env var. +_IDF_DEFAULT_TARGETS_EXPLICIT = bool(os.environ.get("ESPHOME_IDF_DEFAULT_TARGETS")) ESPHOME_IDF_DEFAULT_TOOLS = str_to_lst_of_str( os.environ.get("ESPHOME_IDF_DEFAULT_TOOLS", "cmake;ninja") @@ -63,7 +70,7 @@ ESPHOME_IDF_FRAMEWORK_MIRRORS = str_to_lst_of_str( os.environ.get("ESPHOME_IDF_FRAMEWORK_MIRRORS") or [ "https://github.com/esphome-libs/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.tar.xz", - "https://github.com/esphome-libs/esp-idf/releases/download/v{MAJOR}.{MINOR}{EXTRA}/esp-idf-v{MAJOR}.{MINOR}{EXTRA}.tar.xz", + "https://github.com/esphome-libs/esp-idf/releases/download/v{SHORT_VERSION}/esp-idf-v{SHORT_VERSION}.tar.xz", ] ) @@ -196,7 +203,35 @@ def _get_python_env_path(version: str) -> Path: return get_idf_tools_path() / "penvs" / f"{version}" -def _check_stamp(file: PathType, data: dict[str, str]) -> bool: +def _read_stamp(file: PathType) -> dict | None: + """Return a stamp file's dict contents, or None if missing or invalid. + + A missing stamp is the normal first-install case and stays silent; the + other branches indicate a real fault that forces a full reinstall on + every build, so they warn. + """ + try: + with Path(file).open(encoding="utf-8") as f: + data = json.load(f) + except FileNotFoundError: + return None + except json.JSONDecodeError as e: + _LOGGER.warning("Ignoring corrupt stamp file %s: %s", file, e) + return None + except OSError as e: + _LOGGER.warning("Could not read stamp file %s: %s", file, e) + return None + if not isinstance(data, dict): + _LOGGER.warning( + "Ignoring stamp file %s with unexpected type %s", + file, + type(data).__name__, + ) + return None + return data + + +def _check_stamp(file: PathType, data: dict[str, Any]) -> bool: """ Check if a stamp file contains the expected data. @@ -207,17 +242,43 @@ def _check_stamp(file: PathType, data: dict[str, str]) -> bool: Returns: True if file exists and contains expected data, False otherwise """ - if not Path(file).is_file(): + return _read_stamp(file) == data + + +def _stamps_match_except_targets(stored: dict, requested: dict) -> bool: + """Whether two stamps agree on every field other than ``targets``. + + Compares whole dicts (minus ``targets``) rather than named keys so any + stamp field added later participates in invalidation by default instead + of being silently ignored. + """ + + def _strip(stamp: dict) -> dict: + return {k: v for k, v in stamp.items() if k != "targets"} + + return _strip(stored) == _strip(requested) + + +def _stamp_covers(stored: dict | None, requested: dict) -> bool: + """Return True if a stored framework stamp already covers this request. + + Every field except ``targets`` must match exactly. ``targets`` may be a + superset of the requested ones: ``idf_tools.py install`` accumulates + targets in idf-env.json across runs, so a framework installed for more + targets than this build needs is still valid. A stored ``all`` covers + every target. + """ + if stored is None: return False - - try: - with Path(file).open(encoding="utf-8") as f: - return json.load(f) == data - except (json.JSONDecodeError, OSError): + if not _stamps_match_except_targets(stored, requested): return False + stored_targets = stored.get("targets") + if not isinstance(stored_targets, list): + return False + return "all" in stored_targets or set(requested["targets"]) <= set(stored_targets) -def _write_stamp(file: PathType, data: dict[str, str]): +def _write_stamp(file: PathType, data: dict[str, Any]): """ Write data to a stamp file in JSON format. @@ -229,6 +290,40 @@ def _write_stamp(file: PathType, data: dict[str, str]): json.dump(data, fp) +def _run_idf_tools_script( + idf_framework_root: PathType, + script_name: str, + msg: str, + args: list[str] | None = None, + env: dict[str, str] | None = None, +) -> tuple[bool, str | None, str | None]: + """Run one of the sibling idf_tools-backed helper scripts. + + The script is executed with the framework's ``tools`` directory on + PYTHONPATH so it imports the framework's own ``idf_tools`` module. + """ + cmd = [ + get_system_python_path(), + str(_SCRIPTS_DIR / script_name), + str(idf_framework_root), + *(args or []), + ] + return run_command( + cmd, + msg=msg, + env=(env or os.environ) + | {"PYTHONPATH": str(Path(idf_framework_root) / "tools")}, + ) + + +def _raise_script_failure(what: str, root: PathType, stderr: str | None) -> NoReturn: + """Raise RuntimeError for a failed helper script, appending stderr detail.""" + detail = (stderr or "").strip() + raise RuntimeError( + f"Can't get {what} of {root}" + (f": {detail}" if detail else "") + ) + + def _get_idf_version( idf_framework_root: PathType, env: dict[str, str] | None = None ) -> str: @@ -246,26 +341,13 @@ def _get_idf_version( RuntimeError: If ESP-IDF version cannot be determined """ - cmd = [ - get_system_python_path(), - str(_SCRIPTS_DIR / "get_idf_version.py"), - str(idf_framework_root), - ] - - success, stdout, stderr = run_command( - cmd, - msg="ESP-IDF version", - env=(env or os.environ) - | {"PYTHONPATH": str(Path(idf_framework_root) / "tools")}, + success, stdout, stderr = _run_idf_tools_script( + idf_framework_root, "get_idf_version.py", "ESP-IDF version", env=env ) if stdout: stdout = stdout.strip() if not success or not stdout: - detail = (stderr or "").strip() - raise RuntimeError( - f"Can't get ESP-IDF version of {idf_framework_root}" - + (f": {detail}" if detail else "") - ) + _raise_script_failure("ESP-IDF version", idf_framework_root, stderr) return stdout @@ -286,24 +368,11 @@ def _get_idf_tool_paths( RuntimeError: If ESP-IDF tool paths cannot be determined """ - cmd = [ - get_system_python_path(), - str(_SCRIPTS_DIR / "get_idf_tool_paths.py"), - str(idf_framework_root), - ] - - success, stdout, stderr = run_command( - cmd, - msg="ESP-IDF tool paths", - env=(env or os.environ) - | {"PYTHONPATH": str(Path(idf_framework_root) / "tools")}, + success, stdout, stderr = _run_idf_tools_script( + idf_framework_root, "get_idf_tool_paths.py", "ESP-IDF tool paths", env=env ) if not success or not stdout: - detail = (stderr or "").strip() - raise RuntimeError( - f"Can't get ESP-IDF tool paths of {idf_framework_root}" - + (f": {detail}" if detail else "") - ) + _raise_script_failure("ESP-IDF tool paths", idf_framework_root, stderr) # Extract json values try: @@ -386,9 +455,10 @@ def _clone_idf_with_submodules( handles branches, tags, and SHAs uniformly (mirrors the approach in ``esphome.git.clone_or_update``). """ - from esphome.git import run_git_command + from esphome.git import run_git_command, update_submodules - _LOGGER.info("Cloning ESP-IDF from %s%s", git_url, f"@{ref}" if ref else "") + key = f"{git_url}@{ref}" if ref else git_url + _LOGGER.info("Cloning ESP-IDF from %s", key) run_git_command(["git", "clone", "--depth=1", "--", git_url, str(framework_path)]) if ref: run_git_command( @@ -399,25 +469,14 @@ def _clone_idf_with_submodules( ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=framework_path, ) - run_git_command( - [ - "git", - "submodule", - "update", - "--init", - "--recursive", - "--depth=1", - ], - git_dir=framework_path, - ) + update_submodules(framework_path, key) - # Sanity-check the resulting tree. run_git_command only raises when - # stderr is non-empty, so a clone that silently produces no working - # tree would otherwise be marked extracted and stuck until - # ``esphome clean``. + # Sanity-check the resulting tree: a clone can exit 0 yet produce no + # usable ESP-IDF checkout, which would otherwise be marked extracted and + # stuck until ``esphome clean``. if not (framework_path / "tools" / "idf_tools.py").is_file(): raise RuntimeError( - f"Clone of {git_url} produced no usable ESP-IDF tree at {framework_path}" + f"Clone of {key} produced no usable ESP-IDF tree at {framework_path}" ) @@ -466,17 +525,21 @@ _NINJA_ARM64_BACKPORT: dict[str, dict[str, str | int]] = { } -def _patch_tools_json_for_linux_arm64(framework_path: Path) -> None: - """Inject ninja linux-arm64 entries into the framework's tools.json on aarch64. +def _patch_tools_json( + framework_path: Path, + apply_patch: Callable[[dict], bool], + patched_log: str, +) -> None: + """Apply an in-place fixup to the framework's tools/tools.json. - Idempotent: a tools.json that already has the entry, or a host that - isn't aarch64, is a no-op. Applied unconditionally on every install - check so a build dir extracted before the backport got fixed up - without forcing a clean. + Shared plumbing for the tools.json patches below: a missing file is a + no-op, an unparseable file logs a warning and skips, and when + ``apply_patch`` reports a change the file is written back atomically. + ``patched_log`` is the info log line, with a single ``%s`` placeholder + for the tools.json path. Patches are idempotent and applied on every + install check, so an already-extracted framework picks them up on the + next build without forcing a clean. """ - if platform.machine() != "aarch64": - return - tools_json = framework_path / "tools" / "tools.json" if not tools_json.is_file(): return @@ -484,37 +547,206 @@ def _patch_tools_json_for_linux_arm64(framework_path: Path) -> None: try: with tools_json.open(encoding="utf-8") as f: data = json.load(f) - except (json.JSONDecodeError, OSError) as e: + # apply_patch also raises inside the guard: a tools.json that is + # valid JSON but not the expected shape (e.g. a top-level list) + # must skip the patch, not crash the install check this patch is + # meant to recover. + changed = apply_patch(data) + except (json.JSONDecodeError, OSError, AttributeError, TypeError, KeyError) as e: _LOGGER.warning( - "Could not parse %s for linux-arm64 backport (%s); " - "skipping. A clean reinstall of the framework directory " - "may be needed.", + "Could not apply tools.json patch to %s (%s); skipping. A clean " + "reinstall of the framework directory may be needed.", tools_json, e, ) return - changed = False - for tool in data.get("tools", []): - if tool.get("name") != "ninja": - continue - for ver in tool.get("versions", []): - entry = _NINJA_ARM64_BACKPORT.get(ver.get("name")) - if entry is None or ver.get("linux-arm64"): - continue - ver["linux-arm64"] = entry - changed = True - if changed: # write_file_if_changed stages a tempfile in the destination dir # and atomically replaces — safe against mid-write interruption # and concurrent invocations. write_file_if_changed(tools_json, json.dumps(data, indent=2) + "\n") - _LOGGER.info( - "Patched %s to add ninja linux-arm64 download " - "(espressif/esp-idf#18272 backport).", - tools_json, + _LOGGER.info(patched_log, tools_json) + + +def _patch_tools_json_for_linux_arm64(framework_path: Path) -> None: + """Inject ninja linux-arm64 entries into the framework's tools.json on aarch64. + + A tools.json that already has the entry, or a host that isn't aarch64, + is a no-op. + """ + if platform.machine() != "aarch64": + return + + def apply_patch(data: dict) -> bool: + changed = False + for tool in data.get("tools", []): + if tool.get("name") != "ninja": + continue + for ver in tool.get("versions", []): + entry = _NINJA_ARM64_BACKPORT.get(ver.get("name")) + if entry is None or ver.get("linux-arm64"): + continue + ver["linux-arm64"] = entry + changed = True + return changed + + _patch_tools_json( + framework_path, + apply_patch, + "Patched %s to add ninja linux-arm64 download " + "(espressif/esp-idf#18272 backport).", + ) + + +# Tools marked ``install: always`` in tools.json that no ESPHome build ever +# runs. openocd-esp32 is a JTAG debug server (its post-install check also +# fails outright on systems without libusb-1.0, #17685). The gdb bundles are +# debuggers used only by ``idf.py gdb``/``idf.py monitor`` flows ESPHome never +# invokes; stack decoding uses addr2line from the compiler toolchains instead. +# esp32ulp-elf is the ULP coprocessor toolchain, and ESPHome excludes the IDF +# ``ulp`` component from every build. esp-rom-elfs stays required: the cmake +# gdbinit generation reads ESP_ROM_ELF_DIR during every configure and warns +# when it is missing. +_UNUSED_IDF_TOOLS: tuple[str, ...] = ( + "esp32ulp-elf", + "openocd-esp32", + "riscv32-esp-elf-gdb", + "xtensa-esp-elf-gdb", +) + +# tools.json also lists riscv32-esp-elf as supported on the xtensa chips +# because the S2/S3 ULP coprocessor is a RISC-V core, so installing for an +# S2/S3 target pulls in the whole riscv compiler (~290MB download, 2GB disk) +# just for ULP programs — which ESPHome never builds (the IDF ``ulp`` +# component is excluded by default; a user who re-enables it via +# ``include_builtin_idf_components: [ulp]`` on an S2/S3 and hits a missing +# riscv compiler can set ESPHOME_IDF_DEFAULT_TARGETS=all to install it). +# Removing the xtensa chips from its supported targets keeps it out of +# xtensa-only installs; building a RISC-V variant still installs it. Add any +# future Xtensa chip here; a missing entry only costs the download, while a +# wrongly listed RISC-V chip would strip its own compiler. +_XTENSA_TARGETS: tuple[str, ...] = ("esp32", "esp32s2", "esp32s3") + + +def _patch_tools_json_demote_unused_tools(framework_path: Path) -> None: + """Demote tools ESPHome never runs from ``install: always`` to ``on_request``. + + ``idf_tools.py install required`` downloads every tool marked ``always`` + in tools.json and validates each one after extraction by running its + version command. Demoting the tools in ``_UNUSED_IDF_TOOLS`` drops them + from the ``required`` set: they are no longer downloaded or validated, + and the tool-path export treats a missing ``on_request`` tool as fine. + Besides the download and disk savings, this makes the openocd libusb + validation failure (#17685) impossible; because this runs on every + install check, an install stuck in that failing state (which never wrote + its stamp file) heals on the next build without a clean. A user who + wants one of these tools can still name it explicitly in + ESPHOME_IDF_DEFAULT_TOOLS; explicit names bypass install-type filtering. + + Also removes the xtensa chips from riscv32-esp-elf's supported targets + (see ``_XTENSA_TARGETS``) so xtensa-only installs don't pull in the + RISC-V compiler for ULP programs ESPHome never builds. + """ + + def apply_patch(data: dict) -> bool: + changed = False + for tool in data.get("tools", []): + if ( + tool.get("name") in _UNUSED_IDF_TOOLS + and tool.get("install") == "always" + ): + tool["install"] = "on_request" + changed = True + if tool.get("name") == "riscv32-esp-elf": + targets = tool.get("supported_targets") + # Guard the type so unexpected JSON here cannot abort the + # other demotions; this patch is best-effort. Log it so a + # silently resumed riscv download is diagnosable. + if not isinstance(targets, list): + _LOGGER.warning( + "Unexpected supported_targets for riscv32-esp-elf " + "in tools.json (%s); not excluding it from xtensa " + "installs", + type(targets).__name__, + ) + continue + if any(t in targets for t in _XTENSA_TARGETS): + tool["supported_targets"] = [ + t for t in targets if t not in _XTENSA_TARGETS + ] + changed = True + return changed + + _patch_tools_json( + framework_path, + apply_patch, + "Patched %s to skip installing tools ESPHome does not use " + "(openocd, gdb, ULP toolchains).", + ) + + +def _prefetch_idf_tool_archives( + framework_path: Path, + targets_str: str, + tools: list[str], + env: dict[str, str] | None, +) -> None: + """Pre-download the tool archives ``idf_tools.py install`` would fetch. + + ``idf_tools.py``'s own downloader restarts from byte zero on every retry, + which makes large archives effectively impossible to fetch on unstable + connections (#17703). This asks the framework's idf_tools (via + ``get_tool_downloads.py``) which archives the coming install needs, then + downloads each into ``/dist`` with + ``download_with_resume``. The installer then finds the verified archives + already in place ("file ... is already downloaded") and never touches the + network. + + Strictly best-effort: any failure here just logs and returns, leaving + ``idf_tools.py install`` to download whatever is missing exactly as + before. Leftover ``.part`` files live in ``dist/`` and are removed by the + post-install cache prune. + """ + try: + success, stdout, stderr = _run_idf_tools_script( + framework_path, + "get_tool_downloads.py", + "ESP-IDF tool download list", + args=[targets_str, *tools], + env=env, ) + if not success or not stdout: + _LOGGER.warning( + "Could not determine ESP-IDF tool downloads: %s", + (stderr or "").strip(), + ) + return + dist_path = get_idf_tools_path() / "dist" + entries = [ + entry + for entry in json.loads(stdout) + if not (dist_path / entry["dest"]).is_file() + ] + for index, entry in enumerate(entries, start=1): + _LOGGER.info( + "Downloading %s (%d/%d) ...", entry["name"], index, len(entries) + ) + try: + download_with_resume( + entry["url"], + dist_path / entry["dest"], + sha256=entry["sha256"], + size=entry["size"], + ) + except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught + # Keep prefetching the remaining archives; the installer + # will retry this one itself (without resume). + _LOGGER.warning("Could not prefetch %s: %s", entry["name"], e) + except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught + # The installer downloads anything missing itself; never let the + # prefetch become a new way for the install to fail. + _LOGGER.warning("ESP-IDF tool prefetch failed: %s", e) def _check_esphome_idf_framework_install( @@ -536,13 +768,19 @@ def _check_esphome_idf_framework_install( env: Optional dictionary of environment variables to set source_url: Optional override URL for the framework tarball. Supports the same ``{VERSION}`` / ``{MAJOR}`` / ``{MINOR}`` / ``{PATCH}`` / - ``{EXTRA}`` substitutions as ESPHOME_IDF_FRAMEWORK_MIRRORS - (``{EXTRA}`` includes its leading ``-``, e.g. ``-rc1``, or is empty). - When set, it replaces the default mirror list — no implicit fallback, - so a misspelled URL fails loudly. + ``{EXTRA}`` / ``{SHORT_VERSION}`` substitutions as + ESPHOME_IDF_FRAMEWORK_MIRRORS (``{EXTRA}`` includes its leading + ``-``, e.g. ``-rc1``, or is empty; ``{SHORT_VERSION}`` is ``x.y`` + plus any extra and only available for x.y.0 versions — a URL + referencing it is skipped for other versions). When set, it + replaces the default mirror list — no implicit fallback, so a + misspelled or skipped URL fails loudly with an EsphomeError naming + the URL. Returns: - tuple of (framework_path, install_flag) + tuple of (framework_path, fresh_extract_flag). The flag is True only + when the framework tree was downloaded and extracted this run, not + when tools were installed into an existing tree. """ # Sanitize inputs @@ -575,8 +813,8 @@ def _check_esphome_idf_framework_install( # avoids post-extraction renames that race with antivirus on Windows. # Tool install state is tracked separately by the stamp file in step 3, # so we only re-extract when extraction itself is missing or incomplete. - install = force or not extracted_marker.is_file() - if install: + fresh_extract = force or not extracted_marker.is_file() + if fresh_extract: rmdir(framework_path, msg=f"Clean up ESP-IDF {version} framework") git_source = _parse_git_source(source_url) if source_url else None @@ -584,28 +822,51 @@ def _check_esphome_idf_framework_install( git_url, ref = git_source _clone_idf_with_submodules(framework_path, git_url, ref) else: - # Download in temporary file - with tempfile.NamedTemporaryFile() as tmp: - _LOGGER.info("Downloading ESP-IDF %s framework ...", version) + _LOGGER.info("Downloading ESP-IDF %s framework ...", version) - # Create substitutions for the URLs - substitutions = {"VERSION": version} - try: - ver = Version.parse(version) - substitutions["MAJOR"] = str(ver.major) - substitutions["MINOR"] = str(ver.minor) - substitutions["PATCH"] = str(ver.patch) - substitutions["EXTRA"] = f"-{ver.extra}" if ver.extra else "" - except ValueError: - pass - - mirrors = [source_url] if source_url else ESPHOME_IDF_FRAMEWORK_MIRRORS - download_from_mirrors(mirrors, substitutions, tmp.file) - - _LOGGER.info("Extracting ESP-IDF %s framework ...", version) - archive_extract_all( - tmp.file, framework_path, progress_header="Extracting" + # Create substitutions for the URLs. SHORT_VERSION (x.y with + # optional -extra) is only provided for x.y.0 releases, since + # the vX.Y release tags only exist for those; templates that + # reference it are skipped for other versions by + # download_from_mirrors. + substitutions = {"VERSION": version} + try: + ver = Version.parse(version) + substitutions["MAJOR"] = str(ver.major) + substitutions["MINOR"] = str(ver.minor) + substitutions["PATCH"] = str(ver.patch) + substitutions["EXTRA"] = f"-{ver.extra}" if ver.extra else "" + if ver.patch == 0: + substitutions["SHORT_VERSION"] = ( + f"{ver.major}.{ver.minor}{substitutions['EXTRA']}" + ) + except ValueError: + _LOGGER.warning( + "ESP-IDF version '%s' is not a valid version number; " + "only the {VERSION} substitution is available for " + "mirror URLs", + version, ) + + mirrors = [source_url] if source_url else ESPHOME_IDF_FRAMEWORK_MIRRORS + # Download to a persistent file in the tool download cache (not + # a temp file) so an interrupted download resumes on the next + # run; the cache is pruned after a successful install anyway. + tarball_path = get_idf_tools_path() / "dist" / f"esp-idf-{version}.tar.xz" + download_from_mirrors(mirrors, substitutions, tarball_path) + + _LOGGER.info("Extracting ESP-IDF %s framework ...", version) + try: + with tarball_path.open("rb") as tarball: + archive_extract_all( + tarball, framework_path, progress_header="Extracting" + ) + finally: + # Success: drop the archive rather than caching ~70MB twice. + # Failure: a corrupt archive (e.g. torn by an unclean + # shutdown) must not be reused — without a checksum only a + # failed extraction can expose it, so force a re-download. + tarball_path.unlink(missing_ok=True) extracted_marker.touch() # Idempotent post-extract patch: written every invocation so a build @@ -618,10 +879,17 @@ def _check_esphome_idf_framework_install( # a pre-patch tools.json get fixed up without forcing a clean. _patch_tools_json_for_linux_arm64(framework_path) + # Drop tools ESPHome never runs from the required tool set on every + # invocation, so an install that previously failed on the openocd libusb + # check recovers on the next build. + _patch_tools_json_demote_unused_tools(framework_path) + # 3. Check if the framework tools are the same and correctly installed + stored_stamp = None if fresh_extract else _read_stamp(env_stamp_file) + install = fresh_extract if not install: install = True - if _check_stamp(env_stamp_file, stamp_info): + if _stamp_covers(stored_stamp, stamp_info): _LOGGER.info("Checking ESP-IDF %s framework installation ...", version) # Validate via the managed tool-path resolution, not ``idf_tools.py check``: # ``check`` probes tools on the system PATH and aborts if any fail to run (e.g. a @@ -638,6 +906,7 @@ def _check_esphome_idf_framework_install( if install: _LOGGER.info("Installing ESP-IDF %s framework ...", version) targets_str = ",".join(targets) + _prefetch_idf_tool_archives(framework_path, targets_str, tools, env) cmd = [ get_system_python_path(), str(idf_tools_path), @@ -651,11 +920,45 @@ def _check_esphome_idf_framework_install( env=env, stream_output=True, ): + if platform.system() == "Linux" and find_library("usb-1.0") is None: + _LOGGER.error( + "libusb-1.0.so.0 was not found on this system. If the error " + "above mentions it (openocd fails its install check without " + "it), install the libusb 1.0 package, e.g. libusb-1.0-0 " + "(Debian/Ubuntu), libusb1 (Fedora) or libusb (Alpine/Arch), " + "then run the build again." + ) raise RuntimeError(f"ESP-IDF {version} framework installation failure") + # idf_tools.py extracts tool archives from /dist into tools/; the + # archives are not needed afterward and, already compressed, dominate the cached install. + # Best-effort: a failure to prune must not fail an otherwise successful install. + try: + rmdir( + get_idf_tools_path() / "dist", msg="Remove ESP-IDF tool download cache" + ) + except RuntimeError as err: + _LOGGER.debug("Could not remove ESP-IDF tool download cache: %s", err) + + # Record the union of every target installed so far, not just this + # build's. idf_tools.py accumulates targets in idf-env.json and the + # ``required`` metapackage installs tools for all of them, so the + # union is what is actually on disk — and it keeps two variants + # alternating between builds from re-running the installer each time. + # Merge only when everything except targets matches: a reinstall + # triggered by a schema or tools change ran the installer for this + # build's targets alone, so carrying the old targets forward would + # let later builds of those variants skip the reinstall they need. + if ( + stored_stamp + and isinstance(stored_stamp.get("targets"), list) + and _stamps_match_except_targets(stored_stamp, stamp_info) + ): + merged = set(stamp_info["targets"]) | set(stored_stamp["targets"]) + stamp_info["targets"] = ["all"] if "all" in merged else sorted(merged) _write_stamp(env_stamp_file, stamp_info) - return framework_path, install + return framework_path, fresh_extract def _check_esp_idf_python_env_install( @@ -801,7 +1104,11 @@ def check_esp_idf_install( env["IDF_TOOLS_PATH"] = str(get_idf_tools_path()) env["IDF_PATH"] = "" - targets = targets or ESPHOME_IDF_DEFAULT_TARGETS + # An explicit ESPHOME_IDF_DEFAULT_TARGETS wins over the caller's + # per-variant request (builder-image pre-warm); otherwise the caller's + # targets are used, falling back to the default when none were given. + if _IDF_DEFAULT_TARGETS_EXPLICIT or not targets: + targets = ESPHOME_IDF_DEFAULT_TARGETS # Determine which tools need to be installed if not provided if tools is None: @@ -814,15 +1121,18 @@ def check_esp_idf_install( tools.append(tool) # 1) Framework - framework_path, installed = _check_esphome_idf_framework_install( + framework_path, fresh_extract = _check_esphome_idf_framework_install( version, targets, tools, force=force, env=env, source_url=source_url ) features = features or ESPHOME_IDF_DEFAULT_FEATURES - # 2) Python env - python_env_path, installed = _check_esp_idf_python_env_install( - version, features, force=force or installed, env=env + # 2) Python env. Only a freshly extracted framework forces a rebuild — + # the venv depends on the framework version and features, not on which + # toolchains are installed, so adding a target to an existing tree must + # not wipe it. It still self-validates against its own stamp. + python_env_path, _ = _check_esp_idf_python_env_install( + version, features, force=force or fresh_extract, env=env ) return framework_path, python_env_path diff --git a/esphome/espidf/get_tool_downloads.py b/esphome/espidf/get_tool_downloads.py new file mode 100644 index 0000000000..37a6126fae --- /dev/null +++ b/esphome/espidf/get_tool_downloads.py @@ -0,0 +1,88 @@ +"""Print JSON download info for the ESP-IDF tools an install would fetch. + +Run via ``python ...``. +PYTHONPATH must include ``/tools`` so ``idf_tools`` is +importable, and IDF_TOOLS_PATH must be set. Prints a JSON list of +``{name, url, size, sha256, dest}`` for every tool version that is not yet +installed, where ``dest`` is the archive filename ``idf_tools.py install`` +expects to find in ``/dist``. Tools with no download for the +current platform are skipped; already-installed versions are skipped so a +pruned download cache is not re-fetched. + +The target/tool expansion mirrors ``idf_tools.py install`` (targets passed to +``add_and_check_targets`` accumulate with idf-env.json) but nothing is saved +or written — this script only reports what the install would download. +""" + +# pylint: disable=import-error # idf_tools is on PYTHONPATH at runtime only + +from contextlib import redirect_stdout +import json +import os +from pathlib import Path +import sys + +from idf_tools import ( + CURRENT_PLATFORM, + TOOLS_FILE, + IDFEnv, + ToolBinaryError, + add_and_check_targets, + expand_tools_arg, + g, + get_idf_download_url_apply_mirrors, + load_tools_info, +) + + +def collect_downloads() -> list[dict]: + g.idf_path = sys.argv[1] + g.idf_tools_path = os.environ.get("IDF_TOOLS_PATH") + g.tools_json = str(Path(g.idf_path) / TOOLS_FILE) + + targets = add_and_check_targets(IDFEnv.get_idf_env(), sys.argv[2]) + tools_info = load_tools_info() + downloads: list[dict] = [] + + for name in expand_tools_arg(sys.argv[3:], tools_info, targets): + if "@" in name: + name, version = name.split("@", 1) + else: + version = None + tool = tools_info.get(name) + if tool is None or not tool.compatible_with_platform(): + continue + version = version or tool.get_recommended_version() + if version is None: + continue + try: + tool.find_installed_versions() + except ToolBinaryError as e: + # A broken installed binary is idf_tools' problem to repair on + # install; note it and treat the version as not installed. + print(f"tool {name} failed its binary check: {e}", file=sys.stderr) + if version in tool.versions_installed or version not in tool.versions: + continue + download = tool.versions[version].get_download_for_platform(CURRENT_PLATFORM) + if download is None: + continue + downloads.append( + { + "name": f"{name}@{version}", + # Apply the same IDF_MIRROR_PREFIX_MAP / IDF_GITHUB_ASSETS + # rewriting the installer's own downloader applies, so users + # behind a mirror prefetch from the mirror too. + "url": get_idf_download_url_apply_mirrors(None, download.url), + "size": download.size, + "sha256": download.sha256, + "dest": download.rename_dist or Path(download.url).name, + } + ) + return downloads + + +# idf_tools prints informational lines (e.g. mirror URL rewrites) to stdout; +# route them to stderr so stdout carries only the JSON result. +with redirect_stdout(sys.stderr): + result = collect_downloads() +print(json.dumps(result)) diff --git a/esphome/espidf/idedata.py b/esphome/espidf/idedata.py index 0ed357a759..0047d568e2 100644 --- a/esphome/espidf/idedata.py +++ b/esphome/espidf/idedata.py @@ -6,7 +6,7 @@ toolchain has no such command, but its CMake build emits turns that file into the same fields consumers (IDE integration, clang-tidy) expect: - {cxx_path, cxx_flags, defines, includes: {build, toolchain}} + {cc_path, cxx_path, cxx_flags, defines, includes: {build, toolchain}} """ from __future__ import annotations @@ -197,6 +197,28 @@ def _get_toolchain_includes(cxx_path: str) -> list[str]: return includes +def _cc_path_from_cxx(cxx_path: str) -> str: + """Derive the C compiler path from the C++ compiler path. + + compile_commands.json only names the C++ compiler, but consumers reach the + rest of the toolchain (objdump, readelf, addr2line) by rewriting the tail of + ``cc_path``, so they need the ``gcc``-suffixed name. + """ + stem, suffix = ( + (cxx_path[: -len(".exe")], ".exe") + if cxx_path.endswith(".exe") + else (cxx_path, "") + ) + # Rewrite the program name only when it is g++ itself, or a toolchain + # prefixed one such as xtensa-esp32-elf-g++ -> xtensa-esp32-elf-gcc. + # Requiring a separator before the "g++" keeps names that merely end in + # those three characters intact: "clang++" must not become "clangcc". + head = stem[: -len("g++")] + if stem.endswith("g++") and (not head or head.endswith(("-", "/", "\\"))): + stem = f"{head}gcc" + return f"{stem}{suffix}" + + def idedata_from_build(compile_commands: Path) -> dict: """Parse compile_commands.json into the idedata fields consumers expect. @@ -218,6 +240,7 @@ def idedata_from_build(compile_commands: Path) -> dict: build_includes.setdefault(inc, None) return { + "cc_path": _cc_path_from_cxx(cxx_path), "cxx_path": cxx_path, "cxx_flags": cxx_flags, "defines": defines, diff --git a/esphome/espidf/size_summary.py b/esphome/espidf/size_summary.py index 3ba0bf3b4d..7a5305ff0c 100644 --- a/esphome/espidf/size_summary.py +++ b/esphome/espidf/size_summary.py @@ -57,7 +57,7 @@ def _find_app_partition_size(partitions_csv: Path) -> int: """ if not partitions_csv.is_file(): raise ValueError(f"partitions.csv not found at {partitions_csv}") - for row in csv.reader(partitions_csv.read_text().splitlines()): + for row in csv.reader(partitions_csv.read_text(encoding="utf-8").splitlines()): cells = [c.strip() for c in row] if not cells or cells[0].startswith("#") or len(cells) < 5: continue @@ -89,7 +89,7 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None: _LOGGER.debug("Skipping size summary: %s not found", size_json) return try: - data = json.loads(size_json.read_text()) + data = json.loads(size_json.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as e: _LOGGER.debug("Skipping size summary: %s", e) return diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 000ce739db..fd95805c6c 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -9,8 +9,19 @@ import re import shutil import subprocess -from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION -from esphome.const import CONF_FRAMEWORK, CONF_SOURCE +from esphome.components.esp32.const import ( + KEY_ESP32, + KEY_FLASH_SIZE, + KEY_IDF_VERSION, + KEY_VARIANT, + variant_to_idf_target, +) +from esphome.const import ( + CONF_COMPILE_PROCESS_LIMIT, + CONF_ESPHOME, + CONF_FRAMEWORK, + CONF_SOURCE, +) from esphome.core import CORE, EsphomeError from esphome.espidf.framework import check_esp_idf_install, get_framework_env from esphome.espidf.size_summary import print_summary @@ -51,6 +62,27 @@ def _get_framework_source_override() -> str | None: return CORE.config.get(KEY_ESP32, {}).get(CONF_FRAMEWORK, {}).get(CONF_SOURCE) +def _get_configured_targets() -> list[str] | None: + """Return the IDF install target for the configured variant, if known. + + Limiting the toolchain install to the variant being built skips the other + architecture's compiler entirely (several hundred MB of download and 1-2GB + of disk). idf_tools.py accumulates targets across runs, so building a + second variant later installs just its toolchain incrementally. None (no + variant stored, e.g. tooling outside a build) falls back to the default + inside check_esp_idf_install. + + CI always installs every target (None falls through to the "all" + default): runners share one toolchain cache across jobs that build + different variants, so a full install keeps the cached tree identical + everywhere instead of per-variant supersets invalidating each other. + """ + if os.environ.get("CI"): + return None + variant = CORE.data.get(KEY_ESP32, {}).get(KEY_VARIANT) + return [variant_to_idf_target(variant)] if variant else None + + def _get_esphome_esp_idf_paths( version: str | None = None, ) -> tuple[os.PathLike, os.PathLike]: @@ -58,7 +90,9 @@ def _get_esphome_esp_idf_paths( paths = _cache().paths if version not in paths: paths[version] = check_esp_idf_install( - version, source_url=_get_framework_source_override() + version, + targets=_get_configured_targets(), + source_url=_get_framework_source_override(), ) return paths[version] @@ -94,6 +128,14 @@ def _get_idf_env(version: str | None = None) -> dict[str, str]: def _get_cmake_output(build_dir) -> str: cmake_output_cache = _cache().cmake_output if build_dir not in cmake_output_cache: + # Check the build before resolving the env: _get_idf_env() runs + # check_esp_idf_install(), which can download and install the whole + # framework. Never start that for a build that isn't there. Callers + # such as the log stack-trace decoder run against devices that were + # never compiled on this machine. + if not (Path(build_dir) / "CMakeCache.txt").is_file(): + raise EsphomeError(f"No ESP-IDF build found in {build_dir}") + cmd = ["cmake", "-LA", "-N", "."] env = _get_idf_env() @@ -147,7 +189,10 @@ def _get_idf_tool(name: str) -> str: def run_idf_py( - *args, cwd: Path | None = None, capture_output: bool = False + *args, + cwd: Path | None = None, + capture_output: bool = False, + jobs: int | None = None, ) -> int | str: """Run idf.py with the given arguments.""" idf_path = _get_idf_path() @@ -155,6 +200,8 @@ def run_idf_py( raise EsphomeError("ESP-IDF not found") env = _get_idf_env() + if jobs is not None: + env = {**env, "IDF_PY_BUILD_JOBS": str(jobs)} python_executable = _get_idf_tool("python") idf_py = idf_path / "tools" / "idf.py" # Dispatch idf.py through esphome.espidf.runner, which wraps @@ -384,7 +431,7 @@ def run_compile(config, verbose: bool) -> int: args.append("build") args.append("size") - rc = run_idf_py(*args) + rc = run_idf_py(*args, jobs=config[CONF_ESPHOME].get(CONF_COMPILE_PROCESS_LIMIT)) if rc == 0: size_json = CORE.relative_build_path("build", "esp_idf_size.json") partitions = CORE.relative_build_path("partitions.csv") @@ -467,9 +514,16 @@ def get_idedata() -> dict | None: cache = CORE.relative_internal_path("idedata", f"{CORE.name}.json") if cache.is_file() and cache.stat().st_mtime >= compile_commands.stat().st_mtime: try: - return json.loads(cache.read_text(encoding="utf-8")) + cached = json.loads(cache.read_text(encoding="utf-8")) except ValueError: pass + else: + # Caches written before cc_path was emitted stay newer than + # compile_commands.json forever, so rebuild them on the field rather + # than on the timestamp. Check the type too: a corrupted cache can + # still be valid JSON, and "in" would match a substring of a string. + if isinstance(cached, dict) and "cc_path" in cached: + return cached data = idedata_from_build(compile_commands) data["prog_path"] = str(get_elf_path()) diff --git a/esphome/espota2.py b/esphome/espota2.py index 266702c142..fa15c1dda2 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -52,6 +52,7 @@ RESPONSE_ERROR_PARTITION_TABLE_VERIFY = 0x8F RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90 RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91 RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92 +RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93 RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 @@ -157,6 +158,11 @@ _ERROR_MESSAGES: dict[int, str] = { "the bootloader update without rebooting the device. If the device " "fails to boot, recover it via a serial flash." ), + RESPONSE_ERROR_VERSION_DOWNGRADE: ( + "The device rejected the update because it has OTA downgrade protection " + "enabled: the new firmware's version must be newer than the version the " + "device is currently running." + ), RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP", } diff --git a/esphome/external_files.py b/esphome/external_files.py index 4e73c8dc21..69423d3999 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -165,11 +165,8 @@ def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> by _LOGGER.debug("Remote file has not changed %s", url) return path.read_bytes() - _LOGGER.debug( - "Remote file has changed, downloading from %s to %s", - url, - path, - ) + _LOGGER.info("Downloading %s", url) + _LOGGER.debug("Saving to %s", path) try: req = requests.get( @@ -210,9 +207,13 @@ def download_content_many( items: Iterable[tuple[str, Path]], timeout: int = NETWORK_TIMEOUT, max_workers: int = DEFAULT_DOWNLOAD_WORKERS, + description: str = "remote file(s)", ) -> None: """Run `download_content` for each (url, path) pair concurrently. + `description` names the kind of files in the progress log line, e.g. + "wake word manifest(s)". + Wall time drops from `sum(latency)` to roughly `max(latency)` for cached files where the HEAD round-trip dominates. All workers run to completion before this returns; every `cv.Invalid` raised by a worker @@ -230,6 +231,7 @@ def download_content_many( seen: dict[Path, str] = {path: url for url, path in items} if not seen: return + _LOGGER.info("Checking %d %s for updates", len(seen), description) if len(seen) == 1: path, url = next(iter(seen.items())) download_content(url, path, timeout) diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index 69cecc58e2..202d4a2bfb 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -2,21 +2,31 @@ from collections.abc import Iterable from contextlib import ExitStack +import hashlib import io +import json import logging import os from pathlib import Path import subprocess import sys import time -from typing import IO +from typing import IO, TYPE_CHECKING from esphome.helpers import ProgressBar, rmtree +if TYPE_CHECKING: + import requests + PathType = str | os.PathLike _LOGGER = logging.getLogger(__name__) +# Attempts per mirror URL before falling through to the next mirror; only +# mid-stream drops retry (resuming when the server gave a validator), +# connect errors move on immediately. +_MIRROR_ATTEMPTS = 3 + def get_project_link_flags() -> list[str]: """Return the sorted -Wl, linker flags from the current build.""" @@ -37,6 +47,13 @@ def get_project_compile_flags() -> list[str]: ] +def get_project_cxx_compile_flags() -> list[str]: + """Return the sorted flags that apply to C++ compiles only.""" + from esphome.core import CORE # local import to avoid circular dependency + + return sorted(CORE.cxx_build_flags) + + def str_to_lst_of_str(a: str | list[str]) -> list[str]: """ Convert a string to a list of string @@ -387,17 +404,23 @@ def _zip_extract_all( progress.update(1) -def _rename_with_retry(src: Path, dst: Path, attempts: int = 5) -> None: +def _rename_with_retry( + src: Path, dst: Path, attempts: int = 5, overwrite: bool = False +) -> None: """Rename ``src`` to ``dst`` with backoff retries on Windows sharing violations. Antivirus/indexer handles on freshly-written files can briefly block ``os.rename`` with ERROR_SHARING_VIOLATION / ERROR_ACCESS_DENIED. The handle is released within tens of ms in practice, so exponential backoff - works. + works. With ``overwrite`` an existing ``dst`` is replaced instead of + failing. """ for i in range(attempts): try: - src.rename(dst) + if overwrite: + src.replace(dst) + else: + src.rename(dst) return except PermissionError: if i == attempts - 1: @@ -518,8 +541,8 @@ def archive_extract_all( ValueError: If archive format is unsupported """ - # 1. Handle different archive input types with ExitStack() as stack: + # 1. Handle different archive input types archive_ref: io.BufferedIOBase if isinstance(archive, (str, os.PathLike)): archive_ref = stack.enter_context(Path(archive).open("rb")) @@ -545,6 +568,322 @@ def archive_extract_all( matched_fct(archive_ref, extract_dir, progress_header=progress_header) +def _open_ranged( + url: str, offset: int, timeout: int, validator: str | None = None +) -> tuple["requests.Response | None", int]: + """Open a streaming GET, asking the server to resume at ``offset``. + + ``validator`` is an ETag or Last-Modified value from the interrupted + response; it is sent as ``If-Range`` so the server only honors the Range + when the content is unchanged, replying 200 (full body, restart) if the + file was replaced between requests — the resumed bytes can then never be + stitched onto a different file's prefix. + + Returns ``(response, effective_offset)``. The response is None when the + server answered 416 Range Not Satisfiable: the file holds every byte the + server has (a previous attempt was interrupted after the last byte), so + there is nothing to stream and the caller's verification decides whether + the file is good. The offset drops to 0 when the server ignored the + ``Range`` header (no 206), meaning the caller must restart the file. + Raises on connect errors and HTTP error statuses; the response is closed + on failure. + """ + import requests + + headers = {"Range": f"bytes={offset}-"} if offset else {} + if offset and validator: + headers["If-Range"] = validator + resp = requests.get(url, stream=True, timeout=timeout, headers=headers) + if offset and resp.status_code == 416: + resp.close() + return None, offset + if offset and resp.status_code != 206: + _LOGGER.debug( + "Server did not resume %s (HTTP %s), restarting", url, resp.status_code + ) + offset = 0 + if not resp.ok: + resp.close() + resp.raise_for_status() + if offset: + _LOGGER.info("Resuming download at %d bytes ...", offset) + return resp, offset + + +def _verify_file(path: Path, sha256: str | None, size: int | None) -> None: + """Raise EsphomeError when ``path`` fails an available sha256/size check.""" + from esphome.core import EsphomeError + + if size is not None and path.stat().st_size != size: + raise EsphomeError(f"size mismatch: expected {size}, got {path.stat().st_size}") + if sha256 is not None: + with path.open("rb") as f: + digest = hashlib.file_digest(f, "sha256").hexdigest() + if digest != sha256: + raise EsphomeError(f"sha256 mismatch: got {digest}") + + +def _load_download_meta(meta: Path, url: str) -> tuple[str | None, int]: + """Return the ``(validator, total)`` a previous run recorded for ``url``. + + ``(None, 0)`` when there is no sidecar, it is unreadable, or it belongs + to a different URL (e.g. a different mirror was tried last time). + """ + try: + with meta.open(encoding="utf-8") as f: + data = json.load(f) + except (OSError, json.JSONDecodeError): + return None, 0 + if not isinstance(data, dict) or data.get("url") != url: + return None, 0 + validator = data.get("validator") + total = data.get("total") + return ( + validator if isinstance(validator, str) else None, + total if isinstance(total, int) else 0, + ) + + +def _write_download_meta( + meta: Path, url: str, validator: str | None, total: int +) -> None: + """Persist resume metadata next to the part file; best-effort. + + Without a validator there is nothing a later run could resume against, + so any stale sidecar is removed instead. + """ + try: + if validator is None: + meta.unlink(missing_ok=True) + else: + meta.write_text( + json.dumps({"url": url, "validator": validator, "total": total}), + encoding="utf-8", + ) + except OSError as e: + _LOGGER.debug("Could not update download metadata %s: %s", meta, e) + + +def _content_length(resp: "requests.Response") -> int: + """Return the response's Content-Length, or 0 when absent or malformed. + + 0 means "unknown", which downstream disables the progress bar and the + resume/completeness logic — a garbage header from a broken proxy must + degrade to a plain single-stream download, not crash the attempt. + """ + try: + return int(resp.headers.get("content-length", 0)) + except ValueError: + return 0 + + +def _response_validator(resp: "requests.Response") -> str | None: + """Return the response's strong validator for ``If-Range`` resumes. + + Weak ETags (``W/...``) are not usable for byte-range conditionals, so + fall back to Last-Modified, or None when the server offers neither. + """ + etag = resp.headers.get("ETag") + if etag and not etag.startswith("W/"): + return etag + return resp.headers.get("Last-Modified") + + +def _stream_response_to_file( + resp: "requests.Response", f: IO[bytes], offset: int, size: int | None = None +) -> None: + """Stream an open ``_open_ranged`` response body into ``f`` at ``offset``. + + Truncates ``f`` to ``offset`` first, so a server-rejected resume + (effective offset 0) discards the stale bytes. ``offset`` also seeds the + progress bar so a resumed download shows overall progress. ``size`` is + the known full file size; when None it is derived from the response's + content-length, and without either there is no progress bar. + """ + f.seek(offset) + f.truncate(offset) + total_size = size or offset + _content_length(resp) + downloaded = offset + progress = ProgressBar("Downloading") if total_size > 0 else None + for chunk in resp.iter_content(chunk_size=256 * 1024): + if chunk: + f.write(chunk) + downloaded += len(chunk) + if progress is not None: + progress.update(downloaded / total_size) + if progress is not None: + progress.update(1) + + +def download_with_resume( + url: str, + dest: PathType, + sha256: str | None = None, + size: int | None = None, + # More attempts than _MIRROR_ATTEMPTS: a single-URL download has no + # mirror fallback, and each retry only re-fetches the remainder. + attempts: int = 5, + timeout: int = 30, + retry_connect_errors: bool = True, +) -> None: + """Download ``url`` to ``dest``, resuming partial downloads. + + The body streams into ``.part``, which persists across attempts and + esphome runs: a mid-stream connection drop only costs one attempt and the + next continues from where it stopped, so an unstable connection converges + on a complete file instead of restarting from zero each retry (#17703). + When ``size`` / ``sha256`` are given the completed file is verified and a + mismatch restarts from scratch; success renames the part file into place. + An already-present ``dest`` that passes verification is kept as-is. + + Resuming a part file from an earlier run needs proof the content is + unchanged: ``sha256`` when the caller has one, or otherwise the server's + If-Range validator recorded in a ``.part.meta`` sidecar by the run + that started the download — a size alone cannot detect a same-length + content change on the server. + + With ``retry_connect_errors`` disabled, a failure before any body bytes + flow (connect error, HTTP error status) propagates immediately instead + of consuming attempts — for callers with their own fallback, like + ``download_from_mirrors``. + + Raises EsphomeError when all attempts are exhausted. + """ + # Imported lazily: requests is a heavy import (~85ms) and is only needed + # when actually downloading a toolchain, never during config validation. + import requests + + from esphome.core import EsphomeError + + dest = Path(dest) + part = dest.with_name(dest.name + ".part") + meta = part.with_name(part.name + ".meta") + dest.parent.mkdir(parents=True, exist_ok=True) + last_error: Exception | None = None + + # An earlier run already completed this download. Only trust it when + # there is something to verify it against; without sha/size the remote + # content may have changed (e.g. a refreshed constraints file), so + # re-download and atomically replace it. + if dest.is_file() and (sha256 is not None or size is not None): + try: + _verify_file(dest, sha256, size) + return + except EsphomeError: + dest.unlink() + + # Adopt the validator/total the run that started this part file recorded, + # so an unfinished download resumes across runs even without a sha256. + validator, expected_total = _load_download_meta(meta, url) + + for _ in range(attempts): + streamed = False + try: + offset = part.stat().st_size if part.is_file() else 0 + # A stitched resume needs two proofs: content identity (the + # bytes being appended belong to the same file as the prefix) + # and completeness. sha256 provides both, across runs. Without + # it, identity needs this run's If-Range validator — a size + # alone cannot detect a same-length content change, so a + # leftover part file from an earlier run must restart — and + # completeness needs a known total length. + if ( + offset + and sha256 is None + and (validator is None or not (size or expected_total)) + ): + _LOGGER.debug( + "Restarting %s from zero: cannot prove a resumed " + "file correct (no sha256, validator=%s, total=%s)", + url, + validator is not None, + size or expected_total, + ) + offset = 0 + if size is None or offset < size: + resp, offset = _open_ranged(url, offset, timeout, validator) + # A None response means HTTP 416: the part file already holds + # every byte the server has; fall through to verification. + if resp is not None: + with resp, part.open("ab") as f: + streamed = True + if offset == 0: + validator = _response_validator(resp) + expected_total = _content_length(resp) + # Recorded so a later run can prove an If-Range + # resume of this part file safe. + _write_download_meta(meta, url, validator, expected_total) + _stream_response_to_file(resp, f, offset, size) + # else: a previous run already wrote every byte (or more) but + # was killed before the rename below. Skip the network entirely + # — a Range request past EOF would draw HTTP 416 — and let + # verification decide whether to promote the file or discard it + # and start over. + + expected_size = size if size is not None else expected_total + _verify_file(part, sha256, expected_size or None) + if not expected_size and sha256 is None: + # No sha, no size, and the server sent no usable + # content-length: nothing can prove the download complete + # (urllib3 still errors on most short bodies, but not on a + # cleanly closed chunked stream). Promote with a debug + # note rather than fail or warn: some servers (e.g. the + # Espressif constraints host) never send a length, the user + # can do nothing about it, and every current caller + # extracts or parses the file afterwards, where corruption + # fails loudly. + _LOGGER.debug( + "Downloaded %s without any way to verify completeness", + dest.name, + ) + # Retry on Windows sharing violations: an antivirus handle on the + # freshly-written file must not get the verified download deleted + # as corrupt by the except clause below. If even the backoff + # retries fail, keep the verified part so the next attempt (or + # run) only has to redo the rename, not the download. + try: + _rename_with_retry(part, dest, overwrite=True) + except PermissionError as e: + _LOGGER.debug("Could not move %s into place: %s", part, e) + last_error = e + continue + meta.unlink(missing_ok=True) + return + except requests.RequestException as e: + # Network failures — including connect errors, since a single + # URL has no mirror-list fallback — keep the part file for the + # next attempt (or the next esphome run) to resume from. Checked + # before OSError: RequestException subclasses IOError. + if not retry_connect_errors and not streamed: + # The caller falls back to another URL on pre-body failures. + raise + _LOGGER.debug("Download of %s interrupted: %s", url, e) + last_error = e + except (OSError, EsphomeError) as e: + # A completed-but-corrupt file (or local disk error) can't be + # trusted for resume; start over. + _LOGGER.debug("Discarding %s: %s", part, e) + part.unlink(missing_ok=True) + meta.unlink(missing_ok=True) + last_error = e + + raise EsphomeError( + f"Failed to download {url} after {attempts} attempts: " + f"{_failure_reason(last_error)}" + ) from last_error + + +def _failure_reason(e: Exception) -> str: + """Format a download exception for the aggregated error message. + + ``requests`` appends " for url: " to HTTP errors; the URL is already + printed on the line above, so strip the suffix to keep lines short. Falls + back to the repr for exceptions with no message (e.g. ``TimeoutError()``) + so the line always names the failure. + """ + return str(e).split(" for url: ", maxsplit=1)[0] or repr(e) + + def download_from_mirrors( mirrors: list[str], substitutions: dict[str, str], @@ -563,70 +902,170 @@ def download_from_mirrors( Returns: The source URL. + Mirror URL templates that reference a substitution not present in + ``substitutions`` are skipped, so callers can offer templates that only + apply to some downloads. + + A path target downloads through ``download_with_resume``, so an + interrupted download resumes on the next esphome run; a file-like target + only resumes mid-stream drops within this call. + Raises: ValueError: If mirrors list is empty. - Exception: If all download attempts fail. + EsphomeError: If all download attempts fail; the message lists every + attempted URL with its individual failure reason. Also raised if + no template matched the provided substitutions. """ - # Imported lazily: requests is a heavy import (~85ms) and is only needed - # when actually downloading a toolchain, never during config validation. + # Imported lazily: requests is a heavy import (~85ms) and is only + # needed when actually downloading, never during config validation. import requests - # 1. Open target file for writing if path given - with ExitStack() as stack: - if isinstance(target, (str, os.PathLike)): - f = stack.enter_context(Path(target).open("wb")) - elif isinstance(target, (io.RawIOBase, io.IOBase)): - f = target - else: - raise TypeError( - f"target must be str, Path, or file-like object: {type(target)}" - ) + from esphome.core import EsphomeError - # 2. Try each mirror in order - last_exception = None + # 1. Classify the target: filesystem path or open file object + path_target: Path | None = None + f: IO[bytes] | None = None + if isinstance(target, (str, os.PathLike)): + path_target = Path(target) + elif isinstance(target, (io.RawIOBase, io.IOBase)): + f = target + else: + raise TypeError( + f"target must be str, Path, or file-like object: {type(target)}" + ) - for mirror in mirrors: - # 3. Apply substitutions to URL + # 2. Try each mirror in order + failures: list[tuple[str, Exception]] = [] + skipped: list[tuple[str, str]] = [] + + for mirror in mirrors: + # 3. Apply substitutions to URL + try: url = mirror.format(**substitutions) + except KeyError as e: + # The template references a substitution not provided for + # this download (e.g. SHORT_VERSION only exists for x.y.0 + # versions) - expected, the template just doesn't apply. + _LOGGER.debug("Skipping mirror %s: %s not available", mirror, e) + skipped.append((mirror, f"not applicable ({e.args[0]} not available)")) + continue + except (IndexError, ValueError) as e: + # A malformed template (unbalanced braces, bad format spec) + # is an authoring error, not an expected fallthrough - warn + # even if a later mirror succeeds. + _LOGGER.warning("Skipping malformed mirror URL template %s: %r", mirror, e) + skipped.append((mirror, f"skipped ({e!r})")) + continue - _LOGGER.debug("Trying downloading from %s", url) + _LOGGER.debug("Trying to download from %s", url) + + # Path targets delegate to download_with_resume so a partial + # download persists (and resumes) across esphome runs. + if path_target is not None: + try: + download_with_resume( + url, + path_target, + attempts=_MIRROR_ATTEMPTS, + timeout=timeout, + # Pre-body failures (connect/HTTP errors) fall to the + # next mirror immediately; only mid-stream drops + # retry-with-resume on the same URL. + retry_connect_errors=False, + ) + return url + except (requests.RequestException, OSError, EsphomeError) as e: + # Everything download_with_resume classifies as a download + # failure; programming errors propagate. + _LOGGER.debug("Failed to download %s: %s", url, str(e)) + failures.append((url, e)) + continue + + # 4. Download; mid-stream failures retry the same mirror with + # resume (see download_with_resume) instead of starting over. + # There is no checksum to verify a resumed file against, so a + # stitch is only trusted when the server proves consistency: the + # If-Range validator guarantees 206 only for unchanged content, + # and the expected total length (when the first response carried + # one) guards against short or shifted bodies. Without a + # validator the retry restarts from zero. + offset = 0 + expected_total = 0 + validator = None + for attempt in range(_MIRROR_ATTEMPTS): + try: + resp, offset = _open_ranged(url, offset, timeout, validator) + except (requests.RequestException, OSError) as e: + # Connect/HTTP error, no bytes flowed — next mirror. + _LOGGER.debug("Failed to download %s: %s", url, str(e)) + failures.append((url, e)) + break try: - # 4. Reset file pointer and download - f.seek(0) - f.truncate(0) + # A None response means HTTP 416: the file already holds + # every byte the server has (a drop after the last byte); + # only the length check below remains. + if resp is not None: + with resp: + if offset == 0: + validator = _response_validator(resp) + expected_total = _content_length(resp) + _stream_response_to_file(resp, f, offset) - with requests.get(url, stream=True, timeout=timeout) as r: - r.raise_for_status() - - total_size = int(r.headers.get("content-length", 0)) - downloaded = 0 - - progress = ProgressBar("Downloading") if total_size > 0 else None - - for chunk in r.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - - downloaded += len(chunk) - - if progress is not None: - progress.update(downloaded / total_size) - - if progress is not None: - progress.update(1) + if expected_total and f.tell() != expected_total: + raise EsphomeError( + f"size mismatch: expected {expected_total}, got {f.tell()}" + ) + if not expected_total: + # Same trust decision as download_with_resume's + # unverifiable promotion; surface it at the same level. + _LOGGER.debug( + "Downloaded %s without any way to verify completeness", + url, + ) _LOGGER.debug("Downloaded successfully from: %s", url) - # 6. Reset file pointer and return + # 5. Reset file pointer and return f.seek(0) return url - except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught + except (requests.RequestException, OSError, EsphomeError) as e: + # Mid-stream drop: keep the received bytes and retry this + # mirror from the current position — but only when the + # server gave a validator to resume against safely AND a + # total length to prove the stitched file complete (the + # length check above is the only verification here). _LOGGER.debug("Failed to download %s: %s", url, str(e)) - last_exception = e + if validator and expected_total: + offset = f.tell() + else: + _LOGGER.debug( + "Restarting %s from zero: cannot prove a " + "resumed file complete (validator=%s, total=%s)", + url, + validator is not None, + expected_total, + ) + offset = 0 + if attempt == _MIRROR_ATTEMPTS - 1: + failures.append((url, e)) - # 7. Raise last exception if all mirrors failed - if last_exception: - raise last_exception - raise ValueError("download_from_mirrors called with an empty mirrors list") + # 6. Report every attempted URL if all mirrors failed. Falling back + # past an early mirror is normal (e.g. only one of the framework URL + # templates matches a given version's tag), so raising only the last + # error would hide the failure that actually matters. + if failures: + attempts = "".join( + f"\n {url}\n {_failure_reason(e)}" for url, e in failures + ) + attempts += "".join(f"\n {mirror}\n {reason}" for mirror, reason in skipped) + raise EsphomeError( + f"Failed to download from all mirrors:{attempts}" + ) from failures[0][1] + if skipped: + details = "".join(f"\n {mirror}\n {reason}" for mirror, reason in skipped) + raise EsphomeError( + f"No mirror URL template matched the provided substitutions:{details}" + ) + raise ValueError("download_from_mirrors called with an empty mirrors list") diff --git a/esphome/git.py b/esphome/git.py index c4a612753b..d1dca3b3ae 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -1,23 +1,82 @@ -from collections.abc import Callable +from collections.abc import Callable, Iterator +from contextlib import contextmanager from dataclasses import dataclass +from enum import Enum, auto +import errno import hashlib import logging +import os from pathlib import Path import re import subprocess import sys import time +from typing import TYPE_CHECKING import urllib.parse import esphome.config_validation as cv -from esphome.core import CORE, TimePeriodSeconds -from esphome.helpers import rmtree +from esphome.core import CORE, EsphomeError, TimePeriodSeconds +from esphome.helpers import ( + add_git_ceiling_directory, + format_duration, + rmtree, + write_file, +) + +if TYPE_CHECKING: + from filelock import FileLock _LOGGER = logging.getLogger(__name__) # Special value to indicate never refresh NEVER_REFRESH = TimePeriodSeconds(seconds=-1) +# `refresh: never` validates to a huge interval rather than the NEVER_REFRESH +# sentinel; treat any interval at least that long as refresh disabled instead +# of logging a countdown of hundreds of years +_REFRESH_DISABLED_SECONDS = cv.source_refresh(cv.SOURCE_REFRESH_NEVER).total_seconds + +# revert() runs on an already-failing path; bound its wait for the cache +# entry lock so that recovery cannot hang forever behind another process. +_REVERT_LOCK_TIMEOUT_SECONDS = 60 + +# When a complete cache entry already exists, a caller does not wait forever +# behind another process's stalled clone or update (git sets no network +# timeouts): after this bound it uses the existing clone without refreshing +# it. With no complete entry there is nothing to fall back to, so the wait +# is unbounded. +_COMPLETE_ENTRY_LOCK_TIMEOUT_SECONDS = 60 + +# Written inside .git only while the entry is a complete, quiescent +# checkout: after every clone step (clone, ref fetch, reset, submodule init) +# has finished, and removed for the duration of a refresh's rewrite +# (stash/fetch/reset). A directory without it is an interrupted clone or +# update (e.g. the process was killed mid-clone) and must be re-cloned; +# without this check such a directory would be trusted forever when the +# caller uses NEVER_REFRESH, and the bounded-wait fallback would hand a +# mid-rewrite tree to a timed-out peer. Lives in .git so +# stash/reset/checkout can never touch it and it does not pollute the +# worktree. +_CLONE_COMPLETE_MARKER = "esphome_clone_complete" + +# Environment variables that scope git to a specific repository. Git hooks and +# some CI wrappers export these; if they leak into the git commands run here, +# git binds to the caller's repository instead of the one being managed. The +# effects range from loud (`git clone` producing a bare-style directory with +# no working tree) to silent (an ambient GIT_INDEX_FILE makes +# `git submodule update --init` exit 0 without initializing anything). +_GIT_REPO_SCOPING_ENV = frozenset( + { + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_INDEX_FILE", + "GIT_OBJECT_DIRECTORY", + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_COMMON_DIR", + "GIT_NAMESPACE", + } +) + class GitException(cv.Invalid): """Base exception for git-related errors.""" @@ -35,32 +94,61 @@ class GitRepositoryError(GitException): """Exception raised when a git repository is in an invalid state.""" -def run_git_command(cmd: list[str], git_dir: Path | None = None) -> str: - if git_dir is not None: - _LOGGER.debug( - "Running git command with repository isolation: %s (git_dir=%s)", - " ".join(cmd), - git_dir, - ) - else: - _LOGGER.debug("Running git command: %s", " ".join(cmd)) +def _redact_url_credentials(text: str) -> str: + """Mask userinfo in any URLs embedded in ``text``. - # Set up environment for repository isolation if git_dir is provided - # Force git to only operate on this specific repository by setting - # GIT_DIR and GIT_WORK_TREE. This prevents git from walking up the - # directory tree to find parent repositories when the target repo's - # .git directory is corrupt. Without this, commands like 'git stash' - # could accidentally operate on parent repositories (e.g., the main - # ESPHome repo) instead of failing, causing data loss. - env: dict[str, str] | None = None - cwd: str | None = None + Users can put credentials directly in a git URL, and log output is + routinely pasted into public issues. + """ + return re.sub(r"://[^/@\s]+@", "://***@", text) + + +def run_git_command( + cmd: list[str], git_dir: Path | None = None, *, cwd: Path | None = None +) -> str: + """Run a git command and return its stdout. + + The repository-scoping environment variables in ``_GIT_REPO_SCOPING_ENV`` + are always stripped. ``git_dir`` additionally pins GIT_DIR/GIT_WORK_TREE + to that repository and runs the command there; ``cwd`` alone runs the + command in that directory with GIT_CEILING_DIRECTORIES capping repository + discovery at its parent. + """ + # Every invocation starts from an environment with the repository-scoping + # variables stripped (see _GIT_REPO_SCOPING_ENV) so a git hook or CI + # wrapper invoking ESPHome can never redirect these commands to its own + # repository or index. + # + # ``git_dir`` then re-adds GIT_DIR and GIT_WORK_TREE pointing at the + # managed repository. This prevents git from walking up the directory + # tree to find parent repositories when the target repo's .git directory + # is corrupt. Without this, commands like 'git stash' could accidentally + # operate on parent repositories (e.g., the main ESPHome repo) instead of + # failing, causing data loss. + # + # ``cwd`` (without ``git_dir``) runs the command in that directory + # without GIT_DIR/GIT_WORK_TREE. The ``git submodule`` porcelain needs + # this: on some installations (e.g. Windows setups where a shim hands + # git untranslated paths) it refuses to run when GIT_DIR/GIT_WORK_TREE + # are set, failing with "cannot be used without a working tree". + # GIT_CEILING_DIRECTORIES (which git only honors as an absolute path) + # keeps the parent-repo-walk protection instead: if the repo's .git is + # missing or corrupt, git fails rather than discovering an enclosing + # repository. + env = {k: v for k, v in os.environ.items() if k not in _GIT_REPO_SCOPING_ENV} if git_dir is not None: - env = { - **subprocess.os.environ, - "GIT_DIR": str(Path(git_dir) / ".git"), - "GIT_WORK_TREE": str(git_dir), - } - cwd = str(git_dir) + env["GIT_DIR"] = str(Path(git_dir) / ".git") + env["GIT_WORK_TREE"] = str(git_dir) + cwd = git_dir + elif cwd is not None: + add_git_ceiling_directory(env, Path(cwd).absolute().parent) + + _LOGGER.debug( + "Running git command: %s (cwd=%s, isolated=%s)", + _redact_url_credentials(" ".join(cmd)), + cwd, + git_dir is not None, + ) try: ret = subprocess.run( @@ -78,16 +166,31 @@ def run_git_command(cmd: list[str], git_dir: Path | None = None) -> str: "for installation instructions." ) from err - if ret.returncode != 0 and ret.stderr: - err_str = ret.stderr.decode("utf-8") - lines = [x.strip() for x in err_str.splitlines()] - if lines[-1].startswith("fatal:"): - raise GitCommandError(lines[-1][len("fatal: ") :]) - raise GitCommandError(err_str) + if ret.returncode != 0: + if ret.stderr: + err_str = ret.stderr.decode("utf-8") + lines = [x.strip() for x in err_str.splitlines()] + if lines[-1].startswith("fatal:"): + raise GitCommandError(lines[-1][len("fatal: ") :]) + raise GitCommandError(err_str) + raise GitCommandError( + f"git exited with code {ret.returncode}: " + f"{_redact_url_credentials(' '.join(cmd))}" + ) return ret.stdout.decode("utf-8").strip() +def _cache_key(url: str, ref: str | None) -> str: + """Cache key identifying one repository checkout. + + The lock path and the entry directory both hash this, keeping them in + agreement. (micro_wake_word still rebuilds the format by hand to locate + manifests; fold it in here if the format ever changes.) + """ + return f"{url}@{ref}" + + def _compute_destination_path(key: str, domain: str) -> Path: base_dir = Path(CORE.data_dir) / domain h = hashlib.new("sha256") @@ -95,6 +198,220 @@ def _compute_destination_path(key: str, domain: str) -> Path: return base_dir / h.hexdigest()[:8] +def _repo_entry_dir(key: str, domain: str, subpath: Path | None) -> Path: + """Worktree directory of one cache entry: the hash dir plus optional subpath.""" + repo_dir = _compute_destination_path(key, domain) + if subpath: + repo_dir = repo_dir / subpath + return repo_dir + + +def _repo_lock_path(key: str, domain: str) -> Path: + """Path of the lock file serializing all work on one cache entry. + + Lives next to the hash directory, never inside it, so the removal of a + broken or incomplete clone can never delete a lock another process holds. + """ + repo_dir = _compute_destination_path(key, domain) + return repo_dir.parent / f"{repo_dir.name}.lock" + + +class _LockStatus(Enum): + ACQUIRED = auto() + # A bounded wait expired while another process held the lock. + TIMEOUT = auto() + # The lock could not be taken at all; callers proceed unlocked, + # matching the behavior before the lock existed. + UNAVAILABLE = auto() + + +# Errnos that mean the filesystem genuinely cannot take file locks (NFS +# without a lock daemon, some FUSE mounts). Any other OSError (permissions, +# read-only volume, full disk) is a cache directory problem, which the git +# commands themselves report clearly when it actually matters. EPERM is +# deliberately absent: it usually means a permissions problem, so it takes +# the generic message that names no cause. On Linux ENOTSUP and EOPNOTSUPP +# are the same value; the set folds them. +_NO_LOCK_SUPPORT_ERRNOS = frozenset( + {errno.ENOLCK, errno.ENOSYS, errno.EOPNOTSUPP, errno.ENOTSUP} +) + + +def _acquire_repo_lock( + lock: "FileLock", + safe_key: str, + timeout: float, + wait_message: str = "Waiting for another process to finish updating %s", +) -> _LockStatus: + """Acquire ``lock``, logging ``wait_message`` when a wait actually begins. + + ``timeout`` of -1 waits forever; a positive value bounds the wait and + can yield ``TIMEOUT``. + """ + from filelock import Timeout + + try: + try: + lock.acquire(blocking=False) + except Timeout: + # Waiting on another process's clone or update can take + # minutes; say so instead of appearing hung. + _LOGGER.info(wait_message, safe_key) + lock.acquire(timeout=timeout) + except Timeout: + return _LockStatus.TIMEOUT + except OSError as err: + if err.errno in _NO_LOCK_SUPPORT_ERRNOS: + _LOGGER.warning( + "The filesystem does not support locking the cache entry for " + "%s (%s), continuing without a lock", + safe_key, + err, + ) + else: + # Not a locking problem (permissions, read-only volume, full + # disk). Still continue unlocked: a pre-seeded read-only cache + # with refresh disabled only reads and must keep working, and + # in every other case the git commands fail with the real error. + _LOGGER.warning( + "Could not take the cache entry lock for %s (%s), " + "continuing without a lock", + safe_key, + err, + ) + return _LockStatus.UNAVAILABLE + return _LockStatus.ACQUIRED + + +@contextmanager +def _repo_cache_lock( + key: str, domain: str, repo_dir: Path +) -> Iterator[tuple[bool, "FileLock | None"]]: + """Hold the cache entry lock for ``key`` over the with block. + + Yields ``(use_existing, lock)``. ``use_existing`` is True when the lock + could not be acquired within the bounded wait but ``repo_dir`` is a + complete cache entry; the caller should use it as-is and do nothing + else. Otherwise ``lock`` is the held lock, released when the block + exits, or ``None`` when the lock could not be taken at all and the + caller proceeds unlocked. + """ + # Lazy import: keeps filelock off the CLI startup import path. + from filelock import FileLock + + safe_key = _redact_url_credentials(key) + # acquire() creates the lock file's directory itself; git clone later + # creates the hash directory next to it. fallback_to_soft would silently + # downgrade ENOSYS to a SoftFileLock, whose stale existence marker from + # another host on a shared cache could hang the unbounded wait forever; + # routing it through the OSError handler runs unlocked instead. + lock: FileLock | None = FileLock( + str(_repo_lock_path(key, domain)), fallback_to_soft=False + ) + status = _acquire_repo_lock(lock, safe_key, _COMPLETE_ENTRY_LOCK_TIMEOUT_SECONDS) + if status is _LockStatus.TIMEOUT: + if _clone_complete_marker_path(repo_dir).is_file(): + # Mutual exclusion matters most while no complete entry exists + # (initial clone, recovery re-clone); with one on disk, reading + # it beats hanging behind a stalled holder. + _LOGGER.warning( + "Timed out waiting for another process updating %s, proceeding " + "with the existing clone, which that process may still be " + "changing", + safe_key, + ) + yield True, None + return + # Nothing to fall back to; the holder is producing the clone this + # caller needs. + status = _acquire_repo_lock( + lock, + safe_key, + timeout=-1, + wait_message="Still waiting for the clone of %s, " + "there is no existing clone to fall back on", + ) + if status is not _LockStatus.ACQUIRED: + lock = None + try: + yield False, lock + finally: + if lock is not None: + lock.release() + + +def _clone_complete_marker_path(repo_dir: Path) -> Path: + return repo_dir / ".git" / _CLONE_COMPLETE_MARKER + + +def _clear_clone_complete_marker(repo_dir: Path) -> None: + """Best-effort removal of the completion marker. + + If the unlink fails (e.g. a file lock on Windows), the marker stays and + the entry keeps its previous trust level; every consumer of the marker + tolerates that. + """ + try: + _clone_complete_marker_path(repo_dir).unlink(missing_ok=True) + except OSError as err: + _LOGGER.debug("Could not delete clone completion marker: %s", err) + + +def _write_clone_complete_marker( + repo_dir: Path, key: str, hash_dir_name: str, safe_key: str +) -> None: + """Mark the entry as a complete, quiescent checkout. + + The key and hash dir name are recorded purely to make cache debugging + easier. The marker is only a validity signal, so a failed write must not + fail an otherwise complete clone or update: the only cost is a re-clone + on the next run. + """ + try: + write_file( + _clone_complete_marker_path(repo_dir), + f"key={key}\nhash={hash_dir_name}\n", + ) + except EsphomeError as err: + _LOGGER.warning( + "Could not write clone completion marker for %s: %s", safe_key, err + ) + + +def _remove_repo_dir(repo_dir: Path) -> None: + """Remove a repo directory, deleting the completion marker first. + + Marker-first ordering guarantees an interrupted removal can never leave a + marker behind next to a partially deleted worktree. The unlink is best + effort: if it fails, rmtree below still gets the chance to remove the + directory, marker included. + """ + _clear_clone_complete_marker(repo_dir) + if repo_dir.is_dir(): + rmtree(repo_dir) + + +def update_submodules(repo_dir: Path, key: str) -> None: + """Initialize/update every submodule the repository declares, recursively, + matching how PlatformIO clones libraries. + + Most repositories declare no submodules, so this does nothing when there + is no ``.gitmodules`` file. Which submodules get populated is git's own + policy (``update = none``, ``submodule.active``, sparse checkouts); + git's exit code is the error signal. + + Runs with plain ``cwd`` rather than ``git_dir`` isolation, which the + ``git submodule`` porcelain does not tolerate (see ``run_git_command``). + """ + if not (repo_dir / ".gitmodules").is_file(): + return + _LOGGER.info("Updating submodules for %s", _redact_url_credentials(key)) + run_git_command( + ["git", "submodule", "update", "--init", "--recursive", "--depth=1"], + cwd=repo_dir, + ) + + def resolve_symlink_stub(repo_dir: Path, file_path: Path) -> Path | None: """Return the symlink target if ``file_path`` is a Windows-checked-out symlink stub. @@ -184,28 +501,106 @@ def resolve_symlink_stub(repo_dir: Path, file_path: Path) -> Path | None: def clone_or_update( *, url: str, - ref: str = None, + ref: str | None = None, refresh: TimePeriodSeconds | None, domain: str, - username: str = None, - password: str = None, - submodules: list[str] | None = None, + username: str | None = None, + password: str | None = None, + init_submodules: bool = False, subpath: Path | None = None, - _recover_broken: bool = True, -) -> tuple[Path, Callable[[], None] | None]: - key = f"{url}@{ref}" +) -> tuple[Path, Callable[[], bool] | None]: + """Clone a repository into the cache, or refresh an existing clone. + All work runs under a per-cache-entry inter-process file lock, so + concurrent resolutions of the same repository (two esphome processes, or + a subprocess plus an in-process load) serialize instead of interleaving. + Without the lock, ``repo_dir.is_dir()`` is true from the instant + ``git clone`` creates the directory: a second caller could read a half + populated worktree, or see the missing completion marker and delete the + clone in progress out from under the first caller. + + The lock guards mutation of the cache entry only; it is released when + this function returns, so a caller still reading the worktree can + overlap a later refresh by another process. That residual window is + narrow (the refresh interval is re-checked under the lock) and predates + the lock. + + Locking is best effort: on a filesystem that cannot take file locks a + warning is logged and the work proceeds unlocked, matching the behavior + before the lock existed. A complete cache entry also caps the wait: if + the holder is still busy after a bounded time (e.g. stalled on the + network), the existing clone is used without refreshing it, so a stuck + process cannot hang every peer that already has a good entry. + """ + key = _cache_key(url, ref) + repo_dir = _repo_entry_dir(key, domain, subpath) + with _repo_cache_lock(key, domain, repo_dir) as (use_existing, lock): + if use_existing: + return repo_dir, None + return _clone_or_update_locked( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + username=username, + password=password, + init_submodules=init_submodules, + subpath=subpath, + lock=lock, + ) + + +def _clone_or_update_locked( + *, + url: str, + ref: str | None, + refresh: TimePeriodSeconds | None, + domain: str, + username: str | None, + password: str | None, + init_submodules: bool, + subpath: Path | None, + lock: "FileLock | None", + _recover_broken: bool = True, +) -> tuple[Path, Callable[[], bool] | None]: + """Body of ``clone_or_update``; the caller holds ``lock``. + + Split out because the broken-repository recovery below re-enters this + function: re-acquiring the already-held lock would deadlock, since OS + file locks taken on separate file descriptors conflict even within one + process. ``lock`` is only re-acquired by the returned ``revert`` + callback, which runs after the wrapper's ``finally`` has released it. + ``lock`` is ``None`` when the filesystem cannot take file locks and the + wrapper fell back to running unlocked. + """ + key = _cache_key(url, ref) + # The user may have embedded credentials in the URL itself; log this + # instead of key. + safe_key = _redact_url_credentials(key) + + # Keep the caller's URL for the recovery re-clone below: rewriting the + # rewritten URL would double the userinfo, and the recursive call must + # compute the same cache key as this one. + original_url = url if username is not None and password is not None: url = url.replace( "://", f"://{urllib.parse.quote(username)}:{urllib.parse.quote(password)}@" ) - repo_dir = _compute_destination_path(key, domain) - if subpath: - repo_dir = repo_dir / subpath + hash_dir_name = _compute_destination_path(key, domain).name + repo_dir = _repo_entry_dir(key, domain, subpath) + + if repo_dir.is_dir() and not _clone_complete_marker_path(repo_dir).is_file(): + # The last clone never finished (killed process, container stop) or + # predates the marker; either way it cannot be trusted, especially + # with NEVER_REFRESH where it would otherwise be reused forever. + _LOGGER.warning( + "Removing incomplete clone of %s at %s, will re-clone", safe_key, repo_dir + ) + _remove_repo_dir(repo_dir) if not repo_dir.is_dir(): - _LOGGER.info("Cloning %s", key) + _LOGGER.info("Cloning %s", safe_key) _LOGGER.debug("Location: %s", repo_dir) try: cmd = ["git", "clone", "--depth=1"] @@ -224,33 +619,35 @@ def clone_or_update( ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir ) - if submodules is not None: - _LOGGER.info( - "Initializing submodules (%s) for %s", ", ".join(submodules), key - ) - run_git_command( - ["git", "submodule", "update", "--init", "--depth=1", "--"] - + submodules, - git_dir=repo_dir, - ) + if init_submodules: + update_submodules(repo_dir, key) + except GitException: # Remove incomplete clone to prevent stale state. Without this, # a failed ref fetch leaves a clone on the default branch, and # subsequent calls skip the update due to the refresh window. - if repo_dir.is_dir(): - rmtree(repo_dir) + _remove_repo_dir(repo_dir) raise + # Every git step succeeded. + _write_clone_complete_marker(repo_dir, key, hash_dir_name, safe_key) + else: if refresh == NEVER_REFRESH or CORE.skip_external_update: - _LOGGER.debug("Skipping update for %s (refresh disabled)", key) + _LOGGER.debug("Skipping update for %s (refresh disabled)", safe_key) return repo_dir, None file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") # On first clone, FETCH_HEAD does not exist if not file_timestamp.exists(): file_timestamp = Path(repo_dir / ".git" / "HEAD") - age_seconds = time.time() - file_timestamp.stat().st_mtime + try: + age_seconds = time.time() - file_timestamp.stat().st_mtime + except OSError: + # A .git with neither FETCH_HEAD nor HEAD is corrupt (e.g. a + # partially deleted clone). Force the update path so the + # broken-repository recovery below removes and re-clones it. + age_seconds = float("inf") if refresh is None or age_seconds > refresh.total_seconds: # Try to update the repository, recovering from broken state if needed old_sha: str | None = None @@ -261,9 +658,16 @@ def clone_or_update( ["git", "rev-parse", "HEAD"], git_dir=repo_dir ) - _LOGGER.info("Updating %s", key) + _LOGGER.info("Updating %s", safe_key) _LOGGER.debug("Location: %s", repo_dir) + # The entry is about to be rewritten; drop the marker so a + # timed-out peer's fallback and the incomplete-entry check + # can tell a quiescent complete entry from one mid-rewrite, + # and so an update interrupted by a crash re-clones instead + # of being trusted. + _clear_clone_complete_marker(repo_dir) + # Stash local changes (if any) # Use git_dir to ensure this only affects the specific repo run_git_command( @@ -287,55 +691,139 @@ def clone_or_update( ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir, ) + + # Inside the try so a submodule failure routes through the + # recovery re-clone below instead of leaving a repo that the + # refresh window would silently accept on the next run. + if init_submodules: + update_submodules(repo_dir, key) + + # Recorded so revert() can tell whether the checkout is + # still the one this update produced. + new_sha = run_git_command( + ["git", "rev-parse", "HEAD"], git_dir=repo_dir + ) + + # The rewrite finished; the entry is trustworthy again. + _write_clone_complete_marker(repo_dir, key, hash_dir_name, safe_key) except GitException as err: # Repository is in a broken state or update failed # Only attempt recovery once to prevent infinite recursion if not _recover_broken: _LOGGER.error( "Repository %s recovery failed, cannot retry (already attempted once)", - key, + safe_key, ) raise _LOGGER.warning( "Repository %s has issues (%s), attempting recovery", - key, + safe_key, err, ) _LOGGER.info("Removing broken repository at %s", repo_dir) - rmtree(repo_dir) + _remove_repo_dir(repo_dir) _LOGGER.info("Successfully removed broken repository, re-cloning...") - # Recursively call clone_or_update to re-clone - # Set _recover_broken=False to prevent infinite recursion - result = clone_or_update( - url=url, + # Re-clone while still holding the lock; going through the + # public wrapper would try to re-acquire it and deadlock. + # Set _recover_broken=False to prevent infinite recursion. + result = _clone_or_update_locked( + url=original_url, ref=ref, refresh=refresh, domain=domain, username=username, password=password, - submodules=submodules, + init_submodules=init_submodules, + subpath=subpath, + lock=lock, _recover_broken=False, ) - _LOGGER.info("Repository %s successfully recovered", key) + _LOGGER.info("Repository %s successfully recovered", safe_key) return result - if submodules is not None: - _LOGGER.info( - "Updating submodules (%s) for %s", ", ".join(submodules), key - ) - run_git_command( - ["git", "submodule", "update", "--init", "--depth=1", "--"] - + submodules, - git_dir=repo_dir, - ) + def revert() -> bool: + """Reset the checkout to the pre-update SHA. - def revert(): - _LOGGER.info("Reverting changes to %s -> %s", key, old_sha) - run_git_command(["git", "reset", "--hard", old_sha], git_dir=repo_dir) + Returns False when the revert did not happen: the cache + entry lock could not be acquired in time, the checkout + moved since this update (another process refreshed it), or + the reset itself failed. A retry cannot reach the + pre-update content then. + """ + if lock is None: + # The wrapper already warned about the unlockable + # filesystem; revert unlocked like everything else. + status = _LockStatus.UNAVAILABLE + else: + status = _acquire_repo_lock( + lock, safe_key, _REVERT_LOCK_TIMEOUT_SECONDS + ) + if status is _LockStatus.TIMEOUT: + # revert() only runs on an already-failing path; skip + # rather than hang so the original error can surface. + _LOGGER.warning( + "Could not lock %s to revert to %s, skipping revert; " + "the cached checkout keeps the un-reverted content " + "until its next refresh", + safe_key, + old_sha, + ) + return False + try: + # Anything can happen between the wrapper releasing the + # lock and revert() re-acquiring it; only undo this + # process's own update, never a peer's newer refresh. + head = run_git_command( + ["git", "rev-parse", "HEAD"], git_dir=repo_dir + ) + if head != new_sha: + _LOGGER.warning( + "Not reverting %s: the checkout moved since this " + "update (another process refreshed it)", + safe_key, + ) + return False + # Announced only once every skip check has passed, so + # the log says exactly one thing per outcome. + _LOGGER.info("Reverting changes to %s -> %s", safe_key, old_sha) + run_git_command( + ["git", "reset", "--hard", old_sha], git_dir=repo_dir + ) + except GitException as err: + # GitException is a cv.Invalid; letting it escape would + # replace the caller's original error with a bare git + # message. Report the failed reset like the skip above, + # and drop the marker: an entry whose reset fails cannot + # be trusted, so the next use re-clones it instead of + # the refresh window silently accepting it. + _LOGGER.warning( + "Could not revert %s to %s (%s), the entry will be " + "re-cloned on next use", + safe_key, + old_sha, + err, + ) + _clear_clone_complete_marker(repo_dir) + return False + finally: + if status is _LockStatus.ACQUIRED: + lock.release() + return True return repo_dir, revert + if refresh.total_seconds >= _REFRESH_DISABLED_SECONDS: + # refresh: never + _LOGGER.debug("Skipping update for %s (refresh disabled)", safe_key) + else: + _LOGGER.info( + "Skipping update for %s, will refresh on the next run after %s " + "(refresh: %s); use refresh: always to update now", + safe_key, + format_duration(refresh.total_seconds - age_seconds), + format_duration(refresh.total_seconds), + ) return repo_dir, None diff --git a/esphome/helpers.py b/esphome/helpers.py index 631bcb6f39..683aaedcf5 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -148,6 +148,21 @@ def indent(text, padding=" "): return "\n".join(indent_list(text, padding)) +def format_duration(seconds: float) -> str: + """Format a duration in seconds as a short string like "1d 2h" or "42s". + + Uses the two largest non-zero units, with unit suffixes matching the YAML + time period shorthand (d, h, min, s). + """ + remainder = max(0, int(seconds)) + parts = [] + for suffix, length in (("d", 86400), ("h", 3600), ("min", 60), ("s", 1)): + value, remainder = divmod(remainder, length) + if value: + parts.append(f"{value}{suffix}") + return " ".join(parts[:2]) if parts else "0s" + + # From https://stackoverflow.com/a/14945195/8924614 def cpp_string_escape(string, encoding="utf-8"): def _should_escape(byte: int) -> bool: diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 7ad41fa978..e88c0649ea 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -18,13 +18,13 @@ dependencies: esphome/micro-wav: version: 0.2.0 espressif/esp-dsp: - version: "1.7.1" + version: "1.8.2" espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: version: 2.1.7 espressif/mdns: - version: 1.11.0 + version: 1.11.3 espressif/esp_wifi_remote: version: 1.5.1 rules: @@ -48,7 +48,7 @@ dependencies: rules: - if: "target in [esp32, esp32p4]" espressif/esp-zigbee-lib: - version: 2.0.2 + version: 2.0.3 rules: - if: "target in [esp32h2, esp32c5, esp32c6]" espressif/lan87xx: @@ -98,7 +98,7 @@ dependencies: esp32async/asynctcp: version: 3.4.91 sendspin/sendspin-cpp: - version: 0.6.1 + version: 0.7.0 lvgl/lvgl: version: 9.5.0 fastled/FastLED: diff --git a/esphome/loader.py b/esphome/loader.py index a9287abf86..22db8b156a 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -135,6 +135,19 @@ class ComponentManifest: """ return getattr(self.module, "FINAL_VALIDATE_SCHEMA", None) + @property + def legacy_config_migrate(self) -> Callable[[ConfigType], ConfigType | None] | None: + """Optional `LEGACY_CONFIG_MIGRATE` callable on a platform component module. + + Called once, before platform entries are processed, with the raw top-level + config for this domain. It may transform a pre-platform-format config (e.g. + a bare list or legacy dict form) into the normalized list of `platform:` + tagged entries and return it. Returning ``None`` means "already in the new + format, leave untouched". This is an intentionally removable deprecation + shim hook. + """ + return getattr(self.module, "LEGACY_CONFIG_MIGRATE", None) + @property def resources(self) -> list[FileResource]: """Return a list of all file resources defined in the package of this component. diff --git a/esphome/mqtt.py b/esphome/mqtt.py index c6a7a7558b..3198de9d21 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -110,8 +110,12 @@ def prepare( CONF_CLIENT_CERTIFICATE_KEY ): with ( - tempfile.NamedTemporaryFile(mode="w+", delete=False) as cert_file, - tempfile.NamedTemporaryFile(mode="w+", delete=False) as key_file, + tempfile.NamedTemporaryFile( + encoding="utf-8", mode="w+", delete=False + ) as cert_file, + tempfile.NamedTemporaryFile( + encoding="utf-8", mode="w+", delete=False + ) as key_file, ): try: cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE)) diff --git a/esphome/platformio/ccache.py.script b/esphome/platformio/ccache.py.script new file mode 100644 index 0000000000..cc08a8c044 --- /dev/null +++ b/esphome/platformio/ccache.py.script @@ -0,0 +1,34 @@ +import os +import shutil + +# pylint: disable=E0602 +Import("env") # noqa + +# ESPHome decides whether ccache is used and exports the CCACHE_* settings +# into the environment before PlatformIO starts (_ccache_env() in +# esphome/platformio/toolchain.py); this script only supplies the SCons-level +# mechanism. +# +# This is a "pre" script, so the platform's builder (which sets CC/CXX and +# clones the construction environment for framework and library builds) runs +# after it. Replacing CC/CXX here would be overwritten, and replacing them in +# a "post" script would miss the already-cloned library environments. Wrapping +# SPAWN instead is ordering-proof: clones copy the wrapper, and every compiler +# invocation from every environment funnels through it at execution time. +if ( + os.environ.get("ESPHOME_CCACHE_ENABLE") == "1" + and (ccache_path := shutil.which("ccache")) is not None +): + original_spawn = env["SPAWN"] + + def ccache_spawn(sh, escape, cmd, args, child_env): + # Only wrap compile steps (gcc/g++ with -c); linking, archiving and + # the other tools gain nothing from ccache. + prog = os.path.basename(cmd).removesuffix(".exe") + if prog.endswith(("gcc", "g++")) and "-c" in args: + cmd = ccache_path + args = [escape(ccache_path), *args] + return original_spawn(sh, escape, cmd, args, child_env) + + env.Replace(SPAWN=ccache_spawn) + print("ESPHome: Compiling with ccache") diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 291bedb5cd..1a523ce0ab 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -25,7 +25,7 @@ from pathlib import Path import re import tempfile from typing import Any -from urllib.parse import urlparse, urlsplit, urlunsplit +from urllib.parse import urlsplit, urlunsplit from esphome import git from esphome.core import CORE, Library @@ -134,7 +134,7 @@ class GitSource(Source): ref=self.ref, refresh=git.NEVER_REFRESH if not force else None, domain=domain, - submodules=[], + init_submodules=True, subpath=Path(dir_suffix), ) return path @@ -292,6 +292,12 @@ def collect_filtered_files(src_dir: PathType, src_filters: list[str]) -> list[st for root, _, files in os.walk(item): matched.extend([str(Path(root) / f) for f in files]) + # glob keeps the pattern's literal separators for non-wildcard path + # components, so on Windows the same file can surface with different + # separators depending on where the wildcards sit; normalize so the + # include/exclude set operations below compare equal paths. + matched = [os.path.normpath(m) for m in matched] + # FILTER_REGEX only ever captures "+" or "-", so the else is the "-" case. if sign == "+": selected.update(matched) @@ -517,6 +523,17 @@ class _LibNode: edges: set[str] = field(default_factory=set) +def _url_or_none(value: Any) -> str | None: + """Return ``value`` if it parses as a URL (scheme and host), else None.""" + if not value or not isinstance(value, str): + return None + try: + parsed = urlsplit(value) + except ValueError: + return None + return value if parsed.scheme and parsed.netloc else None + + def _node_key( name: str | None, version: str | None, repository: str | None ) -> tuple[str, bool, tuple[str | None, str | None]]: @@ -527,9 +544,23 @@ def _node_key( inconsistently -- bare ``name`` vs ``owner/name``, or git vs registry -- maps to distinct keys and isn't deduplicated; ``convert_libraries`` warns about that after resolution rather than merging the nodes. + + PlatformIO's Library Manager also accepted a git URL in the *name* + position (``add_library("https://github.com/x/y", None)``), including the + ``git+`` VCS prefix and the ``CustomName=URL`` form; recognize those here + so such specs resolve as git sources instead of failing a registry lookup. """ + if not repository and name and "://" in name: + # Try the whole name first so a bare URL whose query contains ``=`` + # stays intact; fall back to the ``CustomName=URL`` form, where the + # key derives from the URL path and the custom name is irrelevant. + repository = _url_or_none(name) or _url_or_none(name.split("=", 1)[-1]) + if repository is None: + # Anything with ``://`` was meant to be a URL; failing it fast + # beats a confusing registry "package not found" error. + raise RuntimeError(f"Invalid PIO library URL: {name}") if repository: - split_result = urlsplit(repository) + split_result = urlsplit(repository.removeprefix("git+")) key = str(split_result.path).strip("/").removesuffix(".git") ref = split_result.fragment.strip() or None url = urlunsplit(split_result._replace(fragment="")) @@ -638,14 +669,30 @@ def convert_libraries( library_json_path = component.path / "library.json" library_properties_path = component.path / "library.properties" - if library_json_path.is_file(): + has_json = library_json_path.is_file() + has_properties = library_properties_path.is_file() + if not has_json and not has_properties: + # The shared cache can hold a broken copy (e.g. a clone or an + # extraction interrupted by a killed process). Force one + # re-download so a bad cache entry self-heals instead of failing + # every build until the user runs a full clean. + _LOGGER.warning( + "Library %s at %s is missing library.json and library.properties; " + "re-downloading", + key, + component.path, + ) + component.download(force=True, salt=salt, namespace=backend.cache_key) + has_json = library_json_path.is_file() + has_properties = library_properties_path.is_file() + if has_json: component.data = _parse_library_json(library_json_path) - elif library_properties_path.is_file(): + elif has_properties: component.data = _parse_library_properties(library_properties_path) else: raise RuntimeError( f"Invalid PIO library {key}: missing library.json and " - "library.properties" + f"library.properties in {component.path}" ) try: @@ -681,13 +728,9 @@ def convert_libraries( continue # The version field may actually be a URL (git/archive dependency). dep_version = dependency["version"] - dep_url = None - try: - parsed = urlparse(dep_version) - if all([parsed.scheme, parsed.netloc]): - dep_url, dep_version = dep_version, None - except (TypeError, ValueError): - pass + dep_url = _url_or_none(dep_version) + if dep_url is not None: + dep_version = None dep_key = add_spec(dep_name, dep_version, dep_url) node.edges.add(dep_key) worklist.append(dep_key) diff --git a/esphome/platformio/toolchain.py b/esphome/platformio/toolchain.py index c97df812e3..0959cbfffb 100644 --- a/esphome/platformio/toolchain.py +++ b/esphome/platformio/toolchain.py @@ -1,17 +1,44 @@ +from collections.abc import Iterable import json import logging import os from pathlib import Path import re +import shutil import sys +from typing import TYPE_CHECKING + +import platformdirs from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError -from esphome.helpers import add_git_ceiling_directory +from esphome.helpers import ( + add_git_ceiling_directory, + copy_file_if_changed, + get_bool_env, + rmtree, + write_file, +) from esphome.util import FlashImage, run_external_process +if TYPE_CHECKING: + from platformio.project.config import ProjectConfig + _LOGGER = logging.getLogger(__name__) +# PlatformIO cache subdirs resolved via ProjectConfig. A full ``clean-all`` wipes +# these plus the whole ``core_dir``; a Python-version heal wipes these plus the +# penv while keeping ``core_dir`` (so the sibling stamp/lock survive). +_PIO_CACHE_DIRS = ("cache_dir", "packages_dir", "platforms_dir") + +# Marker recording the Python major.minor the PlatformIO cache was provisioned +# under, plus the lock guarding the check/wipe. Both live in the dir resolved +# by ``_pio_stamp_dir`` (NOT wiped by the heal), so they survive the wipe and +# are rewritten after it. +_PIO_PYTHON_STAMP_FILE = ".esphome.pio.stamp.json" +_PIO_PYTHON_STAMP_LOCK = ".esphome.pio.stamp.lock" +_PIO_PYTHON_STAMP_SCHEMA = "0" + def _strip_win_long_path_prefix(path: str) -> str: r"""Strip the Windows extended-length path prefix from ``path``. @@ -44,7 +71,245 @@ def _strip_win_long_path_prefix(path: str) -> str: return path +def get_platformio_config() -> "ProjectConfig | None": + """Return PlatformIO's ``ProjectConfig``, or None when PlatformIO is absent.""" + try: + from platformio.project.config import ProjectConfig + except ImportError: + return None + return ProjectConfig.get_instance() + + +def _pio_stamp_dir(config: "ProjectConfig") -> Path: + """Return the persistent home for the python-version stamp and lock. + + The parent of ``platforms_dir``, not ``core_dir``: the container/add-on + images relocate the platform/package caches to a persistent volume while + ``core_dir`` stays at the ephemeral default (its ``appstate.json`` must not + move), so a stamp under ``core_dir`` would be wiped on every image update + while the stale cache it guards survives. Everywhere else ``platforms_dir`` + sits inside ``core_dir`` and this resolves to ``core_dir``. + """ + return Path(config.get("platformio", "platforms_dir")).parent + + +def _delete_platformio_dirs(config: "ProjectConfig", pio_dirs: Iterable[str]) -> None: + """Delete each named PlatformIO dir resolved from *config*.""" + for pio_dir in pio_dirs: + path = Path(config.get("platformio", pio_dir)) + if path.is_dir(): + _LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path) + rmtree(path) + + +def clean_platformio_cache() -> None: + """Wipe the whole PlatformIO cache (cache/packages/platforms/core). + + The full set ``clean-all`` (Reset Build Environment) clears. No-op when + PlatformIO is unavailable. + """ + config = get_platformio_config() + if config is None: + return + _delete_platformio_dirs(config, [*_PIO_CACHE_DIRS, "core_dir"]) + + +def _clean_platformio_python_env(config: "ProjectConfig", core_dir: Path) -> None: + """Wipe the cache subdirs + penv for a Python-version change. + + Keeps ``core_dir`` itself (and the stamp/lock siblings under it); otherwise + the same cache set ``clean-all`` clears. + """ + _delete_platformio_dirs(config, _PIO_CACHE_DIRS) + penv = core_dir / "penv" + if penv.is_dir(): + _LOGGER.info("Deleting PlatformIO penv %s", penv) + rmtree(penv) + + +def _current_python_minor() -> str: + """Return the running interpreter's ``major.minor`` (e.g. ``3.13``).""" + return f"{sys.version_info.major}.{sys.version_info.minor}" + + +def _read_pio_stamp_python(stamp_file: Path) -> str | None: + """Return the ``python_version`` recorded in *stamp_file*, or None.""" + try: + with stamp_file.open(encoding="utf-8") as f: + data = json.load(f) + except FileNotFoundError: + return None + except (json.JSONDecodeError, OSError) as err: + # A present-but-unreadable stamp is a distinct signal from an absent + # one, and it drives a cache clean; surface why at normal verbosity. + _LOGGER.warning("Could not read %s: %s", stamp_file, err) + return None + if not isinstance(data, dict): + return None + version = data.get("python_version") + return version if isinstance(version, str) else None + + +def _write_pio_stamp_python(stamp_file: Path, python_version: str) -> None: + """Atomically write the PlatformIO python-version stamp.""" + write_file( + stamp_file, + json.dumps( + { + "schema_version": _PIO_PYTHON_STAMP_SCHEMA, + "python_version": python_version, + } + ), + ) + + +def heal_platformio_python_env() -> None: + """Wipe the PlatformIO cache unless it is stamped for the running Python. + + A PlatformIO platform/tool package pins the Python versions it accepts when + it is provisioned, and ESPHome pins platforms to exact, immutable versions, + so a later interpreter bump (a container upgrading its base Python) leaves + the cached platform rejecting the new interpreter ("Python version must be + between ...") until the cache is wiped. A stamp records the ``major.minor`` + the cache was provisioned for; when it doesn't match the running + interpreter (or has never been written for an existing cache), the same + PlatformIO dirs ``clean-all`` wipes are cleaned so PlatformIO + re-provisions, matching Reset Build Environment automatically. The native + ESP-IDF toolchain already self-heals through its own stamp; this covers the + PlatformIO path. No-op when PlatformIO is unavailable. + """ + config = get_platformio_config() + if config is None: + return + try: + _check_platformio_python_stamp(config) + except (EsphomeError, OSError) as err: + # The check is a best-effort repair; a full or read-only cache volume + # must not abort a build that might otherwise work. The stamp write + # surfaces as EsphomeError (write_file wraps OSError). + _LOGGER.warning("PlatformIO build environment check failed: %s", err) + + +def _check_platformio_python_stamp(config: "ProjectConfig") -> None: + """Compare the stamp to the running interpreter; wipe and restamp on mismatch.""" + current = _current_python_minor() + stamp_dir = _pio_stamp_dir(config) + # Host the stamp/lock even before PlatformIO's first run creates the dir. + stamp_dir.mkdir(parents=True, exist_ok=True) + stamp_file = stamp_dir / _PIO_PYTHON_STAMP_FILE + + from filelock import FileLock + + with FileLock(str(stamp_dir / _PIO_PYTHON_STAMP_LOCK)): + provisioned = _read_pio_stamp_python(stamp_file) + if provisioned == current: + return + core_dir = Path(config.get("platformio", "core_dir")) + has_cache = ( + any( + Path(config.get("platformio", pio_dir)).is_dir() + for pio_dir in _PIO_CACHE_DIRS + ) + or (core_dir / "penv").is_dir() + ) + if has_cache: + if provisioned is None: + # An existing cache with no stamp predates the stamp: its + # provisioning interpreter is unknown, so clean once rather + # than leave a possibly-stale cache failing every build. + _LOGGER.info( + "Cleaning the PlatformIO build environment once so it " + "re-provisions for Python %s", + current, + ) + else: + _LOGGER.info( + "Python version changed (%s -> %s); cleaning PlatformIO " + "build environment so it re-provisions for the new " + "interpreter", + provisioned, + current, + ) + _clean_platformio_python_env(config, core_dir) + _write_pio_stamp_python(stamp_file, current) + + +def _ccache_env() -> dict[str, str]: + """Return ccache settings for PlatformIO builds. + + Enabled by default whenever the ``ccache`` binary is on PATH; set + ``ESPHOME_CCACHE_ENABLE=0`` in the environment to opt out (or ``1`` to + force it on). The decision is normalized into ``ESPHOME_CCACHE_ENABLE`` + so platform build scripts (e.g. the esp8266 ``ccache.py`` extra script, + which wraps compiler invocations inside SCons) only have to check for + ``"1"`` instead of re-implementing the policy. + + The returned values are merged into the environment of the PlatformIO + subprocess only, never into ``os.environ``: a long-running process + (e.g. the dashboard) also runs ESP-IDF builds, whose own ccache setup + skips defaults for ``CCACHE_*`` keys it finds already set, so leaking + these values would hand it the wrong cache dir and a stale basedir. + + This mirrors ``_ccache_env()`` in ``esphome/espidf/framework.py``. The + cache lives under the machine-global ESPHome cache dir, so it is shared + across all projects and removed by ``esphome clean-all``. Unlike the + ESP-IDF path, ``CCACHE_DEPEND`` is not set: SCons compiles don't emit + the depfiles depend mode needs, so ccache's default preprocessor mode + is used. + + ``CCACHE_BASEDIR`` rewrites the per-device absolute paths (the generated + sources under src/, the .pioenvs build dir) so different devices with + identical source share cache entries; it is always set to the current + build dir. The other ``CCACHE_*`` values the user already set in the + environment are respected. + """ + if "ESPHOME_CCACHE_ENABLE" in os.environ: + enabled = get_bool_env("ESPHOME_CCACHE_ENABLE") + else: + enabled = shutil.which("ccache") is not None + env = {"ESPHOME_CCACHE_ENABLE": "1" if enabled else "0"} + if not enabled: + return env + # build_path is set during preload for every config-loading command, so it + # being unset means a caller built the environment too early; fail loudly + # rather than with an opaque TypeError from Path(None). + if CORE.build_path is None: + raise ValueError( + "CORE.build_path must be set before constructing the PlatformIO " + "build environment" + ) + env["CCACHE_BASEDIR"] = str(Path(CORE.build_path).resolve()) + defaults = { + "CCACHE_DIR": str( + Path(platformdirs.user_cache_dir("esphome", appauthor=False)) + / "platformio-ccache" + ), + "CCACHE_NOHASHDIR": "true", + } + env.update({k: v for k, v in defaults.items() if k not in os.environ}) + return env + + +def copy_ccache_script() -> None: + """Copy the shared ccache SCons pre-script into the build dir. + + Platform components call this from their ``copy_files()`` and add + ``pre:ccache.py`` to their ``extra_scripts``. The script wraps compiler + invocations inside SCons with ccache; it is platform-agnostic, so it + lives here next to ``_ccache_env()`` rather than being duplicated per + component. + """ + copy_file_if_changed( + Path(__file__).parent / "ccache.py.script", + CORE.relative_build_path("ccache.py"), + ) + + def run_platformio_cli(*args, **kwargs) -> str | int: + # Re-provision the PlatformIO cache if the interpreter's major.minor changed + # since it was last built; a stale platform otherwise rejects the new Python + # with "Python version must be between ..." until Reset Build Environment. + heal_platformio_python_env() os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) os.environ.setdefault( @@ -71,7 +336,14 @@ def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PYTHONEXEPATH"] = python_exe cmd = [python_exe, "-m", "esphome.platformio.runner"] + list(args) - return run_external_process(*cmd, **kwargs) + # ccache settings go into the subprocess environment only (see + # _ccache_env() for why they must not leak into os.environ). A caller + # supplied env is used as the base when present. + base_env = kwargs.pop("env", None) + env = dict(os.environ if base_env is None else base_env) + env.update(_ccache_env()) + + return run_external_process(*cmd, env=env, **kwargs) def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int: diff --git a/esphome/preferences.py b/esphome/preferences.py new file mode 100644 index 0000000000..fce8519130 --- /dev/null +++ b/esphome/preferences.py @@ -0,0 +1,106 @@ +"""Helpers for letting a component choose where a preference is persisted. + +Preferences can be stored either in flash (durable across power loss) or in RTC +memory (fast, survives deep sleep and soft resets but not power loss). The +flash-vs-RTC choice is only meaningful on platforms whose preferences backend +honors the ``in_flash`` flag — currently ESP32 and ESP8266. On other platforms +the value is accepted only as ``flash`` (the sole supported backend). + +Components include :func:`storage_schema` in their config and convert the chosen +value with :func:`is_in_flash` when calling ``make_preference``. +""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_STORAGE +from esphome.core import CORE + +STORAGE_FLASH = "flash" +STORAGE_RTC = "rtc" + + +def _rtc_supported() -> bool: + """Whether the active platform has an RTC-backed preferences backend. + + Mirrors the C++ ``SOC_RTC_MEM_SUPPORTED`` guard in the ESP32 backend: the ESP32-C2 + and -C61 have no RTC memory at all, so RTC storage is unavailable there. + """ + if CORE.is_esp8266: + return True + if CORE.is_esp32: + from esphome.components.esp32 import get_esp32_variant + from esphome.components.esp32.const import VARIANT_ESP32C2, VARIANT_ESP32C61 + + return get_esp32_variant() not in (VARIANT_ESP32C2, VARIANT_ESP32C61) + return False + + +def _default_storage() -> str: + """Default that preserves each platform's historic behavior. + + ESP8266 has always stored these preferences in RTC memory; every other + platform effectively used flash. Evaluated at validation time. + """ + return STORAGE_RTC if CORE.is_esp8266 else STORAGE_FLASH + + +def _validate_storage(value): + value = cv.one_of(STORAGE_FLASH, STORAGE_RTC, lower=True)(value) + if value == STORAGE_RTC and not _rtc_supported(): + raise cv.Invalid( + f"'{STORAGE_RTC}' storage is not supported on this platform; only " + f"'{STORAGE_FLASH}' is available" + ) + return value + + +def storage_schema(): + """Return an Optional(CONF_STORAGE) entry for merging into a component schema.""" + return {cv.Optional(CONF_STORAGE, default=_default_storage): _validate_storage} + + +def request_rtc_storage() -> None: + """Compile the RTC-backed storage into the ESP32 preferences backend. + + The RTC storage region is left out of ESP32 builds unless something asks for + it, so unused builds don't reserve RTC memory. Call this from ``to_code`` + when a config option selects RTC storage. No-op on other platforms (ESP8266 + always has its RTC backend). + """ + if CORE.is_esp32: + cg.add_define("USE_ESP32_RTC_PREFERENCES") + + +def validate_rtc_storage(value): + """Validate a boolean option that requests RTC-backed preference storage. + + ``false`` means "no request", not "disable": it never turns RTC storage off + (another option selecting ``storage: rtc`` still compiles it in). On ESP8266 + the backend is integral and always enabled, so ``false`` is rejected rather + than silently ignored; ``true`` is a tolerated no-op there so shared config + packages work across mixed fleets. + """ + value = cv.boolean(value) + if not value: + if CORE.is_esp8266: + raise cv.Invalid( + "RTC preference storage is always enabled on ESP8266 and cannot " + "be disabled" + ) + return value + if not _rtc_supported(): + raise cv.Invalid("RTC preference storage is not supported on this platform") + return value + + +def is_in_flash(value: str) -> bool: + """Map a CONF_STORAGE value to the ``in_flash`` argument of make_preference. + + Call this from ``to_code``: when RTC storage is selected on ESP32 it also emits + the define that compiles the RTC storage buffer into the ESP32 backend (see + :func:`request_rtc_storage`). + """ + in_flash = value == STORAGE_FLASH + if not in_flash: + request_rtc_storage() + return in_flash diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 9d662df8f8..6376e573c4 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -133,7 +133,7 @@ class StorageJSON: self.no_mdns = no_mdns # The framework used to compile the firmware self.framework = framework - # The core platform of this firmware. Like "esp32", "rp2040", "host" etc. + # The core platform of this firmware. Like "esp32", "rp2", "host" etc. self.core_platform = core_platform # The toolchain used for the build ("platformio" / "esp-idf") self.toolchain = toolchain diff --git a/esphome/util.py b/esphome/util.py index b597b4b42e..f7d33bd2a9 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -314,6 +314,7 @@ def run_external_process(*cmd: str, **kwargs: Any) -> int | str: encoding="utf-8", check=False, close_fds=False, + env=kwargs.get("env"), ) return proc.stdout if capture_stdout else proc.returncode except KeyboardInterrupt: # pylint: disable=try-except-raise diff --git a/esphome/wizard.py b/esphome/wizard.py index f83342cc6a..f7706928e9 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -75,8 +75,8 @@ esp32: type: esp-idf """ -RP2040_CONFIG = """ -rp2040: +RP2_CONFIG = """ +rp2: board: {board} """ @@ -98,7 +98,7 @@ rtl87xx: HARDWARE_BASE_CONFIGS = { "ESP8266": ESP8266_CONFIG, "ESP32": ESP32_CONFIG, - "RP2040": RP2040_CONFIG, + "RP2": RP2_CONFIG, "BK72XX": BK72XX_CONFIG, "LN882X": LN882X_CONFIG, "RTL87XX": RTL87XX_CONFIG, @@ -113,7 +113,7 @@ class WizardFileKwargs(TypedDict): """Keyword arguments for wizard_file function.""" name: str - platform: Literal["ESP8266", "ESP32", "RP2040", "BK72XX", "LN882X", "RTL87XX"] + platform: Literal["ESP8266", "ESP32", "RP2", "BK72XX", "LN882X", "RTL87XX"] board: str ssid: NotRequired[str] psk: NotRequired[str] @@ -213,7 +213,7 @@ def wizard_write(path: Path, **kwargs: Unpack[WizardWriteKwargs]) -> bool: from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards from esphome.components.ln882x import boards as ln882x_boards - from esphome.components.rp2040 import boards as rp2040_boards + from esphome.components.rp2 import boards as rp2_boards from esphome.components.rtl87xx import boards as rtl87xx_boards name = kwargs["name"] @@ -235,8 +235,8 @@ def wizard_write(path: Path, **kwargs: Unpack[WizardWriteKwargs]) -> bool: platform = "ESP8266" elif board in esp32_boards.BOARDS: platform = "ESP32" - elif board in rp2040_boards.BOARDS: - platform = "RP2040" + elif board in rp2_boards.BOARDS: + platform = "RP2" elif board in bk72xx_boards.BOARDS: platform = "BK72XX" elif board in ln882x_boards.BOARDS: @@ -301,7 +301,7 @@ def wizard(path: Path) -> int: from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards from esphome.components.ln882x import boards as ln882x_boards - from esphome.components.rp2040 import boards as rp2040_boards + from esphome.components.rp2 import boards as rp2_boards from esphome.components.rtl87xx import boards as rtl87xx_boards if path.suffix not in (".yaml", ".yml"): @@ -373,7 +373,7 @@ def wizard(path: Path) -> int: "firmwares for it." ) - wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "LN882X", "RTL87XX", "RP2040"] + wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "LN882X", "RTL87XX", "RP2"] safe_print( "Please choose one of the supported microcontrollers " "(Use ESP8266 for Sonoff devices)." @@ -405,7 +405,7 @@ def wizard(path: Path) -> int: board_link = ( "https://docs.platformio.org/en/latest/platforms/espressif8266.html#boards" ) - elif platform == "RP2040": + elif platform == "RP2": board_link = "https://www.raspberrypi.com/documentation/microcontrollers/silicon.html#rp2040" elif platform in ["BK72XX", "LN882X", "RTL87XX"]: board_link = "https://docs.libretiny.eu/docs/status/supported/" @@ -421,27 +421,21 @@ def wizard(path: Path) -> int: safe_print(f"(Type {color(AnsiFore.GREEN, 'esp01_1m')} for Sonoff devices)") safe_print() # Don't sleep because user needs to copy link - if platform == "ESP32": - safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcu-32s")}".') - boards_list = esp32_boards.BOARDS.items() - elif platform == "ESP8266": - safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcuv2")}".') - boards_list = esp8266_boards.BOARDS.items() - elif platform == "BK72XX": - safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".') - boards_list = bk72xx_boards.BOARDS.items() - elif platform == "LN882X": - safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wl2s")}".') - boards_list = ln882x_boards.BOARDS.items() - elif platform == "RTL87XX": - safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".') - boards_list = rtl87xx_boards.BOARDS.items() - elif platform == "RP2040": - safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "rpipicow")}".') - boards_list = rp2040_boards.BOARDS.items() - - else: - raise NotImplementedError("Unknown platform!") + # Platform-to-(example board, boards module) lookup. Dict-driven so the + # set of supported platforms has a single source of truth and the elif + # chain — which left the last entry's "False" branch structurally + # unreachable in tests — is gone. + example_boards = { + "ESP32": ("nodemcu-32s", esp32_boards), + "ESP8266": ("nodemcuv2", esp8266_boards), + "BK72XX": ("cb2s", bk72xx_boards), + "LN882X": ("wl2s", ln882x_boards), + "RTL87XX": ("wr3", rtl87xx_boards), + "RP2": ("rpipicow", rp2_boards), + } + example, boards_module = example_boards[platform] + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, example)}".') + boards_list = boards_module.BOARDS.items() boards = [] safe_print("Options:") diff --git a/esphome/writer.py b/esphome/writer.py index b7eeec916d..866377d2f5 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -670,18 +670,9 @@ def clean_all(configuration: list[str]): rmtree(install_path) # Clean PlatformIO project files - try: - from platformio.project.config import ProjectConfig - except ImportError: - # PlatformIO is not available, skip cleaning - pass - else: - config = ProjectConfig.get_instance() - for pio_dir in ["cache_dir", "packages_dir", "platforms_dir", "core_dir"]: - path = Path(config.get("platformio", pio_dir)) - if path.is_dir(): - _LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path) - rmtree(path) + from esphome.platformio.toolchain import clean_platformio_cache + + clean_platformio_cache() GITIGNORE_CONTENT = """# Gitignore settings for ESPHome diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 0009cde551..c2db9b97ed 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -840,17 +840,22 @@ def _load_yaml_internal_with_type( loader.dispose() -def dump(dict_, show_secrets=False, sort_keys=False): - """Dump YAML to a string and remove null.""" +def dump(dict_, show_secrets=False, sort_keys=False, relative_to: Path | None = None): + """Dump YAML to a string and remove null. + + When ``relative_to`` is given, Path values are dumped relative to that + directory (POSIX form) so the output is machine independent. + """ if show_secrets: _SECRET_VALUES.clear() _SECRET_CACHE.clear() - # Per-call subclass so the redaction flag doesn't leak across calls. + # Per-call subclass so the flags don't leak across calls. # (``_SECRET_VALUES`` / ``_SECRET_CACHE`` remain module globals; YAML - # processing is single-threaded today, so this isolates only the flag.) + # processing is single-threaded today, so this isolates only the flags.) class _Dumper(ESPHomeDumper): _redact_sensitive = not show_secrets + _relative_to = relative_to return yaml.dump( dict_, @@ -1002,9 +1007,13 @@ def format_path(path: DocumentPath, current_obj: Any) -> str: class ESPHomeDumper(yaml.SafeDumper): - # Default for the base class; per-call subclass in ``dump()`` overrides. + # Defaults for the base class; per-call subclass in ``dump()`` overrides. # When True, ``represent_sensitive`` wraps values in ANSI conceal codes. _redact_sensitive: bool = False + # When set, ``represent_path`` dumps Path values relative to this + # directory (in POSIX form) so the output does not depend on where the + # config lives on the machine that produced it. + _relative_to: Path | None = None def represent_mapping(self, tag, mapping, flow_style=None): value = [] @@ -1040,6 +1049,21 @@ class ESPHomeDumper(yaml.SafeDumper): return self.represent_secret(value) return self.represent_scalar(tag="tag:yaml.org,2002:str", value=str(value)) + def represent_path(self, value: Path) -> yaml.ScalarNode: + if self._relative_to is not None: + # Normalize both sides lexically (no symlink resolution) so ".." + # segments do not defeat the prefix match, and walk up so files + # referenced outside the anchor directory stay relative too. A + # path that still cannot be relativized (e.g. a different drive) + # keeps its POSIX form so separators stay stable across OSes. + path = Path(os.path.normpath(value)) + with suppress(ValueError): + path = path.relative_to( + os.path.normpath(self._relative_to), walk_up=True + ) + return self.represent_stringify(path.as_posix()) + return self.represent_stringify(value) + def represent_sensitive(self, value: SensitiveStr) -> yaml.ScalarNode: # Only the redact-and-not-a-secret branch is unique to sensitive # values; otherwise let ``represent_stringify`` handle ``!secret`` @@ -1138,5 +1162,5 @@ ESPHomeDumper.add_multi_representer(Extend, ESPHomeDumper.represent_extend) ESPHomeDumper.add_multi_representer(Remove, ESPHomeDumper.represent_remove) ESPHomeDumper.add_multi_representer(core.ID, ESPHomeDumper.represent_id) ESPHomeDumper.add_multi_representer(uuid.UUID, ESPHomeDumper.represent_stringify) -ESPHomeDumper.add_multi_representer(Path, ESPHomeDumper.represent_stringify) +ESPHomeDumper.add_multi_representer(Path, ESPHomeDumper.represent_path) ESPHomeDumper.add_multi_representer(IncludeFile, ESPHomeDumper.represent_include_file) diff --git a/platformio.ini b/platformio.ini index 061e92a64a..9f2ac74ac0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,7 +45,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} esphome/noise-c@0.1.11 ; api - improv/Improv@1.2.4 ; improv_serial / esp32_improv + improv/Improv@1.2.6 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; online_image @@ -141,10 +141,10 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.9/esp32-core-3.3.9.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.11/esp32-core-3.3.11.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.5/esp-idf-v5.5.5.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -178,9 +178,9 @@ extra_scripts = ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.5/esp-idf-v5.5.5.tar.xz framework = espidf lib_deps = @@ -214,8 +214,19 @@ lib_deps = ${common:idf-component-libs.lib_deps} ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base + WiFi ; wifi (arduino-pico built-in) + lwIP_CYW43 ; wifi (arduino-pico built-in, WiFi dependency) + HTTPClient ; http_request (arduino-pico built-in) + Updater ; ota (arduino-pico built-in) + MD5Builder ; md5 (arduino-pico built-in) + LEAmDNS ; mdns (arduino-pico built-in) + lwIP_w5500 ; ethernet (arduino-pico built-in) + lwIP-Ethernet ; ethernet (arduino-pico built-in, lwIP_w5500/lwIP_CYW43 dependency) + WebServer ; web_server_base (arduino-pico built-in, ESPAsyncWebServer dependency) + http-parser ; web_server_base (arduino-pico built-in, ESPAsyncWebServer dependency) build_flags = ${common:arduino.build_flags} + -DUSE_RP2 -DUSE_RP2040 -DUSE_RP2040_FRAMEWORK_ARDUINO build_unflags = @@ -228,9 +239,17 @@ platform = https://github.com/libretiny-eu/libretiny.git#v1.13.0 framework = arduino lib_compat_mode = soft lib_deps = + ${common.lib_deps_base} ${common:idf-component-libs.lib_deps} ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.5 ; wireguard + esphome/noise-c@0.1.11 ; api + ESP32Async/AsyncTCP@3.4.5 ; async_tcp + DNSServer ; captive_portal + heman/AsyncMqttClient-esphome@2.0.0 ; mqtt + improv/Improv@1.2.6 ; improv_serial + kikuchan98/pngle@1.1.0 ; online_image + https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; online_image build_flags = ${common:arduino.build_flags} -DUSE_LIBRETINY @@ -510,6 +529,17 @@ build_flags = build_unflags = ${common.build_unflags} +[env:rp2-tidy] +extends = common:rp2040-arduino +; The W variant so the cyw43 / WiFi library paths are part of the idedata. +board = rpipicow +build_flags = + ${common:rp2040-arduino.build_flags} + ${flags:clangtidy.build_flags} + -DPIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH +build_unflags = + ${common.build_unflags} + ;;;;;;;; LibreTiny ;;;;;;;; [env:bk72xx-arduino] @@ -556,6 +586,54 @@ build_flags = build_unflags = ${common.build_unflags} +[env:bk72xx-tidy] +extends = common:libretiny-arduino +board = generic-bk7231n-qfn32-tuya +build_flags = + ${common:libretiny-arduino.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_BK72XX + -DUSE_LIBRETINY_VARIANT_BK7231N +build_unflags = + ${common.build_unflags} + +[env:ln882h-tidy] +extends = common:libretiny-arduino +board = generic-ln882h +build_flags = + ${common:libretiny-arduino.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_LN882X + -DUSE_LIBRETINY_VARIANT_LN882H + ; the SDK lwip port dir is missing from pio idedata; lwipopts.h include_next needs it + -I${platformio.packages_dir}/framework-lightning-ln882h/components/net/lwip-2.1.3/src/port/ln_osal/include +build_unflags = + ${common.build_unflags} + +[env:rtl87xxb-tidy] +extends = common:libretiny-arduino +board = generic-rtl8710bn-2mb-788k +; mirror the libretiny codegen pin: RTL8710B needs 8.2.3+ for task notifications +custom_versions.freertos = 8.2.3 +build_flags = + ${common:libretiny-arduino.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_RTL87XX + -DUSE_LIBRETINY_VARIANT_RTL8710B +build_unflags = + ${common.build_unflags} + +[env:rtl87xxc-tidy] +extends = common:libretiny-arduino +board = generic-rtl8720cf-2mb-992k +build_flags = + ${common:libretiny-arduino.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_RTL87XX + -DUSE_LIBRETINY_VARIANT_RTL8720C +build_unflags = + ${common.build_unflags} + ;;;;;;;; Host ;;;;;;;; [env:host] diff --git a/pyproject.toml b/pyproject.toml index e959578553..eda3c4cf7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==82.0.1", "wheel>=0.43,<0.48"] +requires = ["setuptools==83.0.0", "wheel>=0.43,<0.48"] build-backend = "setuptools.build_meta" [project] @@ -110,6 +110,10 @@ target-version = "py312" exclude = ['generated'] [tool.ruff.lint] +# Preview mode is scoped: with explicit-preview-rules only rules named in +# select run in preview, prefixes like "PL" keep their stable set. +preview = true +explicit-preview-rules = true select = [ "B", # flake8-bugbear "BLE", # flake8-blind-except @@ -131,6 +135,7 @@ select = [ "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint + "PLW1514", # require explicit encoding on text file I/O (Windows defaults to cp1252) "PTH", # flake8-use-pathlib "PYI", # flake8-pyi "Q", # flake8-quotes @@ -151,6 +156,7 @@ ignore = [ "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR0917", # Too many positional arguments ({c_pos} > {max_pos}) "PLW1641", # Object does not implement `__hash__` method "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target diff --git a/requirements.txt b/requirements.txt index 95388f278f..6260e4a44a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,18 @@ -cryptography==49.0.0 +# cryptography 49+ ships no Intel macOS wheels (arm64 only); esptool caps <49 there. +# Keep 48.0.1, the last universal2 release, so esphome stays installable on Intel Macs. +cryptography==49.0.0; platform_system != "Darwin" or platform_machine != "x86_64" +cryptography==48.0.1; platform_system == "Darwin" and platform_machine == "x86_64" voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 colorama==0.4.6 tzlocal==5.4.4 # from time -tzdata>=2026.2 # from time +tzdata>=2026.3 # from time pyserial==3.5 platformio==6.1.19 esptool==5.3.1 click==8.3.3 -aioesphomeapi==45.5.2 +aioesphomeapi==45.7.0 zeroconf==0.150.0 puremagic==2.2.0 ruamel.yaml==0.19.1 # dashboard_import @@ -20,10 +23,11 @@ resvg-py==0.3.3 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 -smpclient==6.0.0 +smpclient==7.2.0 requests==2.34.2 py7zr==1.1.3 -platformdirs==4.10.0 # native esp-idf toolchain global cache dir +platformdirs==4.11.0 # native esp-idf toolchain global cache dir +filelock==3.32.0 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg # esp-idf >= 5.0 requires this pyparsing >= 3.3.2 diff --git a/requirements_dev.txt b/requirements_dev.txt index 7e66c7244d..f2cf855d6b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,4 @@ # Useful stuff when working in a development environment clang-format==13.0.1 # also change in .pre-commit-config.yaml and Dockerfile when updating -clang-tidy==22.1.7 +clang-tidy==22.1.8 yamllint==1.38.0 # also change in .pre-commit-config.yaml when updating diff --git a/requirements_test.txt b/requirements_test.txt index ebd93ea390..e7de8eb5e7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.6 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.20 # also change in .pre-commit-config.yaml when updating +ruff==0.16.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 8eb80d9943..a6ccb79544 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -90,6 +90,7 @@ ISOLATED_COMPONENTS = { "openthread_info": "Conflicts with wifi: used by most components", "matrix_keypad": "Needs isolation due to keypad", "microphone": "Defines PDM microphone requiring I2S port 0 - conflicts with micro_wake_word PDM mic when merged", + "mipi_rgb": "RGB display occupies many GPIOs (including ones used by the shared i2c bus) that conflict when merged with other bus components", "modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus", "neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)", "packages": "cannot merge packages", diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 10ca1295b7..be8b445542 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -61,6 +61,13 @@ for path in components_dir.iterdir(): codeowners[f"esphome/components/{name}/*"].extend(comp.codeowners) for platform_path in path.iterdir(): + if platform_path.name == "__init__.py": + # `import pkg.__init__` is valid but distinct from `import pkg`: it re-executes + # the component's __init__.py as a second, separate module. That's harmless for + # components whose top-level code is idempotent, but not guaranteed in general + # (e.g. code that registers into a global registry with a duplicate check), so + # never treat __init__.py itself as a platform candidate. + continue platform_name = platform_path.stem platform = get_platform(platform_name, name) if platform is None: diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 974957245a..f6dcf00851 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -390,16 +390,6 @@ def fix_mapping(): output["mapping"][S_SCHEMAS][S_CONFIG_SCHEMA] = config -def fix_image(): - if "image" not in output: - return - from esphome.components.image import IMAGE_SCHEMA - - config = convert_config(IMAGE_SCHEMA, "image/CONFIG_SCHEMA") - config["is_list"] = True - output["image"][S_SCHEMAS][S_CONFIG_SCHEMA] = config - - def fix_menu(): if "display_menu_base" not in output: return @@ -763,7 +753,6 @@ def build_schema(): fix_font() fix_globals() fix_mapping() - fix_image() add_logger_tags() shrink() fix_menu() @@ -785,6 +774,29 @@ def build_schema(): # bundle core inside esphome data["esphome"]["core"] = data.pop("core")["core"] + # Surface deprecated component aliases (declared via ``ALIASES = [...]`` + # on the canonical component) so language servers / dashboard + # autocomplete still accept legacy top-level keys instead of flagging + # them as unknown. Each alias gets its own bundle that mirrors the + # canonical schema; ``alias_of`` and the optional ``removal_version`` + # metadata let consumers render a deprecation hint and point users at + # the canonical name. Without this, configs migrated only at runtime + # (via the ``_resolve_component_aliases`` pre-pass) would still light + # up as errors in the editor. + for domain, manifest in components.items(): + aliases = manifest.aliases + if not aliases or domain not in data: + continue + canonical_bundle = data[domain].get(domain) + if canonical_bundle is None: + continue + for alias in aliases: + alias_entry = dict(canonical_bundle) + alias_entry["alias_of"] = domain + if manifest.alias_removal_version is not None: + alias_entry["removal_version"] = manifest.alias_removal_version + data[alias] = {alias: alias_entry} + if GENERATED_ID_TYPES: print( "Unconsumed id_type matchers:", diff --git a/script/check_import_time.py b/script/check_import_time.py index 0d5362c968..0f2b395902 100755 --- a/script/check_import_time.py +++ b/script/check_import_time.py @@ -194,7 +194,7 @@ def cmd_update(args: argparse.Namespace) -> int: def cmd_har_only(args: argparse.Namespace) -> int: - Path(args.har).write_text(run_waterfall(TARGET_MODULE)) + Path(args.har).write_text(run_waterfall(TARGET_MODULE), encoding="utf-8") print(f"Wrote waterfall HAR to {args.har}") return 0 diff --git a/script/ci-custom.py b/script/ci-custom.py index 4568732b88..90748a13b9 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -345,9 +345,11 @@ def lint_const_ordered(fname, content): ( mi, 1, - f"Constant {highlight(mline)} is not ordered, please make sure all " - f"constants are ordered. See line {mi} (should go to line {target}, " - f"{target_text})", + ( + f"Constant {highlight(mline)} is not ordered, please make sure all " + f"constants are ordered. See line {mi} (should go to line {target}, " + f"{target_text})" + ), ) ) return errs @@ -555,7 +557,7 @@ def lint_constants_usage(): # Maximum allowed CONF_ constants in esphome/const.py. # This file is frozen — new constants go in esphome/components/const/__init__.py. # Decrease this number when constants are moved out of const.py. -CONST_PY_MAX_CONF = 1014 +CONST_PY_MAX_CONF = 1015 @lint_content_check(include=["esphome/const.py"]) @@ -621,6 +623,9 @@ def convert_path_to_relative(abspath, current): "esphome/components/web_server/__init__.py", # const.py has absolute import in docstring example for external components "esphome/components/esp8266/const.py", + # rp2040/__init__.py is the deprecation shim that documents the canonical + # rp2 module path and its own legacy import paths in docstrings/comments. + "esphome/components/rp2040/__init__.py", ], ) def lint_relative_py_import(fname: Path, line, col, content): @@ -650,13 +655,13 @@ def lint_relative_py_import(fname: Path, line, col, content): "esphome/components/async_tcp/async_tcp.h", "esphome/components/esp32/core.cpp", "esphome/components/esp8266/core.cpp", - "esphome/components/rp2040/core.cpp", + "esphome/components/rp2/core.cpp", "esphome/components/libretiny/core.cpp", "esphome/components/host/core.cpp", "esphome/components/zephyr/core.cpp", "esphome/components/esp32/helpers.cpp", "esphome/components/esp8266/helpers.cpp", - "esphome/components/rp2040/helpers.cpp", + "esphome/components/rp2/helpers.cpp", "esphome/components/libretiny/helpers.cpp", "esphome/components/host/helpers.cpp", "esphome/components/zephyr/helpers.cpp", @@ -987,12 +992,14 @@ def lint_log_multiline_continuation(fname, content): ( lineno, col, - "Multi-line log message has a continuation line that does " - "not start with a space. The log viewer uses leading " - "whitespace to detect continuation lines and re-add the " - f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n" - "Either start the continuation with a space/indent, or " - "split into separate ESP_LOG* calls.", + ( + "Multi-line log message has a continuation line that does " + "not start with a space. The log viewer uses leading " + "whitespace to detect continuation lines and re-add the " + f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n" + "Either start the continuation with a space/indent, or " + "split into separate ESP_LOG* calls." + ), ) ) return errs @@ -1070,10 +1077,12 @@ def lint_test_package_key_matches_bus(fname, content): ( lineno, 1, - f"Package key {highlight(pkg_key)} does not match bus directory " - f"{highlight(bus_dir)}. The package key must match the directory " - f"name under tests/test_build_components/common/. " - f"Change {highlight(pkg_key)} to {highlight(bus_dir)}.", + ( + f"Package key {highlight(pkg_key)} does not match bus directory " + f"{highlight(bus_dir)}. The package key must match the directory " + f"name under tests/test_build_components/common/. " + f"Change {highlight(pkg_key)} to {highlight(bus_dir)}." + ), ) ) return errs diff --git a/script/ci_check_test_fixture_list_form.py b/script/ci_check_test_fixture_list_form.py new file mode 100755 index 0000000000..6da1f8337d --- /dev/null +++ b/script/ci_check_test_fixture_list_form.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Fail when a test fixture writes a platform-list domain as a single dict. + +Component tests are merged and built in groups in CI (see +``script/merge_component_configs.py``). ESPHome's ``merge_config`` concatenates +two lists, but when one side is a dict it replaces the other side wholesale +(``esphome/config_helpers.py``). A domain such as ``one_wire:`` or ``ota:`` +written in single-dict form therefore deletes every entry other components +contributed to that domain before it in the merge, and is itself deleted by any +list that merges after it. The resulting failure only appears when the affected +components land in the same group -- usually a full component matrix run on an +unrelated PR long after the fixture was written (this is what broke the +dallas_temp tests when ds2484 was added, see #17868). + +This guard scans every fixture under ``tests/components/`` and rejects any +top-level domain written as a dict with a ``platform`` key. Such a domain is by +definition a platform list (single-dict form is only user-config sugar), so the +fix is always to write it as a one-element list: + + one_wire: + - platform: gpio + pin: 4 +""" + +from __future__ import annotations + +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from esphome.core import EsphomeError # noqa: E402 +from script.analyze_component_buses import ISOLATED_COMPONENTS # noqa: E402 +from script.merge_component_configs import load_yaml_file # noqa: E402 + +# Resolved relative to this file (not the CWD) so the scan cannot silently cover +# nothing when run from a different directory. +ROOT_DIR = Path(__file__).resolve().parent.parent +TESTS_DIR = ROOT_DIR / "tests" / "components" + + +def main() -> int: + offenders: list[str] = [] + parse_errors: list[str] = [] + fixtures_scanned = 0 + + for fixture in sorted(TESTS_DIR.glob("*/*.yaml")): + # Isolated components are never merged with others, so dict form + # cannot clobber anyone there. + if fixture.parent.name in ISOLATED_COMPONENTS: + continue + try: + data = load_yaml_file(fixture) + except EsphomeError as err: + parse_errors.append(f"{fixture.relative_to(ROOT_DIR)}: {err}") + continue + fixtures_scanned += 1 + if not isinstance(data, dict): + continue + for key, value in data.items(): + if isinstance(value, dict) and "platform" in value: + offenders.append(f"{fixture.relative_to(ROOT_DIR)}: '{key}:'") + + if offenders: + print("Test fixtures with platform domains in single-dict form:\n") + for line in offenders: + print(f" - {line}") + print( + "\nWrite the domain as a one-element list ('- platform: ...') so " + "grouped CI builds can merge it with other components' entries; " + "in dict form it replaces or is replaced by their lists wholesale." + ) + + if parse_errors: + # A fixture we could not parse was never scanned, so the run is not a + # clean pass even if no offenders were found among the rest. + print( + f"\n{len(parse_errors)} test fixture(s) could not be parsed and " + "were not checked:" + ) + for line in parse_errors: + print(f" - {line}") + + if fixtures_scanned == 0: + # A scan that covered nothing is a false green -- the whole point of the + # guard is defeated. Fail loudly (wrong working directory or layout change). + print( + f"\nERROR: scanned 0 test fixtures under {TESTS_DIR}; " + "the guard covered nothing.", + file=sys.stderr, + ) + + if offenders or parse_errors or fixtures_scanned == 0: + return 1 + + print( + f"No single-dict platform domains found ({fixtures_scanned} fixtures scanned)." + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/ci_memory_impact_extract.py b/script/ci_memory_impact_extract.py index 20a737cdbf..2d74362169 100755 --- a/script/ci_memory_impact_extract.py +++ b/script/ci_memory_impact_extract.py @@ -33,6 +33,11 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) # pylint: disable=wrong-import-position from esphome.analyze_memory import MemoryAnalyzer +from esphome.analyze_memory.toolchain import ( + find_elf_path, + find_idedata_path, + idedata_candidates, +) from esphome.platformio.toolchain import IDEData from script.ci_helpers import write_github_output @@ -130,53 +135,31 @@ def run_detailed_analysis(build_dir: str) -> dict | None: print(f"Build directory not found: {build_dir}", file=sys.stderr) return None - # Find firmware.elf (or raw_firmware.elf for LibreTiny) - elf_path = None - for elf_candidate in [ - build_path / "firmware.elf", - build_path / ".pioenvs" / build_path.name / "firmware.elf", - # LibreTiny uses raw_firmware.elf - build_path / "raw_firmware.elf", - build_path / ".pioenvs" / build_path.name / "raw_firmware.elf", - ]: - if elf_candidate.exists(): - elf_path = str(elf_candidate) - break - + elf_path = find_elf_path(build_path) if not elf_path: - print( - f"firmware.elf/raw_firmware.elf not found in {build_dir}", file=sys.stderr - ) + print(f"No firmware ELF found in {build_dir}", file=sys.stderr) return None - # Find idedata.json - check multiple locations - device_name = build_path.name - idedata_candidates = [ - # In .pioenvs for test builds - build_path / ".pioenvs" / device_name / "idedata.json", - # In .esphome/idedata for regular builds - Path.home() / ".esphome" / "idedata" / f"{device_name}.json", - # Check parent directories for .esphome/idedata (for test_build_components) - build_path.parent.parent.parent / "idedata" / f"{device_name}.json", - ] - idedata = None - for idedata_path in idedata_candidates: - if not idedata_path.exists(): - continue + if idedata_path := find_idedata_path(build_path): try: with idedata_path.open(encoding="utf-8") as f: raw_data = json.load(f) idedata = IDEData(raw_data) print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) - break except (json.JSONDecodeError, OSError) as e: print( f"Warning: Failed to load idedata from {idedata_path}: {e}", file=sys.stderr, ) + else: + # Without idedata the analyzer falls back to whatever binutils are on + # PATH, which are the wrong architecture for a cross build, so say where + # we looked rather than let the results quietly get worse. + searched = "\n ".join(str(p) for p in idedata_candidates(build_path)) + print(f"Warning: idedata not found, searched:\n {searched}", file=sys.stderr) - analyzer = MemoryAnalyzer(elf_path, idedata=idedata) + analyzer = MemoryAnalyzer(str(elf_path), idedata=idedata) components = analyzer.analyze() # Convert to JSON-serializable format @@ -286,7 +269,7 @@ def main() -> int: if args.output_build_dir and build_dir: build_dir_path = Path(args.output_build_dir) build_dir_path.parent.mkdir(parents=True, exist_ok=True) - build_dir_path.write_text(build_dir) + build_dir_path.write_text(build_dir, encoding="utf-8") print(f"Wrote build directory to {args.output_build_dir}", file=sys.stderr) # Run detailed analysis if build directory available @@ -320,6 +303,19 @@ def main() -> int: else: print(f"{ram_bytes},{flash_bytes}") + # The build produced usable totals, so a missing detailed analysis means the + # build layout moved out from under this script rather than a broken build. + # Fail loudly: the comment would otherwise silently drop the component + # breakdown and the symbol tables, which is easy to miss for a long time. + if detailed_analysis is None: + print( + "::error::Detailed memory analysis unavailable even though the build " + f"succeeded (build directory: {build_dir or 'not detected'}). The PR " + "comment would be missing its component breakdown and symbol changes.", + file=sys.stderr, + ) + return 1 + return 0 diff --git a/script/clang-tidy b/script/clang-tidy index 1416b9b332..4f1bc6021c 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -29,7 +29,7 @@ from helpers import ( ) -def clang_options(idedata): +def clang_options(idedata, environment): cmd = [] # extract target architecture from triplet in g++ filename @@ -74,6 +74,8 @@ def clang_options(idedata): "-fno-jump-tables", "-fno-shrink-wrap", "-mno-target-align", + # GCC-only flag emitted by the LibreTiny build + "-mthumb-interwork", ) if "zephyr" in triplet: @@ -95,30 +97,59 @@ def clang_options(idedata): [ # disable built-in include directories from the host "-nostdinc", - # replace pgmspace.h, as it uses GNU extensions clang doesn't support - # https://github.com/earlephilhower/newlib-xtensa/pull/18 - "-D_PGMSPACE_H_", - "-Dpgm_read_byte(s)=(*(const uint8_t *)(s))", - "-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))", - "-Dpgm_read_word(s)=(*(const uint16_t *)(s))", - "-Dpgm_read_dword(s)=(*(const uint32_t *)(s))", - "-Dpgm_read_ptr(s)=(*(const void *const *)(s))", - "-DPROGMEM=", - "-DPGM_P=const char *", - "-DPSTR(s)=(s)", - # this next one is also needed with upstream pgmspace.h - # suppress warning about identifier naming in expansion of this macro - "-DPSTRN(s, n)=(s)", - # suppress warning about attribute cannot be applied to type - # https://github.com/esp8266/Arduino/pull/8258 - "-Ddeprecated(x)=", # allow to condition code on the presence of clang-tidy "-DCLANG_TIDY", # (esp-idf) Fix __once_callable in some libstdc++ headers "-D_GLIBCXX_HAVE_TLS", + # suppress warning about attribute cannot be applied to type + # https://github.com/esp8266/Arduino/pull/8258 + # also keeps deprecation diagnostics consistent across environments + "-Ddeprecated(x)=", ] ) + if environment.startswith("rp2"): + # clang's ARM backend doesn't know GCC's long_call attribute (IRAM_ATTR) + cmd.append("-Wno-unknown-attributes") + elif environment.startswith(("bk72xx", "ln882h", "rtl87xx")): + cmd.extend( + [ + # GCC on arm-none-eabi types (u)int32_t as (unsigned) long; clang + # types it as (unsigned) int, clashing with LibreTiny's lwip + # port typedefs. Match the GCC type model. + "-U__UINT32_TYPE__", + "-D__UINT32_TYPE__=long unsigned int", + "-U__INT32_TYPE__", + "-D__INT32_TYPE__=long int", + # newlib's machine/endian.h macroizes __bswap16 into + # __builtin_bswap16; the beken BDK then defines __bswap16 as a + # function, which GCC tolerates as a builtin redeclaration but + # clang rejects + "-D__MACHINE_ENDIAN_H__", + ] + ) + else: + # replace pgmspace.h, as it uses GNU extensions clang doesn't support + # https://github.com/earlephilhower/newlib-xtensa/pull/18 + # arduino-pico ships clang-parseable pgmspace inline functions, so the + # replacements are skipped there (they clash with those definitions). + cmd.extend( + [ + "-D_PGMSPACE_H_", + "-Dpgm_read_byte(s)=(*(const uint8_t *)(s))", + "-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))", + "-Dpgm_read_word(s)=(*(const uint16_t *)(s))", + "-Dpgm_read_dword(s)=(*(const uint32_t *)(s))", + "-Dpgm_read_ptr(s)=(*(const void *const *)(s))", + "-DPROGMEM=", + "-DPGM_P=const char *", + "-DPSTR(s)=(s)", + # this next one is also needed with upstream pgmspace.h + # suppress warning about identifier naming in expansion of this macro + "-DPSTRN(s, n)=(s)", + ] + ) + # Copy compiler flags, dropping: ones clang doesn't understand; -Werror* # (clang-tidy enforces .clang-tidy's WarningsAsErrors, and a build -Werror # would bypass the -clang-diagnostic-* suppressions); and -std= (the native @@ -142,17 +173,43 @@ def clang_options(idedata): ) cmd.append("-std=gnu++20") - # defines - cmd.extend(f"-D{define}" for define in idedata["defines"]) + if environment.startswith(("bk72xx", "ln882h", "rtl87xx")): + # LibreTiny leaves function-like macro values unparenthesized + # (bugprone-macro-parentheses); its SDK-internal FAL_PART_TABLE macro trips the same + # check. Assumes define values are always expressions (never type- or char-literal), + # which holds for the current LibreTiny idedata. + def sanitize_define(define): + name, sep, value = define.partition("=") + if ( + sep + and value + and not value.startswith(("(", '"')) + and ("(" in name or not re.fullmatch(r"[\w.]+", value)) + ): + value = f"({value})" + return f"-D{name}{sep}{value}" - # add toolchain include directories using -isystem to suppress their errors + # FAL_PART_TABLE and the delay() remap are library-scope LibreTiny flags that the real + # build never applies to esphome sources. Strip LibreTiny's shell quoting from define + # names first so the skip list matches regardless of which names it happens to quote. + for define in idedata["defines"]: + define = define.replace("'", "") + if define.startswith(("FAL_PART_TABLE", "delay(")): + continue + cmd.append(sanitize_define(define)) + else: + cmd.extend(f"-D{define}" for define in idedata["defines"]) + + # toolchain include directories, using -isystem to suppress their errors # idedata contains include directories for all toolchains of this platform, only use those from the one in use toolchain_dir = os.path.normpath(f"{idedata['cxx_path']}/../../") + toolchain_includes = [] for directory in idedata["includes"]["toolchain"]: if directory.startswith(toolchain_dir) and "picolibc" not in directory: - cmd.extend(["-isystem", directory]) + toolchain_includes.extend(["-isystem", directory]) - # add library include directories using -isystem to suppress their errors + # library include directories, using -isystem to suppress their errors + build_includes = [] for directory in list(idedata["includes"]["build"]): # skip our own directories, we add those later if ( @@ -166,7 +223,16 @@ def clang_options(idedata): ) or (directory.startswith(f"{root_path}") and "/.pio/" in directory) ): - cmd.extend(["-isystem", directory]) + build_includes.extend(["-isystem", directory]) + + if "zephyr" in triplet: + # Zephyr's POSIX layer shadows libc headers (sys/select.h, ...) with + # coherently-guarded versions; the real build searches the Zephyr + # include dirs before the toolchain's, and the shadowed headers clash + # (e.g. newlib's sigset_t vs Zephyr's) in the opposite order. + cmd.extend(build_includes + toolchain_includes) + else: + cmd.extend(toolchain_includes + build_includes) # add the esphome include directory using -I cmd.extend(["-I", root_path]) @@ -196,6 +262,16 @@ def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): if sys.stdout.isatty(): invocation.append("--use-color") + if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx")): + # MMIO peripheral access on these bare-metal platforms is all + # fixed-address. + # bugprone-pointer-arithmetic-on-polymorphic-object (and its + # cert-ctr56-cpp alias) crashes clang-tidy 22 with infinite matcher + # recursion on lvgl_esphome.h under the RP2 defines. + invocation.append( + "--checks=-clang-analyzer-core.FixedAddressDereference," + "-bugprone-pointer-arithmetic-on-polymorphic-object,-cert-ctr56-cpp" + ) invocation.append(f"--header-filter={Path(basepath).resolve()}/.*") invocation.append(str(Path(path).resolve())) invocation.append("--") @@ -340,7 +416,7 @@ def main(): # Load idedata and options only if we have files to check idedata = load_idedata(args.environment) - options = clang_options(idedata) + options = clang_options(idedata, args.environment) tmpdir = None if args.fix: @@ -407,7 +483,9 @@ def main(): print("Error applying fixes.\n", file=sys.stderr) raise - return len(failed_files) + # Cap at 255: shells truncate exit codes to one byte, so 256 failures + # would otherwise report success + return min(len(failed_files), 255) if __name__ == "__main__": diff --git a/script/clang_tidy_hash.py b/script/clang_tidy_hash.py index 00bcaf45b0..57ca90711c 100644 --- a/script/clang_tidy_hash.py +++ b/script/clang_tidy_hash.py @@ -21,6 +21,8 @@ CLANG_TIDY_GLOBAL_FILES = ( "platformio.ini", "requirements_dev.txt", "esphome/idf_component.yml", + "esphome/components/esp32/__init__.py", + "esphome/components/nrf52/__init__.py", ) # sdkconfig.defaults and per-target sdkconfig.defaults. files flip the diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 756f3884b8..061485c76c 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -160,7 +160,8 @@ class Platform(StrEnum): BK72XX_ARD = "bk72xx-ard" # LibreTiny BK7231N RTL87XX_ARD = "rtl87xx-ard" # LibreTiny RTL8720x LN882X_ARD = "ln882x-ard" # LibreTiny LN882x - RP2040_ARD = "rp2040-ard" # Raspberry Pi Pico + RP2040_ARD = "rp2040-ard" # RP2 family, RP2040 chip (Pico / Pico W) + RP2350_ARD = "rp2350-ard" # RP2 family, RP2350 chip (Pico 2 / Pico 2 W) NRF52_ZEPHYR = "nrf52-adafruit" # Nordic nRF52 (Zephyr) @@ -190,7 +191,8 @@ MEMORY_IMPACT_PLATFORM_PREFERENCE = [ Platform.BK72XX_ARD, # LibreTiny BK7231N Platform.RTL87XX_ARD, # LibreTiny RTL8720x Platform.LN882X_ARD, # LibreTiny LN882x - Platform.RP2040_ARD, # Raspberry Pi Pico + Platform.RP2040_ARD, # Raspberry Pi Pico (RP2040) + Platform.RP2350_ARD, # Raspberry Pi Pico 2 (RP2350) Platform.NRF52_ZEPHYR, # Nordic nRF52 (Zephyr) ] @@ -859,7 +861,8 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None: - *_libretiny.cpp, *_bk72*.* -> BK72XX (LibreTiny) - *_rtl87*.* -> RTL87XX (LibreTiny Realtek) - *_ln882*.* -> LN882X (LibreTiny Lightning) - - *_pico.cpp, *_rp2040.* -> RP2040_ARD + - *_rp2350*.*, *_pico2*.* -> RP2350_ARD (RP2 family, RP2350 chip) + - *_rp2040*.*, *_pico*.* -> RP2040_ARD (RP2 family, RP2040 chip) Args: filename: File path to check @@ -901,8 +904,14 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None: if "libretiny" in filename_lower or "bk72" in filename_lower: return Platform.BK72XX_ARD - # RP2040 / Raspberry Pi Pico - if "pico" in filename_lower or "rp2040" in filename_lower: + # RP2 family (Raspberry Pi Pico): explicit chip names only. Family- + # wide files (named ``_rp2.*``) are shared between RP2040 and RP2350 + # and intentionally don't preferentially route to either chip. + # Check the RP2350 patterns first since ``pico2`` substring-matches + # ``pico``. + if "rp2350" in filename_lower or "pico2" in filename_lower: + return Platform.RP2350_ARD + if "rp2040" in filename_lower or "pico" in filename_lower: return Platform.RP2040_ARD # nRF52 / Zephyr diff --git a/script/generate-rp2040-boards.py b/script/generate-rp2-boards.py similarity index 77% rename from script/generate-rp2040-boards.py rename to script/generate-rp2-boards.py index 1b4846fd2b..94a5cc018a 100755 --- a/script/generate-rp2040-boards.py +++ b/script/generate-rp2-boards.py @@ -8,14 +8,14 @@ import subprocess import sys import tempfile -from esphome.components.rp2040 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION -from esphome.components.rp2040.generate_boards import generate +from esphome.components.rp2 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION +from esphome.components.rp2.generate_boards import generate from esphome.helpers import write_file_if_changed ver = RECOMMENDED_ARDUINO_FRAMEWORK_VERSION version_tag: str = f"{ver.major}.{ver.minor}.{ver.patch}" root: Path = Path(__file__).parent.parent -boards_file_path: Path = root / "esphome" / "components" / "rp2040" / "boards.py" +boards_file_path: Path = root / "esphome" / "components" / "rp2" / "boards.py" def main(check: bool) -> None: @@ -42,10 +42,10 @@ def main(check: bool) -> None: if check: existing_content: str = boards_file_path.read_text(encoding="utf-8") if existing_content != content: - print("esphome/components/rp2040/boards.py is not up to date.") - print("Please run `script/generate-rp2040-boards.py`") + print("esphome/components/rp2/boards.py is not up to date.") + print("Please run `script/generate-rp2-boards.py`") sys.exit(1) - print("esphome/components/rp2040/boards.py is up to date") + print("esphome/components/rp2/boards.py is up to date") elif write_file_if_changed(boards_file_path, content): print("RP2040 boards updated successfully.") diff --git a/script/helpers.py b/script/helpers.py index 0086a00e85..6ba093b413 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -421,7 +421,10 @@ def _get_github_event_data() -> dict | None: """ github_event_path = os.environ.get("GITHUB_EVENT_PATH") if github_event_path and Path(github_event_path).exists(): - with Path(github_event_path).open() as f: + # The event payload is UTF-8 JSON; without an explicit encoding + # Windows decodes it as cp1252 and any non ASCII byte (an ellipsis in + # a commit title is enough) raises UnicodeDecodeError. + with Path(github_event_path).open(encoding="utf-8") as f: return json.load(f) return None diff --git a/script/helpers_zephyr.py b/script/helpers_zephyr.py index 66ef6ffc98..c26ad7f2cd 100644 --- a/script/helpers_zephyr.py +++ b/script/helpers_zephyr.py @@ -1,59 +1,32 @@ +"""Load clang-tidy idedata for the nrf52/Zephyr environment. + +The compile commands come from a configure-only build of a minimal Zephyr +project using the native sdk-nrf toolchain (see +``esphome.components.nrf52.clang_tidy``); this module extracts the include +paths, defines and compiler flags clang-tidy needs from them. +""" + import json +import os from pathlib import Path import re +import shlex import subprocess def load_idedata(environment, temp_folder, platformio_ini): - build_environment = environment.replace("-tidy", "") - build_dir = Path(temp_folder) / f"build-{build_environment}" - Path(build_dir).mkdir(exist_ok=True) - Path(build_dir / "platformio.ini").write_text( - Path(platformio_ini).read_text(encoding="utf-8"), encoding="utf-8" - ) - esphome_dir = Path(build_dir / "esphome") - esphome_dir.mkdir(exist_ok=True) - Path(esphome_dir / "main.cpp").write_text( - """ -#include -int main() { return 0;} -extern "C" void zboss_signal_handler() {}; -""", - encoding="utf-8", - ) - zephyr_dir = Path(build_dir / "zephyr") - zephyr_dir.mkdir(exist_ok=True) - Path(zephyr_dir / "prj.conf").write_text( - """ -CONFIG_NEWLIB_LIBC=y -CONFIG_BT=y -CONFIG_ADC=y -#mcumgr begin -CONFIG_NET_BUF=y -CONFIG_ZCBOR=y -CONFIG_MCUMGR=y -CONFIG_MCUMGR_GRP_IMG=y -CONFIG_IMG_MANAGER=y -CONFIG_STREAM_FLASH=y -CONFIG_FLASH_MAP=y -CONFIG_FLASH=y -CONFIG_IMG_ERASE_PROGRESSIVELY=y -CONFIG_BOOTLOADER_MCUBOOT=y -CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS=y -CONFIG_MCUMGR_GRP_IMG_STATUS_HOOKS=y -CONFIG_MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK=y -CONFIG_MCUMGR_TRANSPORT_UART=y -#mcumgr end -#zigbee begin -CONFIG_ZIGBEE=y -CONFIG_CRYPTO=y -CONFIG_NVS=y -CONFIG_SETTINGS=y -#zigbee end -""", - encoding="utf-8", - ) - subprocess.run(["pio", "run", "-e", build_environment, "-d", build_dir], check=True) + if explicit := os.environ.get("ESPHOME_ZEPHYR_COMPILE_COMMANDS"): + compile_commands_path = Path(explicit) + else: + from esphome.components.nrf52.clang_tidy import generate_compile_commands + + work_dir = (Path(temp_folder) / f"zephyr-{environment}").resolve() + compile_commands_path = generate_compile_commands( + work_dir, Path(platformio_ini) + ) + + if not compile_commands_path.is_file(): + raise RuntimeError(f"compile_commands.json not found: {compile_commands_path}") def extract_include_paths(command): include_paths = [] @@ -62,7 +35,7 @@ CONFIG_SETTINGS=y split_strings = re.split( r"\s*-\s*(?:I|isystem)", list(filter(lambda x: x, match))[0] ) - include_paths.append(split_strings[1]) + include_paths.append(split_strings[1].strip()) return include_paths def extract_defines(command): @@ -74,15 +47,6 @@ CONFIG_SETTINGS=y if not any(match.startswith(prefix) for prefix in ignore_prefixes) ] - def find_cxx_path(commands): - for entry in commands: - command = entry["command"] - cxx_path = command.split()[0] - if not cxx_path.endswith("++"): - continue - return cxx_path - return None - def get_builtin_include_paths(compiler): result = subprocess.run( [compiler, "-E", "-x", "c++", "-", "-v"], @@ -105,47 +69,48 @@ CONFIG_SETTINGS=y return include_paths def extract_cxx_flags(command): - # Extracts CXXFLAGS from the command string, excluding includes and defines. + # Extracts CXXFLAGS from the command string, excluding includes and + # defines. Anchored per token: a substring match would extract a bogus + # "-format-zero-length" from -Wno-format-zero-length. flag_pattern = re.compile( - r"(-O[0-3s]|-g|-std=[^\s]+|-Wall|-Wextra|-Werror|--[^\s]+|-f[^\s]+|-m[^\s]+|-imacros\s*[^\s]+)" + r"^(-O[0-3s]|-g|-std=.+|-Wall|-Wextra|-Werror|--.+|-f.+|-m.+|-imacros.+)$" ) - return [ - match.replace("-imacros ", "-imacros") - for match in flag_pattern.findall(command) - ] + flags = [] + tokens = shlex.split(command) + for i, token in enumerate(tokens): + if token == "-imacros" and i + 1 < len(tokens): + flags.append(f"-imacros{tokens[i + 1]}") + elif flag_pattern.match(token): + flags.append(token) + return flags def transform_to_idedata_format(compile_commands): - cxx_path = find_cxx_path(compile_commands) - idedata = { + # Use only the tidy app TU (main.cpp): as the app target, its compile + # command already carries the full Zephyr include set. Unioning every + # TU instead would drag in per-library internal include dirs (e.g. the + # Zephyr POSIX shim, whose signal.h redefines newlib's sigset_t) that + # no ESPHome source compiles against. + entry = next( + (e for e in compile_commands if e["file"].endswith("main.cpp")), None + ) + if entry is None: + raise RuntimeError("tidy main.cpp not found in compile_commands.json") + command = entry["command"] + # Find the compiler by name: the command may be prefixed with a + # launcher (Zephyr auto-enables ccache when present). + cxx_path = next((t for t in shlex.split(command) if t.endswith("++")), None) + if cxx_path is None: + raise RuntimeError(f"no C++ compiler in compile command: {command}") + + return { "includes": { "toolchain": get_builtin_include_paths(cxx_path), - "build": set(), + "build": extract_include_paths(command), }, - "defines": set(), + "defines": extract_defines(command), "cxx_path": cxx_path, - "cxx_flags": set(), + "cxx_flags": extract_cxx_flags(command), } - for entry in compile_commands: - command = entry["command"] - exec = command.split()[0] - if exec != cxx_path: - continue - - idedata["includes"]["build"].update(extract_include_paths(command)) - idedata["defines"].update(extract_defines(command)) - idedata["cxx_flags"].update(extract_cxx_flags(command)) - - # Convert sets to lists for JSON serialization - idedata["includes"]["build"] = list(idedata["includes"]["build"]) - idedata["defines"] = list(idedata["defines"]) - idedata["cxx_flags"] = list(idedata["cxx_flags"]) - - return idedata - - compile_commands = json.loads( - Path( - build_dir / ".pio" / "build" / build_environment / "compile_commands.json" - ).read_text(encoding="utf-8") - ) + compile_commands = json.loads(compile_commands_path.read_text(encoding="utf-8")) return transform_to_idedata_format(compile_commands) diff --git a/script/import_time_budget.json b/script/import_time_budget.json index 855d89c56d..cda426bb3e 100644 --- a/script/import_time_budget.json +++ b/script/import_time_budget.json @@ -1,5 +1,5 @@ { "target_module": "esphome.__main__", "margin_pct": 20, - "cumulative_us": 91000 + "cumulative_us": 37200 } diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 5eeeafac2a..c2be7be7fd 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -263,22 +263,39 @@ def prepare_component_body(comp_data: dict, comp_name: str, comp_dir: Path) -> d else {} ) - packages_value = comp_data.get("packages") - if isinstance(packages_value, dict): - common_bus_packages = get_common_bus_packages() - for pkg_name, pkg_value in list(packages_value.items()): - if pkg_name in common_bus_packages: - continue - if isinstance(pkg_value, yaml_util.IncludeFile): - pkg_value = pkg_value.load() - if isinstance(pkg_value, dict): - comp_data = merge_config(comp_data, pkg_value) - elif isinstance(packages_value, list): - for pkg_value in packages_value: - if isinstance(pkg_value, yaml_util.IncludeFile): - pkg_value = pkg_value.load() - if isinstance(pkg_value, dict): - comp_data = merge_config(comp_data, pkg_value) + # Expand component-specific package includes inline. A package include may + # itself pull in further component-specific packages (e.g. web_server's test + # includes common_v2, which includes common with the wifi/network config), so + # keep expanding until only common bus packages remain -- otherwise the nested + # includes are silently dropped when the packages key is removed below. + common_bus_packages = get_common_bus_packages() + while True: + packages_value = comp_data.get("packages") + expanded = False + if isinstance(packages_value, dict): + for pkg_name, pkg_value in list(packages_value.items()): + if pkg_name in common_bus_packages: + continue + # Drop before merging so a nested packages dict introduced by the + # include does not re-add this same key on the next iteration. + del packages_value[pkg_name] + if isinstance(pkg_value, yaml_util.IncludeFile): + pkg_value = pkg_value.load() + if isinstance(pkg_value, dict): + comp_data = merge_config(comp_data, pkg_value) + expanded = True + elif isinstance(packages_value, list): + # List-style packages never contain common bus packages, so expand + # them all and drop the key entirely. + comp_data.pop("packages", None) + for pkg_value in packages_value: + if isinstance(pkg_value, yaml_util.IncludeFile): + pkg_value = pkg_value.load() + if isinstance(pkg_value, dict): + comp_data = merge_config(comp_data, pkg_value) + expanded = True + if not expanded: + break # Common bus packages are re-added once by the caller; drop them here. comp_data.pop("packages", None) diff --git a/script/setup_codspeed_lib.py b/script/setup_codspeed_lib.py index 4f5d1bff24..9ddfee27cc 100755 --- a/script/setup_codspeed_lib.py +++ b/script/setup_codspeed_lib.py @@ -84,7 +84,7 @@ def _read_codspeed_version(cmake_path: Path) -> str: """Extract CODSPEED_VERSION from core/CMakeLists.txt.""" if not cmake_path.exists(): return "0.0.0" - for line in cmake_path.read_text().splitlines(): + for line in cmake_path.read_text(encoding="utf-8").splitlines(): if line.startswith("set(CODSPEED_VERSION"): return line.split()[1].rstrip(")") return "0.0.0" diff --git a/script/test_build_components.py b/script/test_build_components.py index ce2a35add3..ddd8a6a67d 100755 --- a/script/test_build_components.py +++ b/script/test_build_components.py @@ -88,6 +88,38 @@ def show_disk_space_if_ci(esphome_command: str) -> None: sys.stdout.flush() +def start_log_group(title: str) -> None: + """Begin a collapsible log group in the GitHub Actions log viewer. + + Everything printed until the matching :func:`end_log_group` is folded away + by default, so the full ``esphome config``/``compile`` dump for one + configuration no longer pushes the pass/fail result thousands of lines down + the log. Outside CI this is a no-op so local runs stay plain. + + Args: + title: Text shown on the (collapsed) group header line. + """ + if not os.environ.get("GITHUB_ACTIONS"): + return + # Flush so the marker is ordered correctly relative to the child process + # output that follows (the subprocess writes straight to our stdout). + sys.stdout.flush() + print(f"::group::{title}") + sys.stdout.flush() + + +def end_log_group() -> None: + """Close the collapsible log group opened by :func:`start_log_group`. + + Outside CI this is a no-op. + """ + if not os.environ.get("GITHUB_ACTIONS"): + return + sys.stdout.flush() + print("::endgroup::") + sys.stdout.flush() + + def find_component_tests( components_dir: Path, component_pattern: str = "*", @@ -335,7 +367,7 @@ def run_esphome_test( output_file = build_dir / f"{component}.{test_name}.{platform_with_version}.yaml" # Copy base file and substitute component test file reference - base_content = base_file.read_text() + base_content = base_file.read_text(encoding="utf-8") # Get relative path from build dir to test file repo_root = Path(__file__).parent.parent component_test_ref = f"../../{test_file.relative_to(repo_root / 'tests')}" @@ -383,54 +415,48 @@ def run_esphome_test( # Build command string for display/logging cmd_str = " ".join(cmd) - # Run command - print(f"> [{component}] [{test_name}] [{platform_with_version}]") + # Run command inside a collapsible CI log group so the full esphome output + # for this configuration can be folded away by default. + group_title = f"[{component}] [{test_name}] [{platform_with_version}]" + start_log_group(group_title) + print(f"> {group_title}") if use_testing_mode: print(" (using --testing-mode)") start_time = time.time() test_id = f"{component}.{test_name}.{platform_with_version}" + # Always close the group, even if the subprocess or disk-space reporting + # raises, so later output is never folded into the wrong CI log section. try: result = subprocess.run(cmd, check=False) - success = result.returncode == 0 - duration = time.time() - start_time - # Show disk space after build in CI during compile show_disk_space_if_ci(esphome_command) + finally: + end_log_group() - if not success and not continue_on_fail: - # Print command immediately for failed tests - print(f"\n{'=' * 80}") - print("FAILED - Command to reproduce:") - print(f"{'=' * 80}") - print(cmd_str) - print() - raise subprocess.CalledProcessError(result.returncode, cmd) + success = result.returncode == 0 + duration = time.time() - start_time - return TestResult( - test_id=test_id, - components=[component], - platform=platform_with_version, - success=success, - duration=duration, - command=cmd_str, - test_type=esphome_command, - ) - except subprocess.CalledProcessError: - duration = time.time() - start_time - # Re-raise if we're not continuing on fail - if not continue_on_fail: - raise - return TestResult( - test_id=test_id, - components=[component], - platform=platform_with_version, - success=False, - duration=duration, - command=cmd_str, - test_type=esphome_command, - ) + if not success and not continue_on_fail: + # Print command immediately for failed tests. The group is already + # closed, so the failure and reproduce command stay visible. + print(f"\n{'=' * 80}") + print("FAILED - Command to reproduce:") + print(f"{'=' * 80}") + print(cmd_str) + print() + raise subprocess.CalledProcessError(result.returncode, cmd) + + return TestResult( + test_id=test_id, + components=[component], + platform=platform_with_version, + success=success, + duration=duration, + command=cmd_str, + test_type=esphome_command, + ) def run_grouped_test( @@ -498,7 +524,7 @@ def run_grouped_test( # Create test file that includes merged config output_file = build_dir / f"test_{group_name}.{platform_with_version}.yaml" - base_content = base_file.read_text() + base_content = base_file.read_text(encoding="utf-8") merged_ref = merged_config_file.name output_content = base_content.replace("$component_test_file", merged_ref) output_file.write_text(output_content) @@ -534,54 +560,48 @@ def run_grouped_test( # Build command string for display/logging cmd_str = " ".join(cmd) - # Run command + # Run command inside a collapsible CI log group so the full esphome output + # for this grouped configuration can be folded away by default. components_str = ", ".join(components) - print(f"> [GROUPED: {components_str}] [{platform_with_version}]") + group_title = f"[GROUPED: {components_str}] [{platform_with_version}]" + start_log_group(group_title) + print(f"> {group_title}") print(" (using --testing-mode)") start_time = time.time() test_id = f"GROUPED[{','.join(components)}].{platform_with_version}" + # Always close the group, even if the subprocess or disk-space reporting + # raises, so later output is never folded into the wrong CI log section. try: result = subprocess.run(cmd, check=False) - success = result.returncode == 0 - duration = time.time() - start_time - # Show disk space after build in CI during compile show_disk_space_if_ci(esphome_command) + finally: + end_log_group() - if not success and not continue_on_fail: - # Print command immediately for failed tests - print(f"\n{'=' * 80}") - print("FAILED - Command to reproduce:") - print(f"{'=' * 80}") - print(cmd_str) - print() - raise subprocess.CalledProcessError(result.returncode, cmd) + success = result.returncode == 0 + duration = time.time() - start_time - return TestResult( - test_id=test_id, - components=components, - platform=platform_with_version, - success=success, - duration=duration, - command=cmd_str, - test_type=esphome_command, - ) - except subprocess.CalledProcessError: - duration = time.time() - start_time - # Re-raise if we're not continuing on fail - if not continue_on_fail: - raise - return TestResult( - test_id=test_id, - components=components, - platform=platform_with_version, - success=False, - duration=duration, - command=cmd_str, - test_type=esphome_command, - ) + if not success and not continue_on_fail: + # Print command immediately for failed tests. The group is already + # closed, so the failure and reproduce command stay visible. + print(f"\n{'=' * 80}") + print("FAILED - Command to reproduce:") + print(f"{'=' * 80}") + print(cmd_str) + print() + raise subprocess.CalledProcessError(result.returncode, cmd) + + return TestResult( + test_id=test_id, + components=components, + platform=platform_with_version, + success=success, + duration=duration, + command=cmd_str, + test_type=esphome_command, + ) def run_grouped_component_tests( diff --git a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h index bab27549e7..d8b068fb36 100644 --- a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h +++ b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h @@ -32,9 +32,10 @@ class SerialProxy { api::enums::SerialProxyPortType get_port_type() const { return {}; } api::APIConnection *get_api_connection() { return nullptr; } void serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) {} - void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint32_t stop_bits, uint32_t data_size) {} - void write_from_client(const uint8_t *data, size_t len) {} - void set_modem_pins(uint32_t line_states) {} + void configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, + uint32_t stop_bits, uint32_t data_size) {} + void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {} + void set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {} uint32_t get_modem_pins() const { return 0; } uart::UARTFlushResult flush_port() { return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } diff --git a/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h b/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h index ba97e81236..b4ccd8fd00 100644 --- a/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h +++ b/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h @@ -16,7 +16,7 @@ class ZWaveProxy { public: api::APIConnection *get_api_connection() { return nullptr; } void zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) {} - void send_frame(const uint8_t *data, size_t length) {} + void send_frame(api::APIConnection *api_connection, const uint8_t *data, size_t length) {} void api_connection_authenticated(api::APIConnection *conn) {} uint32_t get_feature_flags() const { return 0; } uint32_t get_home_id() { return 0; } diff --git a/tests/component_tests/animation/__init__.py b/tests/component_tests/animation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/animation/config/anim.apng b/tests/component_tests/animation/config/anim.apng new file mode 100644 index 0000000000..927af5eb05 Binary files /dev/null and b/tests/component_tests/animation/config/anim.apng differ diff --git a/tests/component_tests/animation/config/anim.gif b/tests/component_tests/animation/config/anim.gif new file mode 100644 index 0000000000..9932e77448 Binary files /dev/null and b/tests/component_tests/animation/config/anim.gif differ diff --git a/tests/component_tests/animation/config/animation_platform_test.yaml b/tests/component_tests/animation/config/animation_platform_test.yaml new file mode 100644 index 0000000000..8de32ed593 --- /dev/null +++ b/tests/component_tests/animation/config/animation_platform_test.yaml @@ -0,0 +1,33 @@ +# New `image:` `platform: animation` form. Exercises animation/image.py through +# the real platform loader and codegen pipeline. +esphome: + name: test + +esp32: + board: esp32s3box + +image: + - platform: animation + id: test_animation + file: anim.gif + type: rgb565 + loop: + start_frame: 0 + end_frame: 2 + repeat: 3 + - platform: animation + id: test_animation_no_loop + file: anim.gif + type: rgb565 + +spi: + mosi_pin: 6 + clk_pin: 7 + +psram: + mode: quad + +display: + - platform: mipi_spi + id: lcd_display + model: s3box diff --git a/tests/component_tests/animation/config/animation_test.yaml b/tests/component_tests/animation/config/animation_test.yaml new file mode 100644 index 0000000000..1fe6ddf9a4 --- /dev/null +++ b/tests/component_tests/animation/config/animation_test.yaml @@ -0,0 +1,28 @@ +# Legacy top-level `animation:` form. Exercises the deprecation shim and the +# shared codegen path through the real read_config/codegen pipeline. +esphome: + name: test + +esp32: + board: esp32s3box + +animation: + - id: test_animation + file: anim.gif + type: rgb565 + loop: + start_frame: 0 + end_frame: 2 + repeat: 3 + +spi: + mosi_pin: 6 + clk_pin: 7 + +psram: + mode: quad + +display: + - platform: mipi_spi + id: lcd_display + model: s3box diff --git a/tests/component_tests/animation/test_init.py b/tests/component_tests/animation/test_init.py new file mode 100644 index 0000000000..1b5dd0d54c --- /dev/null +++ b/tests/component_tests/animation/test_init.py @@ -0,0 +1,81 @@ +"""Tests for the animation image platform and the legacy `animation:` shim.""" + +from __future__ import annotations + +from collections.abc import Callable +import logging +from pathlib import Path + +import pytest + +from esphome.components.animation import ( + DOMAIN, + LEGACY_REMOVAL_VERSION, + _capture_legacy_entry, + _warn_legacy_animation, +) +from esphome.core import CORE +from esphome.types import ConfigType + +# --------------------------------------------------------------------------- +# Legacy top-level `animation:` deprecation shim -- REMOVE these tests after +# 2027.1.0 together with the shim in esphome/components/animation/__init__.py. +# --------------------------------------------------------------------------- + + +def test_warn_legacy_animation_warns_once( + caplog: pytest.LogCaptureFixture, +) -> None: + """The deprecation warning fires exactly once and never mutates the config.""" + config: ConfigType = {"id": "test_animation", "file": "anim.gif", "type": "rgb565"} + + # A per-entry capture (CONFIG_SCHEMA step) records the raw entry so the + # one-shot warning can print a pasteable migrated block. + assert _capture_legacy_entry(config) is config + + with caplog.at_level(logging.WARNING): + # First call: flag not yet set -> warns and records the flag. + assert _warn_legacy_animation(config) is config + # Second call: flag already set -> stays silent (the dedup branch). + assert _warn_legacy_animation(config) is config + + assert CORE.data[DOMAIN]["legacy_warning_shown"] is True + warnings = [r for r in caplog.records if r.levelno == logging.WARNING] + assert len(warnings) == 1 + assert "deprecated" in caplog.text + assert "platform: animation" in caplog.text + assert LEGACY_REMOVAL_VERSION in caplog.text + + +def test_legacy_animation_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + """The legacy `animation:` block validates, warns, and generates codegen + through the real read_config/codegen pipeline.""" + with caplog.at_level(logging.WARNING): + main_cpp = generate_main(component_config_path("animation_test.yaml")) + + # Deprecation warning surfaced through the real validation pipeline. + assert "animation" in caplog.text + assert "deprecated" in caplog.text + + # setup_animation ran: Animation object constructed and loop configured. + assert "new(test_animation) animation::Animation(" in main_cpp + assert "test_animation->set_loop(0, 2, 3);" in main_cpp + + +def test_animation_platform_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """The `image:` `platform: animation` form generates codegen through the + real platform loader (animation/image.py) without any deprecation warning.""" + main_cpp = generate_main(component_config_path("animation_platform_test.yaml")) + + assert "new(test_animation) animation::Animation(" in main_cpp + assert "test_animation->set_loop(0, 2, 3);" in main_cpp + # The loop-less entry constructs the object but never configures a loop. + assert "new(test_animation_no_loop) animation::Animation(" in main_cpp + assert "test_animation_no_loop->set_loop(" not in main_cpp diff --git a/tests/component_tests/aqi/__init__.py b/tests/component_tests/aqi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/aqi/test_aqi.py b/tests/component_tests/aqi/test_aqi.py new file mode 100644 index 0000000000..712c277508 --- /dev/null +++ b/tests/component_tests/aqi/test_aqi.py @@ -0,0 +1,35 @@ +"""Config-validation tests for the aqi sensor component.""" + +import pytest +from voluptuous import Invalid + +from esphome.components.aqi import CONF_CALCULATION_TYPE, CONF_EXTENDED_RANGE +from esphome.components.aqi.sensor import _validate_extended_range + + +def test_extended_range_rejected_with_caqi(): + """extended_range has no meaning for CAQI (no spec maximum) and must be rejected.""" + with pytest.raises(Invalid, match="CAQI"): + _validate_extended_range( + {CONF_CALCULATION_TYPE: "CAQI", CONF_EXTENDED_RANGE: True} + ) + + +def test_extended_range_rejected_with_caqi_even_when_false(): + """The option is not allowed at all with CAQI, regardless of its value.""" + with pytest.raises(Invalid, match="CAQI"): + _validate_extended_range( + {CONF_CALCULATION_TYPE: "CAQI", CONF_EXTENDED_RANGE: False} + ) + + +def test_extended_range_allowed_with_aqi(): + """extended_range is valid for the US AQI calculation.""" + config = {CONF_CALCULATION_TYPE: "AQI", CONF_EXTENDED_RANGE: True} + assert _validate_extended_range(config) is config + + +def test_caqi_without_extended_range_ok(): + """CAQI is fine as long as extended_range is not set.""" + config = {CONF_CALCULATION_TYPE: "CAQI"} + assert _validate_extended_range(config) is config diff --git a/tests/component_tests/bk72xx_ble_tracker/test_scan_parameter_validation.py b/tests/component_tests/bk72xx_ble_tracker/test_scan_parameter_validation.py new file mode 100644 index 0000000000..968a24dad5 --- /dev/null +++ b/tests/component_tests/bk72xx_ble_tracker/test_scan_parameter_validation.py @@ -0,0 +1,114 @@ +"""Tests for bk72xx_ble_tracker scan parameter validation.""" + +from __future__ import annotations + +import pytest + +from esphome import config_validation as cv +from esphome.components.bk72xx_ble_tracker import SCAN_PARAMETERS_SCHEMA, to_ble_units + + +def _validate(**kwargs: str) -> dict: + """Run a scan_parameters config through the schema, applying defaults.""" + return SCAN_PARAMETERS_SCHEMA(dict(kwargs)) + + +# --- to_ble_units --- + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("2500us", 4), # controller minimum, 2.5 ms + ("30ms", 48), + ("100ms", 160), + ("10240ms", 16384), # controller maximum, 0x4000 + ], +) +def test_to_ble_units_converts_to_controller_units(value: str, expected: int) -> None: + """A time is converted to whole 0.625 ms units.""" + assert to_ble_units(cv.positive_time_period(value)) == expected + + +def test_to_ble_units_truncates() -> None: + """Sub-unit remainders are dropped, which is what makes collapse possible.""" + assert to_ble_units(cv.positive_time_period("3000us")) == 4 + assert to_ble_units(cv.positive_time_period("2500us")) == 4 + + +# --- accepted configurations --- + + +def test_defaults_are_valid() -> None: + """The documented default 100 ms / 30 ms pair validates.""" + config = _validate() + assert to_ble_units(config["interval"]) == 160 + assert to_ble_units(config["window"]) == 48 + + +def test_minimum_separation_accepted() -> None: + """Values one unit apart at the 2.5 ms floor are honest, not collapsed.""" + config = _validate(interval="5000us", window="2500us") + assert to_ble_units(config["interval"]) == 8 + assert to_ble_units(config["window"]) == 4 + + +def test_maximum_interval_accepted() -> None: + """The documented 10240 ms ceiling is inclusive, and maps to 0x4000. + + Pins the ceiling from the accept side, mirroring the 2.5 ms floor above: the + reject cases alone would let the bound silently become exclusive. + """ + config = _validate(interval="10240ms", window="30ms") + assert to_ble_units(config["interval"]) == 16384 + + +def test_maximum_window_accepted() -> None: + """The ceiling applies to the window too, and is likewise inclusive.""" + config = _validate(interval="10240ms", window="10240ms") + assert to_ble_units(config["window"]) == 16384 + + +def test_window_equal_to_interval_accepted() -> None: + """A deliberate 100 % duty cycle is allowed; only an accidental one is not.""" + config = _validate(interval="100ms", window="100ms") + assert to_ble_units(config["interval"]) == to_ble_units(config["window"]) + + +# --- rejected configurations --- + + +def test_window_larger_than_interval_rejected() -> None: + with pytest.raises(cv.Invalid, match="needs to be smaller than scan interval"): + _validate(interval="30ms", window="100ms") + + +@pytest.mark.parametrize( + ("interval", "window", "offender"), + [ + ("2ms", "1ms", "interval"), # below the 2.5 ms controller floor + ("20s", "1s", "interval"), # above the 10240 ms controller ceiling + ("100ms", "1ms", "window"), # window below the floor + ], +) +def test_out_of_range_rejected(interval: str, window: str, offender: str) -> None: + """Values the controller cannot represent are rejected, not silently wrapped.""" + with pytest.raises( + cv.Invalid, match=f"Scan {offender} .* must be between 2.5 ms and 10240 ms" + ): + _validate(interval=interval, window=window) + + +def test_unit_collapse_rejected() -> None: + """Regression: 3000us/2500us both floor to 4 units — a hidden 100 % duty cycle. + + This is the configuration that previously validated and programmed the radio + permanently on despite asking for roughly 83 %. + """ + with pytest.raises(cv.Invalid, match="both round to 4 x 0.625 ms"): + _validate(interval="3000us", window="2500us") + + +def test_duration_shorter_than_three_intervals_rejected() -> None: + with pytest.raises(cv.Invalid, match="must cover at least three scan intervals"): + _validate(duration="1s", interval="500ms", window="100ms") diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 84128d75d7..f105ed5888 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -33,6 +33,43 @@ def test_deep_sleep_run_duration_simple(generate_main): assert "deepsleep->set_run_duration(10000);" in main_cpp +def test_deep_sleep_on_wake_trigger(generate_main): + """ + When deep sleep is configured with a component-level on_wake automation, + a WakeTrigger component should be registered with the wakeup cause as + the automation argument. + """ + main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep3.yaml") + + assert "deep_sleep::WakeTrigger();" in main_cpp + assert "Automation" in main_cpp + + +def test_deep_sleep_ext1_on_wake_triggers(generate_main): + """ + Each esp32_ext1_wakeup pin with an on_wake automation should get its own + Ext1WakeTrigger with the pin number, and all pins (including the legacy + bare-pin shorthand) should contribute to the ext1 wakeup mask. + """ + main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep3.yaml") + + assert "deep_sleep::Ext1WakeTrigger(2);" in main_cpp + assert "deep_sleep::Ext1WakeTrigger(4);" in main_cpp + # GPIO13 has no on_wake, so no trigger is created for it + assert "deep_sleep::Ext1WakeTrigger(13)" not in main_cpp + # mask covers GPIO2, GPIO4 and GPIO13 + assert ".mask = 8212," in main_cpp + + +def test_deep_sleep_no_on_wake_no_triggers(generate_main): + """ + Without any on_wake automations, no wake trigger code should be generated. + """ + main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") + + assert "WakeTrigger" not in main_cpp + + def test_deep_sleep_run_duration_dictionary(generate_main): """ When deep sleep is configured with dictionary run duration, it should be set. diff --git a/tests/component_tests/deep_sleep/test_deep_sleep3.yaml b/tests/component_tests/deep_sleep/test_deep_sleep3.yaml new file mode 100644 index 0000000000..71a0340b65 --- /dev/null +++ b/tests/component_tests/deep_sleep/test_deep_sleep3.yaml @@ -0,0 +1,23 @@ +esphome: + name: test + +esp32: + board: nodemcu-32s + +deep_sleep: + id: deepsleep + sleep_duration: 1min + run_duration: 10s + on_wake: + - lambda: 'ESP_LOGD("test", "cause %d", static_cast(cause));' + esp32_ext1_wakeup: + mode: ANY_HIGH + pins: + - pin: GPIO2 + on_wake: + - lambda: 'ESP_LOGD("test", "left");' + - pin: + number: GPIO4 + on_wake: + - lambda: 'ESP_LOGD("test", "right");' + - number: GPIO13 diff --git a/tests/component_tests/epaper_spi/config/t133a01_no_init_sequence.yaml b/tests/component_tests/epaper_spi/config/t133a01_no_init_sequence.yaml new file mode 100644 index 0000000000..1032927146 --- /dev/null +++ b/tests/component_tests/epaper_spi/config/t133a01_no_init_sequence.yaml @@ -0,0 +1,26 @@ +esphome: + name: test + +esp32: + board: esp32-s3-devkitc-1 + variant: esp32s3 + framework: + type: esp-idf + +spi: + clk_pin: GPIO18 + mosi_pin: GPIO19 + +display: + - platform: epaper_spi + id: epaper_display + model: t133a01 + dc_pin: GPIO21 + reset_pin: GPIO38 + cs_pin: GPIO10 + cs1_pin: GPIO2 + busy_pin: GPIO13 + update_interval: never + dimensions: + width: 200 + height: 200 diff --git a/tests/component_tests/epaper_spi/test_init.py b/tests/component_tests/epaper_spi/test_init.py index c7f34d7dd2..7a0507542e 100644 --- a/tests/component_tests/epaper_spi/test_init.py +++ b/tests/component_tests/epaper_spi/test_init.py @@ -154,6 +154,10 @@ def test_all_predefined_models( if not model.get_default(CONF_CS_PIN): config[CONF_CS_PIN] = 5 + # Dual-CS models (e.g. T133A01) require a second chip-select pin + if model.manages_cs and not model.get_default("cs1_pin"): + config["cs1_pin"] = 4 + # Select an ESP32 variant on which all of this model's pins are valid # (some models default to high-numbered pins only present on the S3). choose_variant_with_pins(_pins_for(model, config)) @@ -204,6 +208,10 @@ def test_individual_models( if not model.get_default(CONF_CS_PIN): config[CONF_CS_PIN] = 5 + # Dual-CS models (e.g. T133A01) require a second chip-select pin + if model.manages_cs and not model.get_default("cs1_pin"): + config["cs1_pin"] = 4 + # Select an ESP32 variant on which all of this model's pins are valid # (some models default to high-numbered pins only present on the S3). choose_variant_with_pins(_pins_for(model, config)) @@ -454,3 +462,24 @@ def test_enable_pin_code_generation( # Both pin objects must be passed to the display via set_enable_pins() as a # std::vector initializer list, in the configured order. assert f"set_enable_pins({{{pin_25}, {pin_26}}});" in main_cpp + + +def test_model_with_no_default_init_sequence_generates( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that code generation succeeds for a model with no default init sequence. + + The base "t133a01" model (used directly, not via one of its `.extend()` + variants) doesn't override `get_init_sequence()` or pass `initsequence` to + its constructor, and the user didn't supply `init_sequence:` either. + `EpaperModel.get_init_sequence()` used to default to `None` in this case, + which made `flatten_sequence()` raise a `TypeError` during code + generation. Regression test for that crash. + """ + main_cpp = generate_main(component_config_path("t133a01_no_init_sequence.yaml")) + + # The generated constructor call takes (name, width, height, init_sequence, + # init_sequence_length, ...); a length of 0 confirms the empty init + # sequence array was generated instead of raising during code generation. + assert re.search(r"epaper_spi::EPaperT133A01\([^;]*,\s*\w+,\s*0\);", main_cpp) diff --git a/tests/component_tests/esp32/config/network_wifi_ethernet_priority.yaml b/tests/component_tests/esp32/config/network_wifi_ethernet_priority.yaml new file mode 100644 index 0000000000..d98d19f545 --- /dev/null +++ b/tests/component_tests/esp32/config/network_wifi_ethernet_priority.yaml @@ -0,0 +1,26 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +ethernet: + type: W5500 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + +network: + priority: + - ethernet + - wifi diff --git a/tests/component_tests/esp32/config/nvs_encryption_s3.yaml b/tests/component_tests/esp32/config/nvs_encryption_s3.yaml new file mode 100644 index 0000000000..371f2e28ca --- /dev/null +++ b/tests/component_tests/esp32/config/nvs_encryption_s3.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + nvs_encryption: + key_id: 0 diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index cea34bef7c..c374e7c964 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -13,6 +13,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32, VARIANTS, NetworkSdkconfigData, + _ota_downgrade_protection_errors, _reconcile_network_sdkconfig, ) from esphome.components.esp32.const import ( @@ -107,6 +108,24 @@ def test_esp32_default_toolchain_is_esp_idf( assert CORE.toolchain == expected +@pytest.mark.parametrize( + "config_toolchain", + [Toolchain.SDK_NRF.value, "nonsense"], +) +def test_esp32_rejects_unsupported_toolchains( + set_core_config: SetCoreConfigCallable, + config_toolchain: str, +) -> None: + """Toolchains esp32 does not support are rejected at validation time.""" + set_core_config(PlatformFramework.ESP32_IDF) + + from esphome.components.esp32 import CONFIG_SCHEMA + + CORE.toolchain = None + with pytest.raises(cv.Invalid, match="Unknown value"): + CONFIG_SCHEMA({"variant": VARIANT_ESP32, "toolchain": config_toolchain}) + + @pytest.mark.parametrize( ("config", "error_match"), [ @@ -174,6 +193,29 @@ def test_esp32_default_toolchain_is_esp_idf( r"'ignore_efuse_mac_crc' is not supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['ignore_efuse_mac_crc'\]", id="ignore_efuse_mac_crc_only_on_esp32", ), + pytest.param( + { + "variant": "esp32", + "board": "esp32dev", + "framework": { + "type": "esp-idf", + "advanced": {"nvs_encryption": {"key_id": 0}}, + }, + }, + r"NVS encryption \(HMAC scheme\) is not supported on ESP32 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]", + id="nvs_encryption_unsupported_on_esp32", + ), + pytest.param( + { + "variant": "esp32s3", + "framework": { + "type": "esp-idf", + "advanced": {"nvs_encryption": {"key_id": 6}}, + }, + }, + r"value must be at most 5 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]\['key_id'\]", + id="nvs_encryption_key_id_out_of_range", + ), ], ) def test_esp32_configuration_errors( @@ -213,6 +255,21 @@ def test_execute_from_psram_p4_sdkconfig( assert "CONFIG_SPIRAM_RODATA" not in sdkconfig +def test_nvs_encryption_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that nvs_encryption sets the HMAC scheme sdkconfig options.""" + generate_main(component_config_path("nvs_encryption_s3.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_NVS_ENCRYPTION") is True + assert sdkconfig.get("CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC") is True + assert sdkconfig.get("CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID") == 0 + # The permanent/irreversible eFuse burn is warned about at config time. + assert "PERMANENT and IRREVERSIBLE" in caplog.text + + @pytest.mark.parametrize( ("fixture", "expect_warning"), [ @@ -560,3 +617,186 @@ def test_network_wifi_ble_coexistence_reconciles_end_to_end( assert sdkconfig.get("CONFIG_LWIP_DHCPS") is False # WiFi present alongside BT -> WiFi stack must stay enabled. assert "CONFIG_ESP_WIFI_ENABLED" not in sdkconfig + + +def test_network_wifi_ethernet_priority_keeps_wifi_enabled( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """End-to-end: with both WiFi and Ethernet declared under network: priority:, + the reconciler must NOT disable the WiFi stack or coexistence (the + multi-interface case unlocked by composing network priority with the + sdkconfig reconciler).""" + generate_main(component_config_path("network_wifi_ethernet_priority.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert "CONFIG_ESP_WIFI_ENABLED" not in sdkconfig + assert "CONFIG_SW_COEXIST_ENABLE" not in sdkconfig + # WiFi has no AP here, so SoftAP/DHCP server are still dropped. + assert sdkconfig.get("CONFIG_ESP_WIFI_SOFTAP_SUPPORT") is False + assert sdkconfig.get("CONFIG_LWIP_DHCPS") is False + + +def test_esp32_build_internals_are_yaml_only() -> None: + """ESP32 raw framework / build inputs are ``YAML_ONLY``. + + The framework block's PlatformIO package pins, raw ESP-IDF + sdkconfig options, the low-level ``advanced`` block, extra IDF + component sources, plus the partition table and toolchain override + on the main schema are build internals — never UI form fields. + User-facing choices (framework type/version, board, variant, …) + stay on the main form. + """ + from esphome.components.esp32 import CONFIG_SCHEMA, FRAMEWORK_SCHEMA + + fw_markers = {str(k): k for k in FRAMEWORK_SCHEMA.schema} + for field in ( + "release", + "source", + "platform_version", + "sdkconfig_options", + "advanced", + "components", + ): + assert fw_markers[field].visibility is cv.Visibility.YAML_ONLY, field + # Framework type/version remain user-facing. + assert fw_markers["type"].visibility is None + assert fw_markers["version"].visibility is None + + main_markers = {str(k): k for k in CONFIG_SCHEMA.validators[0].schema} + assert main_markers["partitions"].visibility is cv.Visibility.YAML_ONLY + # toolchain is a real but rarely-touched override -> advanced disclosure. + assert main_markers["toolchain"].visibility is cv.Visibility.ADVANCED + assert main_markers["board"].visibility is None + assert main_markers["flash_size"].visibility is None + + +def test_downgrade_protection_passes_with_numeric_version_and_signing() -> None: + assert _ota_downgrade_protection_errors("1.2.3", signed_ota_enabled=True) == [] + + +def test_downgrade_protection_accepts_calendar_version() -> None: + assert _ota_downgrade_protection_errors("2024.12.0", signed_ota_enabled=True) == [] + + +def test_downgrade_protection_requires_project_version() -> None: + errs = _ota_downgrade_protection_errors(None, signed_ota_enabled=True) + assert len(errs) == 1 + assert "version" in str(errs[0]) + + +def test_downgrade_protection_rejects_non_numeric_version() -> None: + errs = _ota_downgrade_protection_errors("1.0-beta", signed_ota_enabled=True) + assert len(errs) == 1 + assert "dotted-numeric" in str(errs[0]) + + +def test_downgrade_protection_requires_signed_ota() -> None: + errs = _ota_downgrade_protection_errors("1.2.3", signed_ota_enabled=False) + assert len(errs) == 1 + assert "signed_ota_verification" in str(errs[0]) + + +def test_downgrade_protection_reports_all_unmet_requirements() -> None: + # No project version and no signing -> two distinct errors. + errs = _ota_downgrade_protection_errors(None, signed_ota_enabled=False) + assert len(errs) == 2 + + +@pytest.mark.parametrize( + "config", + [ + # V2 schemes: signing key (sign during build) or no key at all + # (external signing; the public key travels in the signature block). + {"signing_scheme": "rsa3072", "signing_key": "key.pem"}, + {"signing_scheme": "rsa3072"}, + {"signing_scheme": "ecdsa256", "signing_key": "key.pem"}, + {"signing_scheme": "ecdsa256"}, + # V1 ECDSA: exactly one of signing key / verification key. + {"signing_scheme": "ecdsa_v1", "signing_key": "key.pem"}, + {"signing_scheme": "ecdsa_v1", "verification_key": "key.bin"}, + ], +) +def test_signed_ota_keys_valid_combinations(config: dict) -> None: + from esphome.components.esp32 import _validate_signed_ota_keys + + assert _validate_signed_ota_keys(config) is config + + +@pytest.mark.parametrize("value", [None, {}]) +def test_signed_ota_bare_block_selects_v2_external_signing(value: dict | None) -> None: + """A bare `signed_ota_verification:` block is valid: the default V2 + scheme embeds the public key in the signature block, so verifying + externally-signed binaries needs no keys in the config.""" + from esphome.components.esp32 import _validate_signed_ota_verification + + config = _validate_signed_ota_verification(value) + assert config == {"signing_scheme": "rsa3072"} + + +@pytest.mark.parametrize( + ("config", "match"), + [ + # A verification key is meaningless with the V2 schemes -- the public + # key is embedded in each image's signature block. + ( + {"signing_scheme": "rsa3072", "verification_key": "key.bin"}, + "only used with signing scheme 'ecdsa_v1'", + ), + ( + {"signing_scheme": "ecdsa256", "verification_key": "key.bin"}, + "only used with signing scheme 'ecdsa_v1'", + ), + # V1 ECDSA needs a key either way. + ( + {"signing_scheme": "ecdsa_v1"}, + "Signing scheme 'ecdsa_v1' requires either", + ), + # Never both keys at once. + ( + { + "signing_scheme": "rsa3072", + "signing_key": "key.pem", + "verification_key": "key.bin", + }, + "not both", + ), + ( + { + "signing_scheme": "ecdsa_v1", + "signing_key": "key.pem", + "verification_key": "key.bin", + }, + "not both", + ), + ], +) +def test_signed_ota_keys_invalid_combinations(config: dict, match: str) -> None: + from esphome.components.esp32 import _validate_signed_ota_keys + + with pytest.raises(cv.Invalid, match=match): + _validate_signed_ota_keys(config) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + # Full x.y.z versions are rewritten into pioarduino release URLs + ( + "55.3.30", + "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30/platform-espressif32.zip", + ), + ( + "55.3.31-2", + "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-2/platform-espressif32.zip", + ), + # Non-version values pass through untouched + ( + "https://github.com/pioarduino/platform-espressif32.git#develop", + "https://github.com/pioarduino/platform-espressif32.git#develop", + ), + ], +) +def test_parse_pio_platform_version(value: str, expected: str) -> None: + from esphome.components.esp32 import _parse_pio_platform_version + + assert _parse_pio_platform_version(value) == expected diff --git a/tests/component_tests/ethernet/__init__.py b/tests/component_tests/ethernet/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/ethernet/test_ethernet.py b/tests/component_tests/ethernet/test_ethernet.py new file mode 100644 index 0000000000..b3d37561c7 --- /dev/null +++ b/tests/component_tests/ethernet/test_ethernet.py @@ -0,0 +1,37 @@ +"""Tests for the ethernet final-validation coexistence gate.""" + +import pytest +from voluptuous import Invalid + +from esphome.components.ethernet import _final_validate +from esphome.components.network import _validate_priority_list +from esphome.const import CONF_PRIORITY +import esphome.final_validate as fv + + +@pytest.fixture(autouse=True) +def _reset_full_config(): + """Reset fv.full_config so each test starts with a clean slate.""" + token = fv.full_config.set({}) + yield + fv.full_config.reset(token) + + +def test_rejects_wifi_and_ethernet_without_priority() -> None: + """Wi-Fi + ethernet without a network: priority: list must be rejected.""" + fv.full_config.set({"wifi": {}, "ethernet": {}}) + with pytest.raises(Invalid, match="cannot be used together with component wifi"): + _final_validate({}) + + +def test_rejects_wifi_and_ethernet_with_incomplete_priority() -> None: + """A priority list missing an interface is rejected and names what's missing.""" + fv.full_config.set( + { + "wifi": {}, + "ethernet": {}, + "network": {CONF_PRIORITY: _validate_priority_list(["ethernet"])}, + } + ) + with pytest.raises(Invalid, match=r"must.*list both interfaces; missing: wifi"): + _final_validate({}) diff --git a/tests/component_tests/gsl3670/__init__.py b/tests/component_tests/gsl3670/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/gsl3670/test_init.py b/tests/component_tests/gsl3670/test_init.py new file mode 100644 index 0000000000..8528cf23ca --- /dev/null +++ b/tests/component_tests/gsl3670/test_init.py @@ -0,0 +1,285 @@ +"""Tests for the gsl3670 touchscreen configuration validation.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from esphome import config_validation as cv +from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32 +from esphome.components.gsl3670 import touchscreen as gsl +from esphome.const import ( + CONF_CALIBRATION, + CONF_INTERRUPT_PIN, + CONF_MODEL, + CONF_RESET_PIN, + CONF_TRANSFORM, + PlatformFramework, +) +from tests.component_tests.types import SetCoreConfigCallable + +VALID_URL = "https://example.com/fw.bin" + + +def _make_firmware(blocks: int = 2) -> bytes: + """Build a structurally valid firmware blob with ``blocks`` blocks. + + Each block is ``_FW_BLK_SIZE`` bytes: a 4-byte header (page address <= 0xEF + followed by the 1/2/3 marker bytes) and a 128-byte payload. + """ + out = bytearray() + for i in range(blocks): + out += bytes([i, 1, 2, 3]) + bytes(gsl._FW_BLK_SIZE - 4) + return bytes(out) + + +def _write_firmware(tmp_path: Path, data: bytes | None = None) -> Path: + """Write firmware bytes to a temp file and return its path.""" + path = tmp_path / "fw.bin" + path.write_bytes(_make_firmware() if data is None else data) + return path + + +# --------------------------------------------------------------------------- +# _validate_firmware_data - blob structure +# --------------------------------------------------------------------------- + + +def test_validate_firmware_data_accepts_valid_blob() -> None: + """A correctly structured blob passes validation.""" + gsl._validate_firmware_data(_make_firmware(3), "test") + + +@pytest.mark.parametrize("length", [0, gsl._FW_BLK_SIZE - 1, gsl._FW_BLK_SIZE + 1]) +def test_validate_firmware_data_rejects_bad_length(length: int) -> None: + """The blob length must be a non-zero multiple of the block size.""" + with pytest.raises(cv.Invalid, match="length is incorrect"): + gsl._validate_firmware_data(bytes(length), "test") + + +@pytest.mark.parametrize( + "index,value", + [ + (0, 0xF0), # page address must be <= 0xEF + (1, 0x00), # marker byte must be 1 + (2, 0x00), # marker byte must be 2 + (3, 0x00), # marker byte must be 3 + ], +) +def test_validate_firmware_data_rejects_corrupted_header( + index: int, value: int +) -> None: + """A block whose header bytes are wrong is reported as corrupted.""" + data = bytearray(_make_firmware(2)) + # Corrupt the header of the second block. + data[gsl._FW_BLK_SIZE + index] = value + with pytest.raises(cv.Invalid, match="Corrupted firmware at block 1"): + gsl._validate_firmware_data(bytes(data), "test") + + +# --------------------------------------------------------------------------- +# _cache_path / firmware_path +# --------------------------------------------------------------------------- + + +def test_cache_path_is_deterministic_per_url( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """The cache path is derived from (and stable for) the URL.""" + monkeypatch.setattr( + gsl.external_files, "compute_local_file_dir", lambda _: tmp_path + ) + first = gsl._cache_path(VALID_URL) + assert first == gsl._cache_path(VALID_URL) + assert first != gsl._cache_path("https://example.com/other.bin") + assert first.parent == tmp_path + + +def test_firmware_path_prefers_local_file(tmp_path: Path) -> None: + """A ``file`` source is returned as-is, without consulting the cache.""" + path = _write_firmware(tmp_path) + assert gsl.firmware_path({"file": path}) == path + + +def test_firmware_path_uses_cache_for_url( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A ``url`` source resolves to the cache path for that URL.""" + monkeypatch.setattr( + gsl.external_files, "compute_local_file_dir", lambda _: tmp_path + ) + assert gsl.firmware_path({"url": VALID_URL}) == gsl._cache_path(VALID_URL) + + +# --------------------------------------------------------------------------- +# _validate_firmware / FIRMWARE_SCHEMA +# --------------------------------------------------------------------------- + + +def test_firmware_requires_exactly_one_source(tmp_path: Path) -> None: + """Supplying both, or neither, of url/file is an error.""" + path = _write_firmware(tmp_path) + with pytest.raises(cv.Invalid, match="Exactly one"): + gsl._validate_firmware({"url": VALID_URL, "file": path}) + with pytest.raises(cv.Invalid, match="Exactly one"): + gsl._validate_firmware({}) + + +def test_firmware_file_valid(tmp_path: Path) -> None: + """A valid firmware file passes the full FIRMWARE_SCHEMA.""" + path = _write_firmware(tmp_path) + result = gsl.FIRMWARE_SCHEMA({"file": str(path)}) + assert result["file"] == path + + +def test_firmware_file_corrupt_rejected(tmp_path: Path) -> None: + """A file whose contents fail the structural check is rejected.""" + path = _write_firmware(tmp_path, data=b"\x00" * (gsl._FW_BLK_SIZE * 2)) + with pytest.raises(cv.Invalid, match="Corrupted firmware"): + gsl._validate_firmware({"file": path}) + + +def test_firmware_url_downloads_and_validates( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A url source downloads the content and validates its structure.""" + data = _make_firmware() + monkeypatch.setattr( + gsl.external_files, "compute_local_file_dir", lambda _: tmp_path + ) + monkeypatch.setattr(gsl.external_files, "download_content", lambda url, path: data) + assert gsl._validate_firmware({"url": VALID_URL}) == {"url": VALID_URL} + + +def test_firmware_url_sha256_mismatch_rejected( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A configured SHA-256 that does not match the download is rejected.""" + data = _make_firmware() + monkeypatch.setattr( + gsl.external_files, "compute_local_file_dir", lambda _: tmp_path + ) + monkeypatch.setattr(gsl.external_files, "download_content", lambda url, path: data) + with pytest.raises(cv.Invalid, match="SHA-256 mismatch"): + gsl._validate_firmware({"url": VALID_URL, "sha256": "00" * 32}) + + +def test_firmware_url_invalid_structure_rejected( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Downloaded content that is not a valid blob is rejected.""" + monkeypatch.setattr( + gsl.external_files, "compute_local_file_dir", lambda _: tmp_path + ) + monkeypatch.setattr( + gsl.external_files, "download_content", lambda url, path: b"\x00\x01\x02" + ) + with pytest.raises(cv.Invalid, match="length is incorrect"): + gsl._validate_firmware({"url": VALID_URL}) + + +# --------------------------------------------------------------------------- +# CONFIG_SCHEMA +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def _esp32_core(set_core_config: SetCoreConfigCallable) -> None: + """Configure the core as an ESP32 target for the schema tests.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + +def test_config_custom_model_minimal(tmp_path: Path) -> None: + """The CUSTOM model validates with an explicit firmware file and pins.""" + fw = _write_firmware(tmp_path) + result = gsl.CONFIG_SCHEMA( + { + "model": "custom", + "interrupt_pin": 16, + "reset_pin": 4, + "firmware": {"file": str(fw)}, + } + ) + assert result[CONF_MODEL] == "CUSTOM" + assert "id" in result + # The CUSTOM model supplies no transform/calibration defaults. + assert CONF_TRANSFORM not in result + assert CONF_CALIBRATION not in result + + +def test_config_custom_model_requires_firmware() -> None: + """The firmware option is required for the CUSTOM model (no default).""" + with pytest.raises(cv.Invalid, match=r"required key not provided.*firmware"): + gsl.CONFIG_SCHEMA({"model": "custom", "interrupt_pin": 16, "reset_pin": 4}) + + +def test_config_invalid_model_rejected() -> None: + """An unknown model name is rejected.""" + with pytest.raises(cv.Invalid, match="model"): + gsl.CONFIG_SCHEMA({"model": "nonexistent"}) + + +def test_config_seeed_model_applies_defaults(tmp_path: Path) -> None: + """The SEEED model populates transform and calibration defaults. + + ``reset_pin`` is overridden with a plain GPIO so the test does not depend on + the model's default IO-expander pin. + """ + fw = _write_firmware(tmp_path) + result = gsl.CONFIG_SCHEMA( + { + "model": "seeed-reterminal-d1001", + "reset_pin": 4, + "firmware": {"file": str(fw)}, + } + ) + assert result[CONF_MODEL] == "SEEED-RETERMINAL-D1001" + # Transform defaults from the model. + assert result[CONF_TRANSFORM] == { + "swap_xy": True, + "mirror_x": True, + "mirror_y": True, + } + # Calibration defaults from the model. + assert result[CONF_CALIBRATION]["x_min"] == 20 + assert result[CONF_CALIBRATION]["x_max"] == 872 + assert result[CONF_CALIBRATION]["y_min"] == 20 + assert result[CONF_CALIBRATION]["y_max"] == 1644 + # The interrupt pin default (16) is applied without being specified. + assert CONF_INTERRUPT_PIN in result + assert CONF_RESET_PIN in result + + +def test_config_guition_model_applies_defaults(tmp_path: Path) -> None: + """The GUITION model populates transform and calibration defaults.""" + fw = _write_firmware(tmp_path) + result = gsl.CONFIG_SCHEMA( + { + "model": "guition-jc8012p4a1", + "firmware": {"file": str(fw)}, + } + ) + assert result[CONF_MODEL] == "GUITION-JC8012P4A1" + # Transform defaults from the model. + assert result[CONF_TRANSFORM] == { + "swap_xy": True, + "mirror_x": True, + "mirror_y": False, + } + # Calibration defaults from the model. + assert result[CONF_CALIBRATION]["x_min"] == 20 + assert result[CONF_CALIBRATION]["x_max"] == 880 + assert result[CONF_CALIBRATION]["y_min"] == 20 + assert result[CONF_CALIBRATION]["y_max"] == 1648 + assert result[CONF_INTERRUPT_PIN]["number"] == 21 + assert result[CONF_RESET_PIN]["number"] == 22 + + +def test_config_rejects_non_dict() -> None: + """A non-dict configuration is rejected.""" + with pytest.raises(cv.Invalid, match="expected a dictionary"): + gsl.CONFIG_SCHEMA("not a dict") diff --git a/tests/component_tests/heatpumpir/__init__.py b/tests/component_tests/heatpumpir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/heatpumpir/test_init.py b/tests/component_tests/heatpumpir/test_init.py new file mode 100644 index 0000000000..85da0c8bf5 --- /dev/null +++ b/tests/component_tests/heatpumpir/test_init.py @@ -0,0 +1,26 @@ +"""Tests for the heatpumpir climate config validation.""" + +from esphome.components.heatpumpir.climate import _default_visual +from esphome.const import CONF_MAX_TEMPERATURE, CONF_MIN_TEMPERATURE, CONF_VISUAL +from esphome.types import ConfigType + + +def test_default_visual_seeds_from_required_min_max() -> None: + """Without a visual block, the required min/max_temperature seed the visual + range so the entity reports it in Home Assistant instead of 0-100 (#17983).""" + config: ConfigType = {CONF_MIN_TEMPERATURE: 18, CONF_MAX_TEMPERATURE: 30} + _default_visual(config) + assert config[CONF_VISUAL][CONF_MIN_TEMPERATURE] == 18 + assert config[CONF_VISUAL][CONF_MAX_TEMPERATURE] == 30 + + +def test_default_visual_keeps_explicit() -> None: + """An explicit visual min/max is not overwritten by the required temps.""" + config: ConfigType = { + CONF_MIN_TEMPERATURE: 16, + CONF_MAX_TEMPERATURE: 32, + CONF_VISUAL: {CONF_MIN_TEMPERATURE: 18, CONF_MAX_TEMPERATURE: 30}, + } + _default_visual(config) + assert config[CONF_VISUAL][CONF_MIN_TEMPERATURE] == 18 + assert config[CONF_VISUAL][CONF_MAX_TEMPERATURE] == 30 diff --git a/tests/component_tests/image/config/image_test.yaml b/tests/component_tests/image/config/image_test.yaml index c34e0993a5..31c29de21b 100644 --- a/tests/component_tests/image/config/image_test.yaml +++ b/tests/component_tests/image/config/image_test.yaml @@ -4,6 +4,9 @@ esphome: esp32: board: esp32s3box +psram: + mode: octal + image: defaults: type: rgb565 diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index f7f60a1f4d..78462463b1 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +import logging from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch @@ -11,28 +12,36 @@ from PIL import Image as PILImage import pytest from esphome import config_validation as cv +from esphome.components.const import CONF_BYTE_ORDER +from esphome.components.file import image as file_image +from esphome.components.file.image import validate_image_final, write_image from esphome.components.image import ( CONF_ALPHA_CHANNEL, CONF_INVERT_ALPHA, CONF_OPAQUE, CONF_TRANSPARENCY, - CONFIG_SCHEMA, + PLATFORM_FILE, + _flatten_legacy_image_config, + _is_legacy_image_format, + _is_new_image_format, + _migrate_legacy_image_config, get_all_image_metadata, get_image_metadata, - write_image, ) -from esphome.const import CONF_DITHER, CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE +from esphome.const import ( + CONF_DITHER, + CONF_FILE, + CONF_ID, + CONF_PLATFORM, + CONF_RAW_DATA_ID, + CONF_TYPE, +) from esphome.core import CORE @pytest.mark.parametrize( ("config", "error_match"), [ - pytest.param( - "a string", - "Badly formed image configuration, expected a list or a dictionary", - id="invalid_string_config", - ), pytest.param( {"id": "image_id", "type": "rgb565"}, r"required key not provided @ data\['file'\]", @@ -43,6 +52,11 @@ from esphome.core import CORE r"required key not provided @ data\['id'\]", id="missing_id", ), + pytest.param( + {"id": "image_id", "file": "image.png"}, + r"required key not provided @ data\['type'\]", + id="missing_type", + ), pytest.param( {"id": "mdi_id", "file": "mdi:weather-##", "type": "rgb565"}, "Could not parse mdi icon name", @@ -84,155 +98,301 @@ from esphome.core import CORE "File can't be opened as image", id="invalid_image_file", ), - pytest.param( - {"defaults": {}, "images": [{"id": "image_id", "file": "image.png"}]}, - "Type is required either in the image config or in the defaults", - id="missing_type_in_defaults", - ), ], ) -def test_image_configuration_errors( +def test_file_platform_configuration_errors( config: Any, error_match: str, ) -> None: - """Test detection of invalid configuration.""" + """Invalid single-entry ``platform: file`` configs are rejected.""" with pytest.raises(cv.Invalid, match=error_match): - CONFIG_SCHEMA(config) + file_image.CONFIG_SCHEMA(config) + + +def test_file_platform_configuration_success() -> None: + """A fully-specified ``platform: file`` entry validates and keeps its keys.""" + result = file_image.CONFIG_SCHEMA( + { + "id": "image_id", + "file": "image.png", + "type": "rgb565", + "transparency": "chroma_key", + "byte_order": "little_endian", + "dither": "FloydSteinberg", + "resize": "100x100", + "invert_alpha": False, + } + ) + for key in (CONF_TYPE, CONF_ID, CONF_TRANSPARENCY, CONF_RAW_DATA_ID): + assert key in result, f"Missing key {key} in validated image configuration" + + +# --------------------------------------------------------------------------- +# Legacy `image:` config migration -- REMOVE these tests after 2027.1.0 together +# with the migration shim in esphome/components/image/__init__.py. +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("config", "expected"), + [ + pytest.param( + [{CONF_PLATFORM: "file", "id": "a"}], True, id="new_platform_list" + ), + pytest.param([], True, id="empty_list"), + pytest.param([{"id": "a", "file": "x.png"}], False, id="legacy_bare_list"), + pytest.param([{CONF_PLATFORM: "file"}, {"id": "a"}], False, id="mixed_list"), + pytest.param( + [{CONF_PLATFORM: "file"}, "not-a-dict"], False, id="non_dict_entry" + ), + pytest.param({"defaults": {}}, False, id="legacy_dict"), + ], +) +def test_is_new_image_format(config: object, expected: bool) -> None: + assert _is_new_image_format(config) is expected + + +def test_flatten_bare_list_filters_non_dicts() -> None: + out = _flatten_legacy_image_config( + [{"id": "a", "file": "x.png", "type": "binary"}, "not-a-dict"] + ) + assert out == [{"id": "a", "file": "x.png", "type": "binary"}] + + +def test_flatten_non_dict_non_list_yields_nothing() -> None: + assert _flatten_legacy_image_config("a string") == [] + + +def test_flatten_single_dict_with_id() -> None: + config = {"id": "a", "file": "x.png", "type": "binary"} + assert _flatten_legacy_image_config(config) == [config] + + +def test_flatten_single_dict_with_file_only() -> None: + config = {"file": "x.png", "type": "binary"} + assert _flatten_legacy_image_config(config) == [config] + + +def test_flatten_defaults_images_list() -> None: + out = _flatten_legacy_image_config( + { + "defaults": {"type": "rgb565", "byte_order": "little_endian"}, + "images": [{"id": "a", "file": "x.png"}], + } + ) + assert out == [ + { + "id": "a", + "file": "x.png", + "type": "rgb565", + "byte_order": "little_endian", + } + ] + + +def test_flatten_defaults_images_single_dict() -> None: + out = _flatten_legacy_image_config( + { + "defaults": {"type": "rgb565"}, + "images": {"id": "a", "file": "x.png"}, + } + ) + assert out == [{"id": "a", "file": "x.png", "type": "rgb565"}] + + +def test_flatten_type_grouped_list() -> None: + out = _flatten_legacy_image_config({"binary": [{"id": "a", "file": "x.png"}]}) + assert out == [{"id": "a", "file": "x.png", "type": "binary"}] + + +def test_flatten_type_grouped_transparency_list() -> None: + out = _flatten_legacy_image_config( + {"rgb565": {"alpha_channel": [{"id": "a", "file": "x.png"}]}} + ) + assert out == [ + { + "id": "a", + "file": "x.png", + "type": "rgb565", + "transparency": "alpha_channel", + } + ] + + +def test_flatten_type_grouped_transparency_single_dict() -> None: + out = _flatten_legacy_image_config( + {"rgb565": {"alpha_channel": {"id": "a", "file": "x.png"}}} + ) + assert out == [ + { + "id": "a", + "file": "x.png", + "type": "rgb565", + "transparency": "alpha_channel", + } + ] + + +def test_flatten_type_grouped_dict_without_transparency() -> None: + out = _flatten_legacy_image_config({"binary": {"id": "a", "file": "x.png"}}) + assert out == [{"id": "a", "file": "x.png", "type": "binary"}] + + +def test_flatten_drops_byte_order_for_non_endian_type() -> None: + out = _flatten_legacy_image_config( + { + "defaults": {"byte_order": "little_endian"}, + "binary": [{"id": "a", "file": "x.png"}], + } + ) + assert out == [{"id": "a", "file": "x.png", "type": "binary"}] + assert CONF_BYTE_ORDER not in out[0] + + +def test_flatten_keeps_byte_order_for_endian_type() -> None: + out = _flatten_legacy_image_config( + { + "defaults": {"byte_order": "little_endian"}, + "rgb565": [{"id": "a", "file": "x.png"}], + } + ) + assert out[0][CONF_BYTE_ORDER] == "little_endian" + + +def test_flatten_skips_meta_and_unknown_keys() -> None: + out = _flatten_legacy_image_config( + { + "defaults": {"type": "binary"}, + "images": [], + "not_a_type": [{"id": "a", "file": "x.png"}], + } + ) + assert out == [] + + +def test_flatten_images_list_skips_non_dict_entries() -> None: + out = _flatten_legacy_image_config( + { + "defaults": {"type": "binary"}, + "images": [{"id": "a", "file": "x.png"}, "not-a-dict"], + } + ) + assert out == [{"id": "a", "file": "x.png", "type": "binary"}] + + +def test_flatten_type_grouped_list_skips_non_dict_entries() -> None: + out = _flatten_legacy_image_config( + {"binary": [{"id": "a", "file": "x.png"}, "not-a-dict"]} + ) + assert out == [{"id": "a", "file": "x.png", "type": "binary"}] + + +def test_flatten_type_grouped_scalar_value_is_ignored() -> None: + # A known type key whose value is neither a list nor a dict yields nothing. + assert _flatten_legacy_image_config({"binary": "not-a-list-or-dict"}) == [] + + +def test_flatten_type_grouped_transparency_skips_non_dict_entries() -> None: + out = _flatten_legacy_image_config( + {"rgb565": {"alpha_channel": [{"id": "a", "file": "x.png"}, "not-a-dict"]}} + ) + assert out == [ + { + "id": "a", + "file": "x.png", + "type": "rgb565", + "transparency": "alpha_channel", + } + ] + + +def test_migrate_returns_none_for_new_format() -> None: + assert _migrate_legacy_image_config([{CONF_PLATFORM: "file", "id": "a"}]) is None + + +def test_migrate_legacy_warns_and_prepends_platform( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + out = _migrate_legacy_image_config( + [{"id": "a", "file": "x.png", "type": "binary"}] + ) + assert out == [ + {CONF_PLATFORM: PLATFORM_FILE, "id": "a", "file": "x.png", "type": "binary"} + ] + assert "deprecated" in caplog.text + assert f"platform: {PLATFORM_FILE}" in caplog.text + + +@pytest.mark.parametrize( + ("config", "expected"), + [ + # Recognised legacy shapes -> migrate. + pytest.param([{"id": "a", "file": "x.png"}], True, id="bare_list_of_dicts"), + pytest.param({"id": "a", "file": "x.png"}, True, id="single_image_dict"), + pytest.param({"file": "x.png"}, True, id="single_dict_file_only"), + pytest.param({"defaults": {}, "images": []}, True, id="defaults_images"), + pytest.param({"rgb565": [{"id": "a"}]}, True, id="type_grouped"), + # Shapes the legacy schema never accepted -> not migrated. + pytest.param([], False, id="empty_list"), + pytest.param(["bad"], False, id="list_with_non_dict"), + pytest.param([{"id": "a"}, "bad"], False, id="list_mixed_dict_and_non_dict"), + pytest.param( + [{CONF_PLATFORM: "file", "id": "a"}], False, id="already_platform_tagged" + ), + pytest.param({"foo": 1}, False, id="dict_unknown_keys"), + pytest.param("a string", False, id="scalar"), + ], +) +def test_is_legacy_image_format(config: object, expected: bool) -> None: + assert _is_legacy_image_format(config) is expected @pytest.mark.parametrize( "config", [ - pytest.param( - { - "id": "image_id", - "file": "image.png", - "type": "rgb565", - "transparency": "chroma_key", - "byte_order": "little_endian", - "dither": "FloydSteinberg", - "resize": "100x100", - "invert_alpha": False, - }, - id="single_image_all_options", - ), - pytest.param( - [ - { - "id": "image_id", - "file": "image.png", - "type": "binary", - } - ], - id="list_of_images", - ), - pytest.param( - { - "defaults": { - "type": "rgb565", - "transparency": "chroma_key", - "byte_order": "little_endian", - "dither": "FloydSteinberg", - "resize": "100x100", - "invert_alpha": False, - }, - "images": [ - { - "id": "image_id", - "file": "image.png", - } - ], - }, - id="images_with_defaults", - ), - pytest.param( - { - "rgb565": { - "alpha_channel": [ - { - "id": "image_id", - "file": "image.png", - "transparency": "alpha_channel", - "byte_order": "little_endian", - "dither": "FloydSteinberg", - "resize": "100x100", - "invert_alpha": False, - } - ] - }, - "binary": [ - { - "id": "image_id", - "file": "image.png", - "transparency": "opaque", - "dither": "FloydSteinberg", - "resize": "100x100", - "invert_alpha": False, - } - ], - }, - id="type_based_organization", - ), - pytest.param( - { - "defaults": { - "type": "binary", - "transparency": "chroma_key", - "byte_order": "little_endian", - "dither": "FloydSteinberg", - "resize": "100x100", - "invert_alpha": False, - }, - "rgb565": { - "alpha_channel": [ - { - "id": "image_id", - "file": "image.png", - "transparency": "alpha_channel", - "dither": "none", - } - ] - }, - "binary": [ - { - "id": "image_id", - "file": "image.png", - "transparency": "opaque", - } - ], - }, - id="type_based_with_defaults", - ), - pytest.param( - { - "defaults": { - "type": "rgb565", - "transparency": "alpha_channel", - }, - "binary": { - "opaque": [ - { - "id": "image_id", - "file": "image.png", - } - ], - }, - }, - id="binary_with_defaults", - ), + pytest.param(["bad"], id="list_with_non_dict"), + pytest.param([{"id": "a"}, "bad"], id="list_mixed"), + pytest.param({"foo": 1}, id="dict_unknown_keys"), ], ) -def test_image_configuration_success( - config: dict[str, Any] | list[dict[str, Any]], +def test_migrate_returns_none_for_invalid_legacy_shapes( + config: object, caplog: pytest.LogCaptureFixture ) -> None: - """Test successful configuration validation.""" - result = CONFIG_SCHEMA(config) - # All valid configurations should return a list of images - assert isinstance(result, list) - for key in (CONF_TYPE, CONF_ID, CONF_TRANSPARENCY, CONF_RAW_DATA_ID): - assert all(key in x for x in result), ( - f"Missing key {key} in image configuration" + """Unrecognised shapes are not migrated (and emit no warning) so normal + platform validation surfaces a proper error instead of silently dropping + the offending input.""" + with caplog.at_level(logging.WARNING): + assert _migrate_legacy_image_config(config) is None + assert "deprecated" not in caplog.text + + +# --------------------------- end legacy migration -------------------------- + + +def test_validate_image_final_defaults_to_little_endian() -> None: + out = validate_image_final({CONF_FILE: "x.png"}) + assert out[CONF_BYTE_ORDER] == "LITTLE_ENDIAN" + + +def test_validate_image_final_keeps_little_endian( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + out = validate_image_final( + {CONF_FILE: "x.png", CONF_BYTE_ORDER: "LITTLE_ENDIAN"} ) + assert out[CONF_BYTE_ORDER] == "LITTLE_ENDIAN" + assert "big-endian" not in caplog.text + + +def test_validate_image_final_warns_on_big_endian( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + out = validate_image_final({CONF_FILE: "x.png", CONF_BYTE_ORDER: "BIG_ENDIAN"}) + assert out[CONF_BYTE_ORDER] == "BIG_ENDIAN" + assert "big-endian" in caplog.text def test_image_generation( @@ -369,7 +529,7 @@ def test_get_all_image_metadata_empty() -> None: @pytest.fixture def mock_progmem_array(): """Mock progmem_array to avoid needing a proper ID object in tests.""" - with patch("esphome.components.image.cg.progmem_array") as mock_progmem: + with patch("esphome.components.file.image.cg.progmem_array") as mock_progmem: mock_progmem.return_value = MagicMock() yield mock_progmem diff --git a/tests/component_tests/lvgl/config/layout_update_test.yaml b/tests/component_tests/lvgl/config/layout_update_test.yaml new file mode 100644 index 0000000000..84765a60cf --- /dev/null +++ b/tests/component_tests/lvgl/config/layout_update_test.yaml @@ -0,0 +1,92 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +spi: + - id: spi_bus + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: mipi_spi + spi_id: spi_bus + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +lvgl: + id: lvgl_id + displays: tft_display + pages: + - id: main_page + widgets: + # A flex container whose layout options are changed at runtime. + - obj: + id: flex_box + layout: + type: flex + flex_flow: row + widgets: + - label: + text: a + - label: + text: b + + # A grid container whose alignment options are changed at runtime. + # The grid structure (rows/columns) is fixed here at creation. + - obj: + id: grid_box + layout: + type: grid + grid_rows: [content, content] + grid_columns: [fr(1), fr(1)] + widgets: + - label: + text: c + - label: + text: d + + # Button hosting all of the update actions under test. + - button: + id: btn_actions + on_click: + # Update flex container options (type unchanged). + - lvgl.widget.update: + id: flex_box + layout: + flex_flow: column + flex_align_main: center + flex_align_cross: end + pad_row: 7px + # Update grid container alignment options (structure unchanged). + - lvgl.widget.update: + id: grid_box + layout: + grid_column_align: space_between + grid_row_align: center + # Top-level layout applies to the active screen. + - lvgl.update: + layout: + flex_flow: column + pad_column: 5px + # Layout applied to the top display layer. + - lvgl.update: + top_layer: + layout: + flex_flow: row + # Styling applied to the bottom display layer (exercises the + # layers code path that previously generated no code). + - lvgl.update: + bottom_layer: + bg_color: 0x123456 diff --git a/tests/component_tests/lvgl/config/not_paused.yaml b/tests/component_tests/lvgl/config/not_paused.yaml new file mode 100644 index 0000000000..1dfe8f4ee9 --- /dev/null +++ b/tests/component_tests/lvgl/config/not_paused.yaml @@ -0,0 +1,26 @@ +esphome: + name: test-not-paused + +esp32: + board: lolin_c3_mini + +spi: + mosi_pin: + number: GPIO2 + ignore_strapping_warning: true + clk_pin: GPIO1 + +display: + - platform: mipi_spi + data_rate: 20MHz + model: st7735 + cs_pin: + number: GPIO8 + ignore_strapping_warning: true + dc_pin: + number: GPIO3 + +lvgl: + widgets: + - obj: + id: root_obj diff --git a/tests/component_tests/lvgl/config/paused.yaml b/tests/component_tests/lvgl/config/paused.yaml new file mode 100644 index 0000000000..ea747ec75b --- /dev/null +++ b/tests/component_tests/lvgl/config/paused.yaml @@ -0,0 +1,27 @@ +esphome: + name: test-paused + +esp32: + board: lolin_c3_mini + +spi: + mosi_pin: + number: GPIO2 + ignore_strapping_warning: true + clk_pin: GPIO1 + +display: + - platform: mipi_spi + data_rate: 20MHz + model: st7735 + cs_pin: + number: GPIO8 + ignore_strapping_warning: true + dc_pin: + number: GPIO3 + +lvgl: + paused: true + widgets: + - obj: + id: root_obj diff --git a/tests/component_tests/lvgl/test_animation.py b/tests/component_tests/lvgl/test_animation.py new file mode 100644 index 0000000000..1a2cde632c --- /dev/null +++ b/tests/component_tests/lvgl/test_animation.py @@ -0,0 +1,201 @@ +"""Tests for the LVGL animation schema and configuration validation.""" + +from __future__ import annotations + +import pytest +from voluptuous import Invalid, MultipleInvalid + +from esphome.components.lvgl.animation import ( + ANIMABLE_STYLES, + ANIMATION_SCHEMA, + TIMING_SCHEMA, + from_to, + literal_color, +) +from esphome.components.lvgl.defines import LValidator +from esphome.core import Lambda + + +def _animation(**overrides) -> dict: + """A minimal valid animation config, with optional overrides applied.""" + config = { + "id": "anim_id", + "widgets": [{"id": "widget_id", "x": {"from": 0, "to": 100}}], + } + config.update(overrides) + return config + + +# --------------------------------------------------------------------------- +# Animatable property set +# --------------------------------------------------------------------------- + + +class TestAnimableStyles: + def test_all_entries_are_animatable_validators(self) -> None: + """Every animatable style must be an LValidator marked animatable.""" + assert ANIMABLE_STYLES + assert all( + isinstance(v, LValidator) and v.animatable for v in ANIMABLE_STYLES.values() + ) + + def test_known_animatable_present(self) -> None: + for prop in ("x", "y", "opa", "bg_color", "transform_rotation"): + assert prop in ANIMABLE_STYLES + + def test_non_animatable_absent(self) -> None: + # width/height set size but are not animatable; layout/padding never are. + for prop in ("width", "height", "radius", "pad_all", "align"): + assert prop not in ANIMABLE_STYLES + + +# --------------------------------------------------------------------------- +# Animation schema +# --------------------------------------------------------------------------- + + +class TestAnimationSchema: + def test_defaults(self) -> None: + config = ANIMATION_SCHEMA(_animation()) + assert config["duration"].total_milliseconds == 5000 + assert config["start_delay"].total_milliseconds == 0 + assert config["auto_start"] is False + assert config["loop"] is False + assert config["timing"] == [] + + def test_values_preserved(self) -> None: + config = ANIMATION_SCHEMA( + _animation(duration="2s", start_delay="250ms", auto_start=True, loop=True) + ) + assert config["duration"].total_milliseconds == 2000 + assert config["start_delay"].total_milliseconds == 250 + assert config["auto_start"] is True + assert config["loop"] is True + + def test_id_required(self) -> None: + with pytest.raises((Invalid, MultipleInvalid)): + ANIMATION_SCHEMA({"widgets": [{"id": "widget_id"}]}) + + def test_widgets_required(self) -> None: + with pytest.raises((Invalid, MultipleInvalid)): + ANIMATION_SCHEMA({"id": "anim_id"}) + + def test_multiple_properties_and_widgets(self) -> None: + config = ANIMATION_SCHEMA( + _animation( + widgets=[ + { + "id": "w1", + "x": {"from": 0, "to": 100}, + "opa": {"from": "0%", "to": "100%"}, + }, + {"id": "w2", "y": {"from": 10, "to": 50}}, + ] + ) + ) + assert len(config["widgets"]) == 2 + + def test_unknown_property_rejected(self) -> None: + with pytest.raises((Invalid, MultipleInvalid)): + ANIMATION_SCHEMA( + _animation(widgets=[{"id": "w1", "not_a_style": {"from": 0, "to": 1}}]) + ) + + +class TestAnimatedColorLiteral: + """A color animated via from/to must be a literal, not a lambda.""" + + def test_color_lambda_rejected_directly(self) -> None: + with pytest.raises(Invalid, match="lambda"): + literal_color(Lambda("return lv_color_hex(0xFF0000);")) + + def test_color_literal_accepted_directly(self) -> None: + # A literal color value validates without error. + literal_color(0xFF0000) + + def test_color_lambda_rejected_in_animation(self) -> None: + with pytest.raises((Invalid, MultipleInvalid), match="lambda"): + ANIMATION_SCHEMA( + _animation( + widgets=[ + { + "id": "w1", + "text_color": { + "from": Lambda("return lv_color_hex(0xFF0000);"), + "to": 0x00FF00, + }, + } + ] + ) + ) + + def test_color_literals_accepted_in_animation(self) -> None: + config = ANIMATION_SCHEMA( + _animation( + widgets=[{"id": "w1", "text_color": {"from": 0xFF0000, "to": 0x00FF00}}] + ) + ) + assert config["widgets"][0]["id"].id == "w1" + + def test_non_color_property_allows_lambda(self) -> None: + # Only colors are restricted; numeric properties may use lambdas. + config = ANIMATION_SCHEMA( + _animation( + widgets=[{"id": "w1", "x": {"from": Lambda("return 5;"), "to": 100}}] + ) + ) + assert config["widgets"][0]["id"].id == "w1" + + +class TestFromTo: + def test_requires_both(self) -> None: + validator = from_to(lambda value: value) + with pytest.raises((Invalid, MultipleInvalid)): + validator({"from": 1}) + with pytest.raises((Invalid, MultipleInvalid)): + validator({"to": 1}) + + def test_accepts_both(self) -> None: + validator = from_to(lambda value: value) + assert validator({"from": 1, "to": 2}) == {"from": 1, "to": 2} + + +# --------------------------------------------------------------------------- +# Timing schema +# --------------------------------------------------------------------------- + + +class TestTimingSchema: + def test_round_trip_string(self) -> None: + assert TIMING_SCHEMA("round_trip")["type"] == "round_trip" + + def test_ease_in_out_default_weight(self) -> None: + result = TIMING_SCHEMA("ease_in_out") + assert result["type"] == "ease_in_out" + assert result["weight"] == pytest.approx(2.0) + + def test_ease_in_out_custom_weight(self) -> None: + result = TIMING_SCHEMA({"type": "ease_in_out", "weight": 3}) + assert result["weight"] == pytest.approx(3.0) + + def test_gravity_defaults(self) -> None: + result = TIMING_SCHEMA("gravity") + assert result["type"] == "gravity" + assert result["bounce"] == pytest.approx(0.5) + assert result["acceleration"] == pytest.approx(0.5) + + def test_gravity_custom(self) -> None: + result = TIMING_SCHEMA({"type": "gravity", "bounce": 0.3, "acceleration": 0.8}) + assert result["bounce"] == pytest.approx(0.3) + assert result["acceleration"] == pytest.approx(0.8) + + def test_unknown_type_rejected(self) -> None: + with pytest.raises((Invalid, MultipleInvalid)): + TIMING_SCHEMA({"type": "not_a_timing"}) + + def test_timing_list_in_animation(self) -> None: + config = ANIMATION_SCHEMA( + _animation(timing=["round_trip", {"type": "gravity", "bounce": 0.3}]) + ) + types = [t["type"] for t in config["timing"]] + assert types == ["round_trip", "gravity"] diff --git a/tests/component_tests/lvgl/test_layout_update.py b/tests/component_tests/lvgl/test_layout_update.py new file mode 100644 index 0000000000..b9730df379 --- /dev/null +++ b/tests/component_tests/lvgl/test_layout_update.py @@ -0,0 +1,208 @@ +"""Tests for updating LVGL layout options via the update actions. + +The ``lvgl.update`` and ``lvgl.widget.update`` (and per-widget +``lvgl..update``) actions can change a container's layout *options* at +runtime. The layout ``type`` and the grid ``grid_rows``/``grid_columns`` +structure are fixed at widget creation (they determine the cells/options +available to child widgets), so only the simple style options - those applied +via ``lv_obj_set_style_...`` calls - may be changed. + +These tests cover both the ``layout_validator`` (schema/normalisation) and the +generated C++ for each target: a widget, the active screen (top-level +``lvgl.update``) and the display layers. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from voluptuous import Invalid + +from esphome.__main__ import generate_cpp_contents +from esphome.components.lvgl.defines import TYPE_FLEX, TYPE_GRID, get_lv_uses +from esphome.components.lvgl.layout import layout_validator +from esphome.config import read_config +from esphome.core import CORE + +# --------------------------------------------------------------------------- +# layout_validator - schema and normalisation +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "value,expected", + [ + ({"flex_flow": "row"}, {"flex_flow": "LV_FLEX_FLOW_ROW"}), + ({"flex_align_main": "center"}, {"flex_align_main": "LV_FLEX_ALIGN_CENTER"}), + ({"flex_align_cross": "end"}, {"flex_align_cross": "LV_FLEX_ALIGN_END"}), + ( + {"grid_column_align": "space_between"}, + {"grid_column_align": "LV_GRID_ALIGN_SPACE_BETWEEN"}, + ), + ({"grid_row_align": "center"}, {"grid_row_align": "LV_GRID_ALIGN_CENTER"}), + ({"pad_row": "7px"}, {"pad_row": 7}), + ({"pad_column": "5px"}, {"pad_column": 5}), + ], +) +def test_layout_validator_normalises_options(value: dict, expected: dict) -> None: + """Each supported option is accepted and normalised to its LVGL form.""" + assert layout_validator(value) == expected + + +def test_layout_validator_accepts_multiple_options() -> None: + """Several options may be combined in one update.""" + result = layout_validator( + {"flex_flow": "column", "flex_align_main": "center", "pad_row": "4px"} + ) + assert result == { + "flex_flow": "LV_FLEX_FLOW_COLUMN", + "flex_align_main": "LV_FLEX_ALIGN_CENTER", + "pad_row": 4, + } + + +@pytest.mark.parametrize( + "value", + [ + {"type": "flex"}, + {"type": "grid", "grid_column_align": "center"}, + {"grid_rows": 3}, + {"grid_columns": ["fr(1)"]}, + {"grid_rows": [1, 2], "flex_flow": "row"}, + ], +) +def test_layout_validator_rejects_structural_keys(value: dict) -> None: + """The layout type and grid structure are fixed at creation and must not + be changeable via an update action.""" + with pytest.raises(Invalid, match="extra keys not allowed"): + layout_validator(value) + + +def test_layout_validator_rejects_empty() -> None: + """An update must specify at least one layout option.""" + with pytest.raises(Invalid, match="at least one layout option"): + layout_validator({}) + + +def test_layout_validator_registers_flex_use() -> None: + """Validating a flex option registers the flex feature so LV_USE_FLEX is + emitted even when the option is set solely via an update action.""" + layout_validator({"flex_flow": "row"}) + assert TYPE_FLEX in get_lv_uses() + + +def test_layout_validator_registers_grid_use() -> None: + """Validating a grid option registers the grid feature.""" + layout_validator({"grid_column_align": "center"}) + assert TYPE_GRID in get_lv_uses() + + +def test_pad_only_update_registers_no_layout_use() -> None: + """Padding options belong to both layout types, so they alone do not force + either feature on.""" + layout_validator({"pad_row": "4px"}) + uses = get_lv_uses() + assert TYPE_FLEX not in uses + assert TYPE_GRID not in uses + + +# --------------------------------------------------------------------------- +# Generated C++ for the update actions +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def main_cpp(request: pytest.FixtureRequest) -> str: + """Generate the C++ output for the shared layout-update YAML config once + per module (codegen is relatively expensive).""" + config_path = Path(request.fspath).parent / "config" / "layout_update_test.yaml" + original_path = CORE.config_path + try: + CORE.config_path = config_path + CORE.config = read_config({}) + generate_cpp_contents(CORE.config) + return CORE.cpp_global_section + CORE.cpp_main_section + finally: + CORE.config_path = original_path + CORE.reset() + + +def test_widget_flex_update_applies_partial_options(main_cpp: str) -> None: + """``lvgl.widget.update`` changes only the flex options that are specified, + via the appropriate ``lv_obj_set_style_...``/``lv_obj_set_flex_flow`` + calls on the target widget.""" + assert "lv_obj_set_flex_flow(flex_box, LV_FLEX_FLOW_COLUMN)" in main_cpp + assert ( + "lv_obj_set_style_flex_main_place(flex_box, LV_FLEX_ALIGN_CENTER, LV_STATE_DEFAULT)" + in main_cpp + ) + assert ( + "lv_obj_set_style_flex_cross_place(flex_box, LV_FLEX_ALIGN_END, LV_STATE_DEFAULT)" + in main_cpp + ) + assert "lv_obj_set_style_pad_row(flex_box, 7, LV_STATE_DEFAULT)" in main_cpp + + +def test_widget_flex_update_does_not_change_type(main_cpp: str) -> None: + """The update must not re-establish the layout type: ``lv_obj_set_layout`` + is emitted once (at creation) and never from the update action.""" + assert main_cpp.count("lv_obj_set_layout(flex_box,") == 1 + + +def test_widget_flex_update_is_partial(main_cpp: str) -> None: + """An option that was not specified in the update (the track placement) is + only set at creation, not by the partial update.""" + assert main_cpp.count("lv_obj_set_style_flex_track_place(flex_box,") == 1 + + +def test_widget_grid_update_applies_alignments(main_cpp: str) -> None: + """``lvgl.widget.update`` on a grid container changes its alignment + options without touching the grid structure.""" + assert ( + "lv_obj_set_style_grid_column_align(grid_box, LV_GRID_ALIGN_SPACE_BETWEEN, " + "LV_STATE_DEFAULT)" in main_cpp + ) + assert ( + "lv_obj_set_style_grid_row_align(grid_box, LV_GRID_ALIGN_CENTER, LV_STATE_DEFAULT)" + in main_cpp + ) + + +def test_grid_update_does_not_regenerate_descriptor_arrays(main_cpp: str) -> None: + """The grid row/column descriptor arrays are structural and generated once + at creation; an update must not regenerate them.""" + assert main_cpp.count("grid_box_row_dsc") != 0 + # The descriptor array is declared once and referenced once at creation. + assert main_cpp.count("grid_box_row_dsc") == main_cpp.count("grid_box_column_dsc") + assert "lv_obj_set_layout(grid_box," in main_cpp + assert main_cpp.count("lv_obj_set_layout(grid_box,") == 1 + + +def test_top_level_layout_targets_active_screen(main_cpp: str) -> None: + """A top-level ``lvgl.update: { layout: ... }`` applies to the active + screen, not to the LVGL component object.""" + assert ( + "lv_obj_set_flex_flow(lvgl_id->get_screen_active(), LV_FLEX_FLOW_COLUMN)" + in main_cpp + ) + assert ( + "lv_obj_set_style_pad_column(lvgl_id->get_screen_active(), 5, LV_STATE_DEFAULT)" + in main_cpp + ) + + +def test_top_layer_layout_applied(main_cpp: str) -> None: + """A layout under ``top_layer`` is applied to the display's top layer.""" + assert "lv_display_get_layer_top(lvgl_id->get_disp())" in main_cpp + assert "lv_obj_set_flex_flow(top_layer_VAR_, LV_FLEX_FLOW_ROW)" in main_cpp + + +def test_bottom_layer_styling_applied(main_cpp: str) -> None: + """A ``bottom_layer`` style update generates code (previously the layer + keys of ``lvgl.update`` were silently ignored).""" + assert "lv_display_get_layer_bottom(lvgl_id->get_disp())" in main_cpp + assert ( + "lv_obj_set_style_bg_color(bottom_layer_VAR_, lv_color_make(18, 52, 86), " + "LV_PART_MAIN)" in main_cpp + ) diff --git a/tests/component_tests/lvgl/test_multi_conf_validate.py b/tests/component_tests/lvgl/test_multi_conf_validate.py new file mode 100644 index 0000000000..b63b7618e7 --- /dev/null +++ b/tests/component_tests/lvgl/test_multi_conf_validate.py @@ -0,0 +1,55 @@ +"""Tests for LVGL's multi-instance config cross-checks.""" + +from __future__ import annotations + +import pytest + +from esphome.components.lvgl import defines as df, multi_conf_validate +from esphome.components.lvgl.schemas import theme_schema +from esphome.config_validation import Invalid + + +def _config(displays: list[str], theme: dict | None = None) -> dict: + config = { + df.CONF_DISPLAYS: displays, + "log_level": "WARN", + "color_depth": 16, + "byte_order": "big_endian", + df.CONF_TRANSPARENCY_KEY: 0x000400, + } + if theme is not None: + config[df.CONF_THEME] = theme + return config + + +class TestThemeOnMultipleInstances: + def test_raises_when_two_instances_have_theme(self) -> None: + configs = [ + _config(["disp_a"], theme={df.CONF_DARK_MODE: True}), + _config(["disp_b"], theme={df.CONF_DARK_MODE: False}), + ] + with pytest.raises(Invalid, match="'theme' may only be set on one"): + multi_conf_validate(configs) + + def test_raises_even_with_an_empty_theme_block(self) -> None: + # `theme: {}` still creates a CONF_THEME key (with dark_mode defaulted + # by the schema), so it should be treated the same as a populated one. + # Run it through the real schema rather than hand-building the dict, + # so this actually pins that defaulting behaviour. + configs = [ + _config(["disp_a"], theme=theme_schema({})), + _config(["disp_b"], theme=theme_schema({})), + ] + with pytest.raises(Invalid, match="'theme' may only be set on one"): + multi_conf_validate(configs) + + def test_passes_when_only_one_instance_has_theme(self) -> None: + configs = [ + _config(["disp_a"], theme={df.CONF_DARK_MODE: True}), + _config(["disp_b"]), + ] + multi_conf_validate(configs) + + def test_passes_when_no_instance_has_theme(self) -> None: + configs = [_config(["disp_a"]), _config(["disp_b"])] + multi_conf_validate(configs) diff --git a/tests/component_tests/lvgl/test_paused.py b/tests/component_tests/lvgl/test_paused.py new file mode 100644 index 0000000000..eede17ec19 --- /dev/null +++ b/tests/component_tests/lvgl/test_paused.py @@ -0,0 +1,35 @@ +"""Tests for the LVGL ``paused`` option code generation.""" + +from __future__ import annotations + +import re + +_SET_PAUSED_RE = re.compile(r"->set_paused\((.+?)\);") + + +def _extract_set_paused(main_cpp: str) -> list[str]: + """Return the normalised argument text of every set_paused() call found. + + Whitespace within and around the arguments is collapsed so unrelated + code-generation formatting changes don't break these tests. + """ + return [" ".join(m.group(1).split()) for m in _SET_PAUSED_RE.finditer(main_cpp)] + + +class TestPausedCodeGeneration: + """Verify that the ``paused`` option drives the set_paused() call.""" + + def test_paused_true_generates_set_paused( + self, generate_main, component_config_path + ): + """``paused: true`` emits a set_paused(true, false) call.""" + main_cpp = generate_main(component_config_path("paused.yaml")) + calls = _extract_set_paused(main_cpp) + assert calls == ["true, false"] + + def test_paused_default_omits_set_paused( + self, generate_main, component_config_path + ): + """Without ``paused`` (default false) no set_paused call is generated.""" + main_cpp = generate_main(component_config_path("not_paused.yaml")) + assert _extract_set_paused(main_cpp) == [] diff --git a/tests/component_tests/lvgl/test_schema_dict_helpers.py b/tests/component_tests/lvgl/test_schema_dict_helpers.py index 16714f54d7..c8b3a76bf9 100644 --- a/tests/component_tests/lvgl/test_schema_dict_helpers.py +++ b/tests/component_tests/lvgl/test_schema_dict_helpers.py @@ -13,12 +13,7 @@ import pytest import voluptuous as vol from esphome import config_validation as cv -import esphome.components.lvgl -from esphome.components.lvgl import ( - _theme_schema, - defines as df, - schemas as lvgl_schemas, -) +from esphome.components.lvgl import defines as df, schemas as lvgl_schemas from esphome.components.lvgl.schemas import ( ALIGN_TO_SCHEMA, FLAG_SCHEMA, @@ -31,6 +26,8 @@ from esphome.components.lvgl.schemas import ( obj_schema, part_dict, part_schema, + theme_schema, + theme_update_schema, ) from esphome.components.lvgl.types import LvType from esphome.components.lvgl.widgets import WidgetType @@ -43,7 +40,7 @@ def _clear_obj_dict_cache() -> Generator[None]: cache.clear() # The lazily-built theme schema is cached on _build_theme_schema; clear it # too so each test starts from a clean slate. - build_theme = getattr(esphome.components.lvgl, "_build_theme_schema", None) + build_theme = getattr(lvgl_schemas, "_build_theme_schema", None) if build_theme is not None and hasattr(build_theme, "cache_clear"): build_theme.cache_clear() yield @@ -173,12 +170,12 @@ def test_spread_sources_carry_no_extra_schemas(schema: cv.Schema) -> None: def test_theme_schema_merges_obj_dict_and_full_style_props() -> None: - # _theme_schema is the riskiest merge: obj_dict(w) and FULL_STYLE_SCHEMA.schema + # theme_schema is the riskiest merge: obj_dict(w) and FULL_STYLE_SCHEMA.schema # share many STYLE_SCHEMA marker instances. Exercise the merged schema # end-to-end with one key from each side (a STATE_SCHEMA part from obj_dict # and a FULL_STYLE-only property) to lock the behaviour against future # regressions in either source. - out = _theme_schema( + out = theme_schema( { df.CONF_DARK_MODE: True, "obj": { @@ -202,7 +199,7 @@ def test_theme_schema_self_heals_when_a_widget_type_is_registered_later() -> Non # any_widget_schema explicitly supports external components registering # widgets lazily, and the device builder revalidates in-process, so a # widget registered after first use must invalidate the cached snapshot. - _theme_schema({df.CONF_DARK_MODE: True}) # populate the cache + theme_schema({df.CONF_DARK_MODE: True}) # populate the cache name = "test_self_heal_widget" assert name not in WIDGET_TYPES @@ -210,18 +207,68 @@ def test_theme_schema_self_heals_when_a_widget_type_is_registered_later() -> Non # manually so the next theme call sees the new entry. WIDGET_TYPES[name] = WidgetType(name, LvType("test_fake_t"), (), is_mock=True) try: - out = _theme_schema({df.CONF_DARK_MODE: False, name: {"bg_color": 0x010203}}) + out = theme_schema({df.CONF_DARK_MODE: False, name: {"bg_color": 0x010203}}) assert out[name]["bg_color"] == 0x010203 finally: WIDGET_TYPES.pop(name, None) +@pytest.mark.parametrize( + ("config", "expected_path"), + [ + ({"button": {"styles": ["foo"]}}, ["button", "styles"]), + ( + {"button": {"pressed": {"styles": ["foo"]}}}, + ["button", "pressed", "styles"], + ), + ( + {"arc": {"indicator": {"styles": ["foo"]}}}, + ["arc", "indicator", "styles"], + ), + ( + {"arc": {"indicator": {"pressed": {"styles": ["foo"]}}}}, + ["arc", "indicator", "pressed", "styles"], + ), + ], +) +def test_theme_schema_rejects_styles_key( + config: dict, expected_path: list[str] +) -> None: + # `styles:` (references to named styles) is accepted by FULL_STYLE_SCHEMA + # but silently dropped by style_set when building a theme's hidden style + # -- it only walks ALL_STYLES. Reject it instead of quietly doing nothing, + # at the top level and when nested under a part and/or state. + with pytest.raises(vol.Invalid, match="'styles:' is not allowed") as exc_info: + theme_schema(config) + assert exc_info.value.path == expected_path + + +def test_theme_update_schema_rejects_styles_key() -> None: + with pytest.raises(vol.Invalid, match="'styles:' is not allowed") as exc_info: + theme_update_schema({"label": {"styles": ["foo"]}}) + assert exc_info.value.path == ["label", "styles"] + + +def test_theme_update_schema_does_not_request_untargeted_main_default() -> None: + # collect_parts() unconditionally seeds a main/default entry even when + # only a specific state (here "pressed") was targeted -- registering a + # request for that spurious entry would make theme_to_code create an + # unused, empty style and attach it to every widget of this type. + theme_update_schema({"label": {"pressed": {"text_color": 0x010203}}}) + assert df.get_theme_update_requests()["label"] == {("main", "pressed"): None} + + +def test_theme_update_schema_requests_explicit_main_default() -> None: + theme_update_schema({"label": {"text_color": 0x010203}}) + assert df.get_theme_update_requests()["label"] == {("main", "default"): None} + + @pytest.mark.parametrize( "schema", [STATE_SCHEMA, FLAG_SCHEMA, STYLE_SCHEMA, FULL_STYLE_SCHEMA], ) def test_spread_sources_have_no_top_level_marker_defaults(schema: cv.Schema) -> None: - # _theme_schema merges obj_dict(w) with FULL_STYLE_SCHEMA.schema; on a key + # theme_schema merges obj_dict(w) with FULL_STYLE_SCHEMA.schema; on a key # collision, dict-spread keeps the first source's marker (and its default) # but the last source's value, whereas .extend() would take both from the # later source. The two are equivalent today because the overlapping diff --git a/tests/component_tests/micro_wake_word/__init__.py b/tests/component_tests/micro_wake_word/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/micro_wake_word/test_init.py b/tests/component_tests/micro_wake_word/test_init.py new file mode 100644 index 0000000000..5e57653585 --- /dev/null +++ b/tests/component_tests/micro_wake_word/test_init.py @@ -0,0 +1,110 @@ +"""Tests for micro_wake_word local model validation.""" + +import json +import logging +from pathlib import Path +from typing import Any + +import pytest + +from esphome.components.micro_wake_word import LOCAL_SCHEMA +from esphome.core import CORE + +MANIFEST: dict[str, Any] = { + "type": "micro", + "model": "hey_jarvis.tflite", + "author": "someone", + "version": 2, + "wake_word": "hey jarvis", + "trained_languages": ["en"], + "micro": { + "feature_step_size": 10, + "tensor_arena_size": 30000, + "probability_cutoff": 0.97, + "sliding_window_size": 5, + "minimum_esphome_version": "2024.7.0", + }, +} + + +def _registered_files() -> list[Path]: + """Files components registered for bundling this run.""" + data = CORE.data.get("bundle") + return list(data.extra_files) if data else [] + + +@pytest.fixture +def config_dir(tmp_path: Path) -> Path: + """A config dir holding a manifest and its model file.""" + (tmp_path / "models").mkdir() + (tmp_path / "models" / "hey_jarvis.tflite").write_bytes(b"fake model") + (tmp_path / "models" / "hey_jarvis.json").write_text(json.dumps(MANIFEST)) + CORE.config_path = tmp_path / "test.yaml" + return tmp_path + + +def test_local_schema_registers_model_file(config_dir: Path) -> None: + """The model file named by the manifest is registered so bundles include it.""" + LOCAL_SCHEMA({"path": "models/hey_jarvis.json"}) + + assert _registered_files() == [config_dir / "models" / "hey_jarvis.tflite"] + + +def test_local_schema_registers_model_file_in_subdirectory(config_dir: Path) -> None: + """The model reference is resolved relative to the manifest, not the config dir.""" + nested = config_dir / "models" / "nested" + nested.mkdir() + (nested / "model.tflite").write_bytes(b"fake model") + (config_dir / "models" / "nested.json").write_text( + json.dumps({**MANIFEST, "model": "nested/model.tflite"}) + ) + + LOCAL_SCHEMA({"path": "models/nested.json"}) + + assert _registered_files() == [nested / "model.tflite"] + + +def test_local_schema_leaves_config_untouched(config_dir: Path) -> None: + """Registration is a side effect; the model file is not a config key.""" + config = LOCAL_SCHEMA({"path": "models/hey_jarvis.json"}) + + assert config == {"path": config_dir / "models" / "hey_jarvis.json"} + + +def test_local_schema_missing_model_file_still_validates(config_dir: Path) -> None: + """A model file that does not exist is registered, not rejected. + + Raising here would be swallowed by the shorthand validator, which would then + report a confusing error about a missing file in a git repository. + """ + (config_dir / "models" / "hey_jarvis.tflite").unlink() + + LOCAL_SCHEMA({"path": "models/hey_jarvis.json"}) + + assert _registered_files() == [config_dir / "models" / "hey_jarvis.tflite"] + + +@pytest.mark.parametrize( + "contents", + [ + pytest.param("{not valid json", id="malformed"), + pytest.param(json.dumps({"type": "micro"}), id="no_model_key"), + pytest.param(json.dumps(["a", "list"]), id="not_an_object"), + pytest.param(json.dumps({"model": 42}), id="model_not_a_string"), + ], +) +def test_local_schema_bad_manifest_does_not_raise( + config_dir: Path, contents: str, caplog: pytest.LogCaptureFixture +) -> None: + """Manifest problems are left to later stages, which report them better. + + The skipped registration is logged so a bundle built without the model file can + be diagnosed. + """ + (config_dir / "models" / "hey_jarvis.json").write_text(contents) + + with caplog.at_level(logging.DEBUG): + LOCAL_SCHEMA({"path": "models/hey_jarvis.json"}) + + assert _registered_files() == [] + assert "Not registering a model file" in caplog.text diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index c14abdb4fd..6259d85184 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -1,6 +1,7 @@ """Tests for mpi_dsi configuration validation.""" from collections.abc import Callable +import logging from pathlib import Path import pytest @@ -71,6 +72,18 @@ def test_configuration_errors(set_core_config: SetCoreConfigCallable) -> None: } ) + # DSI displays cannot swap axes; enabling swap_xy reports a clear error. + with pytest.raises(cv.Invalid, match="'swap_xy' is not supported by this model"): + CONFIG_SCHEMA( + { + "model": "custom", + "init_sequence": [[0xA0, 0x01]], + "lane_bit_rate": "1.5Gbps", + "dimensions": {"width": 320, "height": 240}, + "transform": {"mirror_x": True, "mirror_y": True, "swap_xy": True}, + } + ) + def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None: """Test successful configuration validation.""" @@ -116,6 +129,59 @@ def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None: CONFIG_SCHEMA(config) +def test_deprecated_model_warning( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """The deprecated M5Stack-Tab5-v2 alias warns and points at the replacement models.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-p4-evboard", KEY_VARIANT: VARIANT_ESP32P4}, + ) + + from esphome.components.mipi_dsi.display import CONFIG_SCHEMA + + with caplog.at_level(logging.WARNING): + CONFIG_SCHEMA({"id": "deprecated_display", "model": "M5Stack-Tab5-v2"}) + assert "M5STACK-TAB5-V2 is deprecated" in caplog.text + # The warning names the replacement models so users know what to switch to. + assert "M5STACK-TAB5-ST7123" in caplog.text + + # The replacement models validate without emitting a deprecation warning. + caplog.clear() + with caplog.at_level(logging.WARNING): + CONFIG_SCHEMA({"id": "st7123_display", "model": "M5Stack-Tab5-ST7123"}) + CONFIG_SCHEMA({"id": "st7121_display", "model": "M5Stack-Tab5-ST7121"}) + assert "deprecated" not in caplog.text + + +def test_metadata_records_rotation(set_core_config: SetCoreConfigCallable) -> None: + """A configured display rotation is recorded in the metadata. + + LVGL relies on this to flag a rotation set in the display config (see the + mipi_spi tests for the end-to-end LVGL rejection). + """ + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-p4-evboard", KEY_VARIANT: VARIANT_ESP32P4}, + ) + + from esphome.components.display import get_display_metadata + from esphome.components.mipi_dsi.display import CONFIG_SCHEMA + + base = { + "model": "custom", + "init_sequence": [[0xA0, 0x01]], + "lane_bit_rate": "1.5Gbps", + "dimensions": {"width": 320, "height": 240}, + } + config = CONFIG_SCHEMA({**base, "id": "rotated", "rotation": 90}) + assert get_display_metadata(config["id"]).rotation == 90 + + config = CONFIG_SCHEMA({**base, "id": "unrotated"}) + assert get_display_metadata(config["id"]).rotation == 0 + + def test_code_generation( generate_main: Callable[[str | Path], str], component_fixture_path: Callable[[str], Path], diff --git a/tests/component_tests/mipi_rgb/__init__.py b/tests/component_tests/mipi_rgb/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/mipi_rgb/test_init.py b/tests/component_tests/mipi_rgb/test_init.py new file mode 100644 index 0000000000..0ab6c022e6 --- /dev/null +++ b/tests/component_tests/mipi_rgb/test_init.py @@ -0,0 +1,89 @@ +"""Tests for mipi_rgb configuration validation, in particular the per-model +``requires`` component check (see esphome.components.mipi.DriverChip.check_requirements).""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32S3 +from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA + +# Importing pca9554 registers its pin schema with pins.PIN_SCHEMA_REGISTRY so that +# models (e.g. SEEED-INDICATOR-D1) that reference pca9554-backed pins in their +# defaults can be validated by the mipi_rgb CONFIG_SCHEMA in this test. +import esphome.components.pca9554 # noqa: F401 +from esphome.const import PlatformFramework +from esphome.core import CORE +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + + +def _validated(config: ConfigType) -> ConfigType: + """Run the component config schema followed by the final validation.""" + config = CONFIG_SCHEMA(config) + FINAL_VALIDATE_SCHEMA(config) + return config + + +def test_model_requires_psram(set_core_config: SetCoreConfigCallable) -> None: + """A model known to have PSRAM on its board rejects a config without it. + + RGB parallel displays always need a full framebuffer, so every model in this + component is expected to carry ``requires={"psram", ...}``. This board has no + other requirements, so its check is exercised in isolation here. + """ + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, + ) + CORE.raw_config = {} + + with pytest.raises( + cv.Invalid, + match=r"ESP32-8048S070 requires component 'psram' to be configured", + ): + _validated({"model": "ESP32-8048S070"}) + + +def test_model_requires_psram_satisfied( + set_core_config: SetCoreConfigCallable, + set_component_config: Any, +) -> None: + """The same board model validates once PSRAM is configured.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, + ) + set_component_config("psram", True) + CORE.raw_config = {"psram": True} + + config = _validated({"model": "ESP32-8048S070"}) + assert config["model"] == "ESP32-8048S070" + + +def test_model_requires_psram_and_expander( + set_core_config: SetCoreConfigCallable, + set_component_config: Any, +) -> None: + """A model that also depends on an I2C GPIO expander lists both when missing.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, + ) + # Only satisfy one of the two requirements. + set_component_config("psram", True) + CORE.raw_config = {"psram": True} + + with pytest.raises( + cv.Invalid, + match=r"SEEED-INDICATOR-D1 requires component 'pca9554' to be configured", + ): + _validated( + { + "model": "SEEED-INDICATOR-D1", + "spi_id": "spi_bus", + } + ) diff --git a/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py new file mode 100644 index 0000000000..e85327c0ab --- /dev/null +++ b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py @@ -0,0 +1,137 @@ +"""Tests for mipi_rgb configuration validation.""" + +import pytest + +from esphome import config_validation as cv + +# Importing these registers their pin schemas with pins.PIN_SCHEMA_REGISTRY so that +# models referencing IO-expander-backed pins in their defaults (e.g. the LilyGO +# T-RGB boards via xl9535, SEEED-INDICATOR-D1 via pca9554, or the Waveshare panels +# via ch422g) can be validated by the mipi_rgb CONFIG_SCHEMA in this test. +import esphome.components.ch422g # noqa: F401 +from esphome.components.display import get_display_metadata +from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32S3 +import esphome.components.pca9554 # noqa: F401 +import esphome.components.xl9535 # noqa: F401 +from esphome.const import ( + CONF_BLUE, + CONF_DIMENSIONS, + CONF_GREEN, + CONF_HEIGHT, + CONF_INIT_SEQUENCE, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_RED, + CONF_SWAP_XY, + CONF_WIDTH, + KEY_VARIANT, + PlatformFramework, +) +from tests.component_tests.types import SetCoreConfigCallable + +# A generic set of data pins so that models without a default pin assignment +# (e.g. CUSTOM and RPI) still validate. +DATA_PINS = { + CONF_RED: [1, 2, 3, 4, 5], + CONF_GREEN: [6, 7, 8, 9, 10, 11], + CONF_BLUE: [12, 13, 14, 15, 16], +} + + +def _set_s3(set_core_config: SetCoreConfigCallable) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={ + KEY_BOARD: "esp32-s3-devkitc-1", + KEY_VARIANT: VARIANT_ESP32S3, + }, + ) + + +def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None: + """Every predefined model validates once required defaults are supplied.""" + _set_s3(set_core_config) + + from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS + + for name, model in MODELS.items(): + config = {"model": name, "data_pins": DATA_PINS, "pclk_pin": 21} + if model.initsequence is None: + config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]] + if not model.get_default(CONF_WIDTH): + config[CONF_DIMENSIONS] = {CONF_WIDTH: 480, CONF_HEIGHT: 480} + CONFIG_SCHEMA(config) + + +def test_transform_matches_model_support( + set_core_config: SetCoreConfigCallable, +) -> None: + """The transform schema only accepts the axes a model actually supports.""" + _set_s3(set_core_config) + + from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS + + # ESP32-8048S070 supports both mirror axes but not swap_xy (RGB displays + # never support axis swapping). + model = MODELS["ESP32-8048S070"] + assert model.transforms == {CONF_MIRROR_X, CONF_MIRROR_Y} + + base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21} + CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": False}}) + + # An unsupported axis may be explicitly disabled (a harmless no-op)... + CONFIG_SCHEMA( + {**base, "transform": {"mirror_x": True, "mirror_y": False, "swap_xy": False}} + ) + + # ...but enabling it reports a clear, model-specific error. + with pytest.raises(cv.Invalid, match="'swap_xy' is not supported by this model"): + CONFIG_SCHEMA( + { + **base, + "transform": {"mirror_x": True, "mirror_y": False, "swap_xy": True}, + } + ) + + +def test_st7701s_only_supports_mirror_x( + set_core_config: SetCoreConfigCallable, +) -> None: + """ST7701S panels shorter than full height only expose mirror_x. + + mirror_y only works at full height (864px), so the LilyGO 480px panels must + reject a mirror_y transform. + """ + _set_s3(set_core_config) + + from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS + + model = MODELS["T-RGB-2.1"] + assert model.transforms == {CONF_MIRROR_X} + assert CONF_SWAP_XY not in model.transforms + + base = {"model": "T-RGB-2.1"} + CONFIG_SCHEMA({**base, "transform": {"mirror_x": True}}) + + with pytest.raises(cv.Invalid, match="'mirror_y' is not supported by this model"): + CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": True}}) + + +def test_metadata_records_rotation( + set_core_config: SetCoreConfigCallable, +) -> None: + """A configured display rotation is recorded in the metadata. + + LVGL relies on this to flag a rotation set in the display config (see the + mipi_spi tests for the end-to-end LVGL rejection). + """ + _set_s3(set_core_config) + + from esphome.components.mipi_rgb.display import CONFIG_SCHEMA + + base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21} + config = CONFIG_SCHEMA({**base, "id": "rotated", "rotation": 90}) + assert get_display_metadata(config["id"]).rotation == 90 + + config = CONFIG_SCHEMA({**base, "id": "unrotated"}) + assert get_display_metadata(config["id"]).rotation == 0 diff --git a/tests/component_tests/mipi_spi/test_display_metadata.py b/tests/component_tests/mipi_spi/test_display_metadata.py index e7f5143d91..06cc8ee09a 100644 --- a/tests/component_tests/mipi_spi/test_display_metadata.py +++ b/tests/component_tests/mipi_spi/test_display_metadata.py @@ -3,6 +3,9 @@ from collections.abc import Callable from pathlib import Path +import pytest + +from esphome import config_validation as cv from esphome.components.const import BYTE_ORDER_BIG from esphome.components.display import get_all_display_metadata, get_display_metadata from esphome.components.esp32 import ( @@ -13,6 +16,7 @@ from esphome.components.esp32 import ( ) from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA from esphome.const import PlatformFramework +from esphome.core import ID from tests.component_tests.types import SetCoreConfigCallable @@ -23,6 +27,18 @@ def validated_config(config): return config +def _lvgl_config(display_id: str) -> dict: + """Build a minimal LVGL config dict referencing the given display id.""" + return { + "displays": [ID(display_id, True)], + "log_level": "WARN", + "color_depth": 16, + "transparency_key": 0x000400, + "draw_rounding": 2, + "buffer_size": 0, + } + + def test_metadata_native_quad_default_test_card( set_core_config: SetCoreConfigCallable, ) -> None: @@ -91,7 +107,7 @@ def test_metadata_no_swap_xy_not_full_hardware_rotation( PlatformFramework.ESP32_IDF, platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) - # JC3248W535 has swap_xy=cv.UNDEFINED -> transforms={mirror_x, mirror_y} only + # JC3248W535 has transforms={mirror_x, mirror_y} only config = CONFIG_SCHEMA({"model": "JC3248W535", "id": "jc3248w535"}) meta = get_display_metadata(config["id"]) assert meta is not None @@ -166,3 +182,69 @@ def test_metadata_via_code_generation_lvgl( assert meta.height == 160 assert meta.has_hardware_rotation is True assert meta.byte_order == BYTE_ORDER_BIG + + +def test_metadata_records_rotation( + set_core_config: SetCoreConfigCallable, +) -> None: + """A configured display rotation is recorded in the metadata.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config = CONFIG_SCHEMA( + {"model": "ST7735", "dc_pin": 18, "id": "rotated", "rotation": 90} + ) + meta = get_display_metadata(config["id"]) + assert meta is not None + assert meta.rotation == 90 + + +def test_metadata_rotation_defaults_to_zero( + set_core_config: SetCoreConfigCallable, +) -> None: + """A display without a rotation reports rotation 0 in its metadata.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config = CONFIG_SCHEMA({"model": "ST7735", "dc_pin": 18, "id": "unrotated"}) + meta = get_display_metadata(config["id"]) + assert meta is not None + assert meta.rotation == 0 + + +def test_rotation_flagged_when_used_with_lvgl( + set_core_config: SetCoreConfigCallable, +) -> None: + """A display with a rotation is rejected when driven by LVGL. + + LVGL manages its own rotation, so a rotation set in the display config must be + flagged and the user directed to configure it in the LVGL block instead. This + exercises the full chain: the mipi_spi schema records the rotation in the + display metadata, and LVGL's final validation reports it. + """ + from esphome.components.lvgl import final_validation + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + CONFIG_SCHEMA({"model": "ST7735", "dc_pin": 18, "id": "rotated", "rotation": 90}) + with pytest.raises(cv.Invalid, match="rotation.*not compatible with LVGL"): + final_validation([_lvgl_config("rotated")]) + + +def test_no_rotation_accepted_with_lvgl( + set_core_config: SetCoreConfigCallable, +) -> None: + """A display without a rotation validates cleanly when driven by LVGL.""" + from esphome.components.lvgl import final_validation + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + CONFIG_SCHEMA({"model": "ST7735", "dc_pin": 18, "id": "unrotated"}) + # Should not raise. + final_validation([_lvgl_config("unrotated")]) diff --git a/tests/component_tests/mipi_spi/test_final_validate.py b/tests/component_tests/mipi_spi/test_final_validate.py index 8c45b47752..77111ae867 100644 --- a/tests/component_tests/mipi_spi/test_final_validate.py +++ b/tests/component_tests/mipi_spi/test_final_validate.py @@ -6,10 +6,13 @@ from typing import Any import pytest +from esphome import config_validation as cv from esphome.components.display import CONF_SHOW_TEST_CARD from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32 +from esphome.components.mipi import DriverChip from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA from esphome.const import CONF_BUFFER_SIZE, PlatformFramework +from esphome.core import CORE from esphome.types import ConfigType from tests.component_tests.types import SetCoreConfigCallable @@ -183,3 +186,77 @@ def test_buffer_size_selected_when_lvgl_with_test_card( ) assert config[CONF_BUFFER_SIZE] == pytest.approx(1.0 / 4) + + +def test_requires_missing_single_component_raises() -> None: + """A model that requires a single component raises when it is absent.""" + CORE.raw_config = {} + chip = DriverChip("TEST-REQUIRES-PSRAM", requires={"psram"}) + + with pytest.raises( + cv.Invalid, + match=r"TEST-REQUIRES-PSRAM requires component 'psram' to be configured", + ): + chip.check_requirements() + + +def test_requires_missing_multiple_components_raises() -> None: + """A model that requires several components lists all the missing ones, pluralized.""" + CORE.raw_config = {} + chip = DriverChip("TEST-REQUIRES-MULTI", requires={"psram", "pca9554"}) + + with pytest.raises( + cv.Invalid, + match=r"TEST-REQUIRES-MULTI requires components '.*' to be configured", + ) as excinfo: + chip.check_requirements() + assert "psram" in str(excinfo.value) + assert "pca9554" in str(excinfo.value) + + +def test_requires_satisfied_does_not_raise() -> None: + """No error is raised once all the required components are configured.""" + CORE.raw_config = {"psram": True, "pca9554": []} + chip = DriverChip("TEST-REQUIRES-SATISFIED", requires={"psram", "pca9554"}) + + chip.check_requirements() # Should not raise + + +def test_requires_absent_does_not_raise() -> None: + """Models without a requires set are unaffected by the check.""" + CORE.raw_config = {} + chip = DriverChip("TEST-REQUIRES-NONE") + + chip.check_requirements() # Should not raise + + +def test_predefined_model_requires_psram( + set_core_config: SetCoreConfigCallable, +) -> None: + """A predefined board model known to have PSRAM rejects a config without it.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + CORE.raw_config = {} + + with pytest.raises( + cv.Invalid, match=r"S3BOX requires component 'psram' to be configured" + ): + _validated({"model": "s3box"}) + + +def test_predefined_model_requires_psram_satisfied( + set_core_config: SetCoreConfigCallable, + set_component_config: Any, +) -> None: + """The same board model validates once PSRAM is configured.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + set_component_config("psram", True) + CORE.raw_config = {"psram": True} + + config = _validated({"model": "s3box"}) + assert config["model"] == "S3BOX" diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index 8edbe095b7..f29883684c 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -136,7 +136,7 @@ def test_dimension_validation( "model": "JC3248W535", "transform": {"mirror_x": False, "mirror_y": True, "swap_xy": True}, }, - "Axis swapping not supported by this model", + "'swap_xy' is not supported by this model", id="axis_swapping_not_supported", ), pytest.param( @@ -361,7 +361,8 @@ def test_native_generation( "mipi_spi::MipiSpiBuffer()" in main_cpp ) - assert "set_init_sequence({240, 1, 8, 242" in main_cpp + # A 10ms post-reset delay ({10, 255}) is prepended ahead of the model commands. + assert "set_init_sequence({10, 255, 240, 1, 8, 242" in main_cpp assert "show_test_card();" in main_cpp assert "set_write_only(true);" in main_cpp @@ -377,6 +378,76 @@ def test_lvgl_generation( "mipi_spi::MipiSpi();" in main_cpp ) - assert "set_init_sequence({177, 3, 1, 44, 45, 178" in main_cpp + # A 10ms post-reset delay ({10, 255}) is prepended ahead of the model commands. + assert "set_init_sequence({10, 255, 177, 3, 1, 44, 45, 178" in main_cpp assert "show_test_card();" not in main_cpp assert "set_auto_clear(false);" in main_cpp + + +# A 10ms delay (flattened to {10, 0xFF}, where 0xFF is the delay marker byte) is +# always prepended to the init sequence, since both a software and a hardware reset +# need to settle before further commands. A custom model has no reset_pin default +# and does not set no_swreset, so when no reset pin is configured the SWRESET command +# ({1, 0}: command 0x01 with no parameters) is prepended ahead of that delay. +_SWRESET_YAML = """ +esphome: + name: swreset-test +esp32: + board: esp32-s3-devkitc-1 + framework: + type: esp-idf +spi: + clk_pin: 1 + mosi_pin: 2 +display: + - platform: mipi_spi + model: custom + id: {display_id} + dc_pin: 4 + cs_pin: 8 + dimensions: + width: 320 + height: 240 + init_sequence: + - [0xA0, 0x01] +{reset_line} +""" + + +def test_swreset_prepended_without_reset_pin( + generate_main: Callable[[str | Path], str], + tmp_path: Path, +) -> None: + """A model with no reset pin (and no no_swreset) gets SWRESET prepended.""" + yaml_file = tmp_path / "swreset.yaml" + yaml_file.write_text( + _SWRESET_YAML.format(display_id="swreset_display", reset_line="") + ) + + main_cpp = generate_main(yaml_file) + + # SWRESET ({1, 0}) followed by a 10ms delay ({10, 255}) is inserted ahead of + # the model's own commands. + assert "swreset_display->set_init_sequence({1, 0, 10, 255, 160, 1, 1," in main_cpp + + +def test_swreset_not_prepended_with_reset_pin( + generate_main: Callable[[str | Path], str], + tmp_path: Path, +) -> None: + """A hardware reset pin performs the reset, so SWRESET must not be prepended. + + The post-reset delay is still required, so the sequence starts with the delay. + """ + yaml_file = tmp_path / "hwreset.yaml" + yaml_file.write_text( + _SWRESET_YAML.format( + display_id="hwreset_display", reset_line=" reset_pin: 5" + ) + ) + + main_cpp = generate_main(yaml_file) + + # The delay ({10, 255}) is still present, but no leading SWRESET ({1, 0}). + assert "hwreset_display->set_init_sequence({10, 255, 160, 1, 1," in main_cpp + assert "hwreset_display->set_init_sequence({1, 0," not in main_cpp diff --git a/tests/component_tests/mipi_spi/test_padding_and_offsets.py b/tests/component_tests/mipi_spi/test_padding_and_offsets.py index 7ae6f0e61f..b2b421c1e2 100644 --- a/tests/component_tests/mipi_spi/test_padding_and_offsets.py +++ b/tests/component_tests/mipi_spi/test_padding_and_offsets.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from pathlib import Path +from typing import Any import pytest @@ -222,6 +223,7 @@ class TestNewModelVariants: def test_m5core2_with_native_dimensions( self, set_core_config: SetCoreConfigCallable, + set_component_config: Callable[[str, Any], None], ) -> None: """Test M5CORE2 variant with reset native_width and native_height.""" set_core_config( @@ -231,6 +233,8 @@ class TestNewModelVariants: KEY_VARIANT: VARIANT_ESP32S3, }, ) + # M5CORE2 has PSRAM on board and requires it to be configured + set_component_config("psram", True) # M5CORE2 should validate successfully config = validated_config({"model": "M5CORE2"}) diff --git a/tests/component_tests/network/__init__.py b/tests/component_tests/network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/network/config/priority_ethernet_first.yaml b/tests/component_tests/network/config/priority_ethernet_first.yaml new file mode 100644 index 0000000000..d98d19f545 --- /dev/null +++ b/tests/component_tests/network/config/priority_ethernet_first.yaml @@ -0,0 +1,26 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +ethernet: + type: W5500 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + +network: + priority: + - ethernet + - wifi diff --git a/tests/component_tests/network/config/priority_wifi_first.yaml b/tests/component_tests/network/config/priority_wifi_first.yaml new file mode 100644 index 0000000000..65247f005f --- /dev/null +++ b/tests/component_tests/network/config/priority_wifi_first.yaml @@ -0,0 +1,26 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +ethernet: + type: W5500 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + +network: + priority: + - wifi + - ethernet diff --git a/tests/component_tests/network/config/wifi_only.yaml b/tests/component_tests/network/config/wifi_only.yaml new file mode 100644 index 0000000000..61dfde3e03 --- /dev/null +++ b/tests/component_tests/network/config/wifi_only.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" diff --git a/tests/component_tests/network/test_priority.py b/tests/component_tests/network/test_priority.py new file mode 100644 index 0000000000..da1c0a061d --- /dev/null +++ b/tests/component_tests/network/test_priority.py @@ -0,0 +1,201 @@ +"""Tests for the ``network: priority:`` list validator.""" + +from collections.abc import Callable +from pathlib import Path +import re + +import pytest +from voluptuous import Invalid + +from esphome.components.network import ( + _SETUP_PRIORITY_AFTER_WIFI, + KEY_NETWORK_PRIORITY, + NETWORK_PRIORITY_BASE, + NETWORK_PRIORITY_STEP, + _final_validate, + _validate_priority_list, + get_network_priority, +) +from esphome.const import CONF_PRIORITY +from esphome.core import CORE +import esphome.final_validate as fv + + +@pytest.fixture(autouse=True) +def _clear_core_data(): + """Wipe CORE.data and reset fv.full_config so each test starts clean.""" + CORE.data.clear() + token = fv.full_config.set({}) + yield + fv.full_config.reset(token) + CORE.data.clear() + + +def test_validates_plain_string_list() -> None: + result = _validate_priority_list(["ethernet", "wifi"]) + assert result == [{"interface": "ethernet"}, {"interface": "wifi"}] + + +def test_normalizes_mixed_case_to_lowercase() -> None: + # Regression check: mixed-case input must be lowercased so downstream + # callers like get_network_priority("ethernet") find a match. + result = _validate_priority_list(["Ethernet", "WIFI"]) + assert result == [{"interface": "ethernet"}, {"interface": "wifi"}] + + +def test_accepts_all_supported_interface_types() -> None: + # Only ethernet and wifi are currently accepted. Other interface types + # (openthread, modem) will be added when their setup-priority consumers + # land — see NETWORK_PLAN.md. + result = _validate_priority_list(["ethernet", "wifi"]) + assert [e["interface"] for e in result] == ["ethernet", "wifi"] + + +def test_rejects_not_yet_supported_interface() -> None: + # openthread / modem are in the long-term roadmap but no setup-priority + # consumer is wired yet, so VALID_NETWORK_TYPES excludes them today. + with pytest.raises(Invalid): + _validate_priority_list(["ethernet", "openthread"]) + with pytest.raises(Invalid): + _validate_priority_list(["wifi", "modem"]) + + +def test_single_interface_is_valid() -> None: + result = _validate_priority_list(["ethernet"]) + assert result == [{"interface": "ethernet"}] + + +def test_rejects_unknown_interface() -> None: + with pytest.raises(Invalid): + _validate_priority_list(["ethernet", "bluetooth"]) + + +def test_rejects_duplicate_entries() -> None: + with pytest.raises(Invalid, match="Duplicate entries"): + _validate_priority_list(["ethernet", "ethernet"]) + + +def test_rejects_duplicates_regardless_of_case() -> None: + # Same interface in mixed cases should still trip the duplicate check + # after normalization. + with pytest.raises(Invalid, match="Duplicate entries"): + _validate_priority_list(["ethernet", "Ethernet"]) + + +def test_rejects_mapping_form() -> None: + # The mapping form (- ethernet: { timeout: 30s }) was removed when the + # timeout option moved to its consumer PR. Verify we reject it cleanly + # instead of silently accepting a no-op. + with pytest.raises(Invalid): + _validate_priority_list([{"ethernet": {"timeout": "30s"}}]) + + +def test_get_network_priority_returns_none_when_unset() -> None: + assert get_network_priority("ethernet") is None + + +def test_get_network_priority_assigns_base_to_first_entry() -> None: + CORE.data[KEY_NETWORK_PRIORITY] = _validate_priority_list(["ethernet", "wifi"]) + assert get_network_priority("ethernet") == NETWORK_PRIORITY_BASE + + +def test_get_network_priority_steps_down_by_step_per_position() -> None: + CORE.data[KEY_NETWORK_PRIORITY] = _validate_priority_list(["ethernet", "wifi"]) + assert get_network_priority("wifi") == NETWORK_PRIORITY_BASE - NETWORK_PRIORITY_STEP + + +def test_get_network_priority_is_case_insensitive_on_query() -> None: + CORE.data[KEY_NETWORK_PRIORITY] = _validate_priority_list(["ethernet"]) + assert get_network_priority("Ethernet") == NETWORK_PRIORITY_BASE + + +def test_get_network_priority_returns_none_for_unlisted_interface() -> None: + CORE.data[KEY_NETWORK_PRIORITY] = _validate_priority_list(["ethernet"]) + assert get_network_priority("wifi") is None + + +def test_final_validate_rejects_priority_iface_without_component() -> None: + """An interface named in 'priority' with no matching component block is rejected.""" + # priority lists wifi, but only ethernet is present in the full config. + fv.full_config.set({"ethernet": {}}) + config = {CONF_PRIORITY: _validate_priority_list(["ethernet", "wifi"])} + with pytest.raises( + Invalid, match=r"'wifi' is listed in 'network: priority:' but no 'wifi:'" + ): + _final_validate(config) + + +def test_final_validate_accepts_when_all_priority_ifaces_present() -> None: + """No error when every interface in 'priority' has a matching component block.""" + fv.full_config.set({"ethernet": {}, "wifi": {}}) + config = {CONF_PRIORITY: _validate_priority_list(["ethernet", "wifi"])} + _final_validate(config) # must not raise + + +def test_final_validate_noop_without_priority_list() -> None: + """A network config without a 'priority' list imposes no component requirements.""" + fv.full_config.set({}) + _final_validate({}) # must not raise + + +def _cpp_setup_priority(name: str) -> float: + """Read a setup_priority constant straight from esphome/core/component.h.""" + header = Path(__file__).parents[3] / "esphome" / "core" / "component.h" + match = re.search( + rf"inline constexpr float {name} = ([\d.]+)f;", header.read_text() + ) + assert match is not None, f"setup_priority::{name} not found in component.h" + return float(match.group(1)) + + +def test_priority_band_constants_match_cpp_setup_priority() -> None: + """The Python priority-band constants mirror the C++ setup_priority values. + + NETWORK_PRIORITY_BASE must equal the historical setup_priority::WIFI / + ::ETHERNET default so a single-entry priority list reproduces the legacy + setup order, and the band guard must track setup_priority::AFTER_WIFI. + Reading the values from component.h turns a silent desync into a CI + failure if either side is ever rebalanced. + """ + assert _cpp_setup_priority("WIFI") == NETWORK_PRIORITY_BASE + assert _cpp_setup_priority("ETHERNET") == NETWORK_PRIORITY_BASE + assert _cpp_setup_priority("AFTER_WIFI") == _SETUP_PRIORITY_AFTER_WIFI + # Must stay below AFTER_BLUETOOTH (NetworkComponent's own priority) so + # interfaces never set up before esp_netif_init(). + assert _cpp_setup_priority("AFTER_BLUETOOTH") > NETWORK_PRIORITY_BASE + + +def test_wifi_first_priority_emits_primary_interface_define( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """A wifi-first priority list emits USE_NETWORK_PRIMARY_INTERFACE_WIFI.""" + generate_main(component_config_path("priority_wifi_first.yaml")) + defines = {d.name for d in CORE.defines} + assert "USE_NETWORK_PRIMARY_INTERFACE_WIFI" in defines + # Emitted by cg.set_setup_priority() at the wifi/ethernet call sites. + assert "USE_SETUP_PRIORITY_OVERRIDE" in defines + + +def test_ethernet_first_priority_emits_no_primary_interface_define( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Ethernet-first matches the built-in preference order, so no define is emitted.""" + generate_main(component_config_path("priority_ethernet_first.yaml")) + assert not any( + d.name.startswith("USE_NETWORK_PRIMARY_INTERFACE_") for d in CORE.defines + ) + # The setup-priority overrides themselves are still emitted. + assert "USE_SETUP_PRIORITY_OVERRIDE" in {d.name for d in CORE.defines} + + +def test_no_primary_interface_define_without_priority( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Without a priority list, no primary-interface define is emitted.""" + generate_main(component_config_path("wifi_only.yaml")) + assert not any( + d.name.startswith("USE_NETWORK_PRIMARY_INTERFACE_") for d in CORE.defines + ) diff --git a/tests/component_tests/online_image/__init__.py b/tests/component_tests/online_image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/online_image/config/online_image_platform_test.yaml b/tests/component_tests/online_image/config/online_image_platform_test.yaml new file mode 100644 index 0000000000..9b92bf75d0 --- /dev/null +++ b/tests/component_tests/online_image/config/online_image_platform_test.yaml @@ -0,0 +1,33 @@ +# New `image:` `platform: online_image` form. Exercises online_image/image.py +# through the real platform loader and codegen pipeline. +esphome: + name: test + +esp32: + board: esp32s3box + +wifi: + ssid: MySSID + password: password1 + +http_request: + verify_ssl: false + +image: + - platform: online_image + id: test_online_image + url: http://example.com/image.png + format: png + type: rgb565 + +spi: + mosi_pin: 6 + clk_pin: 7 + +psram: + mode: quad + +display: + - platform: mipi_spi + id: lcd_display + model: s3box diff --git a/tests/component_tests/online_image/config/online_image_test.yaml b/tests/component_tests/online_image/config/online_image_test.yaml new file mode 100644 index 0000000000..4af398cdff --- /dev/null +++ b/tests/component_tests/online_image/config/online_image_test.yaml @@ -0,0 +1,32 @@ +# Legacy top-level `online_image:` form. Exercises the deprecation shim and the +# shared codegen path through the real read_config/codegen pipeline. +esphome: + name: test + +esp32: + board: esp32s3box + +wifi: + ssid: MySSID + password: password1 + +http_request: + verify_ssl: false + +online_image: + - id: test_online_image + url: http://example.com/image.png + format: png + type: rgb565 + +spi: + mosi_pin: 6 + clk_pin: 7 + +psram: + mode: quad + +display: + - platform: mipi_spi + id: lcd_display + model: s3box diff --git a/tests/component_tests/online_image/test_init.py b/tests/component_tests/online_image/test_init.py new file mode 100644 index 0000000000..76b00ff5ff --- /dev/null +++ b/tests/component_tests/online_image/test_init.py @@ -0,0 +1,76 @@ +"""Tests for the online_image platform and the legacy `online_image:` shim.""" + +from __future__ import annotations + +from collections.abc import Callable +import logging +from pathlib import Path + +import pytest + +from esphome.components.online_image import ( + DOMAIN, + LEGACY_REMOVAL_VERSION, + _capture_legacy_entry, + _warn_legacy_online_image, +) +from esphome.core import CORE +from esphome.types import ConfigType + +# --------------------------------------------------------------------------- +# Legacy top-level `online_image:` deprecation shim -- REMOVE these tests after +# 2027.1.0 together with the shim in esphome/components/online_image/__init__.py. +# --------------------------------------------------------------------------- + + +def test_warn_legacy_online_image_warns_once( + caplog: pytest.LogCaptureFixture, +) -> None: + """The deprecation warning fires exactly once and never mutates the config.""" + config: ConfigType = {"id": "test_online_image", "url": "http://example.com/i.png"} + + # A per-entry capture (CONFIG_SCHEMA step) records the raw entry so the + # one-shot warning can print a pasteable migrated block. + assert _capture_legacy_entry(config) is config + + with caplog.at_level(logging.WARNING): + # First call: flag not yet set -> warns and records the flag. + assert _warn_legacy_online_image(config) is config + # Second call: flag already set -> stays silent (the dedup branch). + assert _warn_legacy_online_image(config) is config + + assert CORE.data[DOMAIN]["legacy_warning_shown"] is True + warnings = [r for r in caplog.records if r.levelno == logging.WARNING] + assert len(warnings) == 1 + assert "deprecated" in caplog.text + assert "platform: online_image" in caplog.text + assert LEGACY_REMOVAL_VERSION in caplog.text + + +def test_legacy_online_image_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + """The legacy `online_image:` block validates, warns, and generates codegen + through the real read_config/codegen pipeline.""" + with caplog.at_level(logging.WARNING): + main_cpp = generate_main(component_config_path("online_image_test.yaml")) + + # Deprecation warning surfaced through the real validation pipeline. + assert "online_image" in caplog.text + assert "deprecated" in caplog.text + + # setup_online_image ran: OnlineImage object constructed and parented. + assert "new(test_online_image) online_image::OnlineImage(" in main_cpp + + +def test_online_image_platform_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """The `image:` `platform: online_image` form generates codegen through the + real platform loader (online_image/image.py) without a deprecation warning.""" + main_cpp = generate_main(component_config_path("online_image_platform_test.yaml")) + + assert "new(test_online_image) online_image::OnlineImage(" in main_cpp diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 6990c1c051..39bffd31b7 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -1264,6 +1264,75 @@ def test_remote_packages_no_revert( ] +@patch("esphome.yaml_util.load_yaml") +@patch("pathlib.Path.is_file") +@patch("esphome.git.clone_or_update") +def test_remote_packages_skipped_revert_does_not_retry( + mock_clone_or_update, mock_is_file, mock_load_yaml +) -> None: + """When revert() reports the rollback was skipped, the load is not + retried (the checkout is unchanged) and the error says so.""" + mock_revert = MagicMock(return_value=False) + mock_clone_or_update.return_value = (Path("/tmp/noexists"), mock_revert) + mock_is_file.return_value = True + mock_load_yaml.side_effect = cv.Invalid("bad yaml") + + config = { + CONF_PACKAGES: { + "pkg": { + CONF_URL: "https://github.com/esphome/repo", + CONF_REF: "main", + CONF_FILES: [{CONF_PATH: "file.yaml"}], + CONF_REFRESH: "1d", + } + } + } + with pytest.raises(cv.Invalid, match="could not revert the cached checkout"): + packages_pass(config) + + assert mock_revert.call_count == 1 + assert mock_load_yaml.call_count == 1 + + +@patch("esphome.yaml_util.load_yaml") +@patch("pathlib.Path.is_file") +@patch("esphome.git.clone_or_update") +def test_remote_packages_successful_revert_retries( + mock_clone_or_update, mock_is_file, mock_load_yaml, caplog: pytest.LogCaptureFixture +) -> None: + """A successful revert retries the load against the reverted checkout and + logs the original error, the only trace that upstream was broken.""" + mock_revert = MagicMock(return_value=True) + mock_clone_or_update.return_value = (Path("/tmp/noexists"), mock_revert) + mock_is_file.return_value = True + mock_load_yaml.side_effect = [ + cv.Invalid("bad yaml"), + OrderedDict( + {CONF_SENSOR: [{CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"}]} + ), + ] + + config = { + CONF_PACKAGES: { + "pkg": { + CONF_URL: "https://github.com/esphome/repo", + CONF_REF: "main", + CONF_FILES: [{CONF_PATH: "file.yaml"}], + CONF_REFRESH: "1d", + } + } + } + with caplog.at_level(logging.WARNING): + actual = packages_pass(config) + + assert actual[CONF_SENSOR] == [ + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"} + ] + assert mock_revert.call_count == 1 + assert mock_load_yaml.call_count == 2 + assert any("reverted the cached checkout" in r.getMessage() for r in caplog.records) + + def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None: """Test that CORE.raw_config contains esphome section from merged package. diff --git a/tests/component_tests/provisioning/test_provisioning.py b/tests/component_tests/provisioning/test_provisioning.py new file mode 100644 index 0000000000..07f5065241 --- /dev/null +++ b/tests/component_tests/provisioning/test_provisioning.py @@ -0,0 +1,84 @@ +"""Tests for the provisioning component config validation.""" + +from __future__ import annotations + +import logging + +import pytest + +from esphome import config_validation as cv +from esphome.components.provisioning import ( + CONFIG_SCHEMA, + FINAL_VALIDATE_SCHEMA, + register_source, + report_hardcoded_credentials, +) +from esphome.const import CONF_TIMEOUT, PlatformFramework +from tests.component_tests.types import SetCoreConfigCallable + + +def test_provisioning_requires_a_source( + set_core_config: SetCoreConfigCallable, +) -> None: + """Provisioning with no registered source is a config error. + + Sources register themselves during their own config validation; with none + registered the window could never resolve, so validation fails. + """ + set_core_config(PlatformFramework.ESP32_IDF) + with pytest.raises(cv.Invalid, match="provisioning-capable component"): + FINAL_VALIDATE_SCHEMA({}) + + +def test_provisioning_accepts_a_registered_source( + set_core_config: SetCoreConfigCallable, +) -> None: + """A component that registered as a provisioning source satisfies validation.""" + set_core_config(PlatformFramework.ESP32_IDF) + register_source("network") + # Should not raise. + assert FINAL_VALIDATE_SCHEMA({}) == {} + + +def test_provisioning_warns_on_hardcoded_credentials( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """A source with credentials set in the config triggers a warning.""" + set_core_config(PlatformFramework.ESP32_IDF) + register_source("network") + report_hardcoded_credentials("wifi") + with caplog.at_level(logging.WARNING): + assert FINAL_VALIDATE_SCHEMA({}) == {} + assert "wifi" in caplog.text + assert "credentials" in caplog.text + + +def test_provisioning_no_warning_without_hardcoded_credentials( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """No credentials warning when no source reports hardcoded credentials.""" + set_core_config(PlatformFramework.ESP32_IDF) + register_source("network") + with caplog.at_level(logging.WARNING): + assert FINAL_VALIDATE_SCHEMA({}) == {} + assert "credentials" not in caplog.text + + +def test_provisioning_rejects_zero_timeout( + set_core_config: SetCoreConfigCallable, +) -> None: + """A zero timeout would leave the window open forever, so it is rejected.""" + set_core_config(PlatformFramework.ESP32_IDF) + with pytest.raises(cv.Invalid): + CONFIG_SCHEMA({CONF_TIMEOUT: "0s"}) + + +def test_provisioning_accepts_positive_timeout( + set_core_config: SetCoreConfigCallable, +) -> None: + """A positive timeout is accepted.""" + set_core_config(PlatformFramework.ESP32_IDF) + config = CONFIG_SCHEMA({CONF_TIMEOUT: "5min"}) + assert config[CONF_TIMEOUT].total_milliseconds == 300000 diff --git a/tests/component_tests/safe_mode/__init__.py b/tests/component_tests/safe_mode/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/safe_mode/test_safe_mode.py b/tests/component_tests/safe_mode/test_safe_mode.py new file mode 100644 index 0000000000..617a3b4855 --- /dev/null +++ b/tests/component_tests/safe_mode/test_safe_mode.py @@ -0,0 +1,45 @@ +"""Tests for the safe_mode component.""" + +from collections.abc import Callable + +from esphome.core import CORE + +SHUTDOWN_DEFINE = "USE_SAFE_MODE_BOOT_IS_GOOD_ON_SHUTDOWN" + + +def _has_define(name: str) -> bool: + return any(define.name == name for define in CORE.defines) + + +def test_boot_is_good_on_shutdown_default( + generate_main: Callable[[str], str], +) -> None: + """By default, an orderly shutdown confirms the app image.""" + main_cpp = generate_main( + "tests/component_tests/safe_mode/test_safe_mode_default.yaml" + ) + + assert "safe_mode::SafeModeComponent" in main_cpp + assert _has_define(SHUTDOWN_DEFINE) + + +def test_boot_is_good_on_shutdown_disabled( + generate_main: Callable[[str], str], +) -> None: + """With boot_is_good_on_shutdown: false, the define is not added.""" + main_cpp = generate_main( + "tests/component_tests/safe_mode/test_safe_mode_no_shutdown_confirm.yaml" + ) + + assert "safe_mode::SafeModeComponent" in main_cpp + assert not _has_define(SHUTDOWN_DEFINE) + + +def test_safe_mode_disabled(generate_main: Callable[[str], str]) -> None: + """With safe_mode disabled, no component and no define are generated.""" + main_cpp = generate_main( + "tests/component_tests/safe_mode/test_safe_mode_disabled.yaml" + ) + + assert "safe_mode::SafeModeComponent" not in main_cpp + assert not _has_define(SHUTDOWN_DEFINE) diff --git a/tests/component_tests/safe_mode/test_safe_mode_default.yaml b/tests/component_tests/safe_mode/test_safe_mode_default.yaml new file mode 100644 index 0000000000..07c38250aa --- /dev/null +++ b/tests/component_tests/safe_mode/test_safe_mode_default.yaml @@ -0,0 +1,8 @@ +--- +esphome: + name: test + +esp32: + board: nodemcu-32s + +safe_mode: diff --git a/tests/component_tests/safe_mode/test_safe_mode_disabled.yaml b/tests/component_tests/safe_mode/test_safe_mode_disabled.yaml new file mode 100644 index 0000000000..ac9b224191 --- /dev/null +++ b/tests/component_tests/safe_mode/test_safe_mode_disabled.yaml @@ -0,0 +1,9 @@ +--- +esphome: + name: test + +esp32: + board: nodemcu-32s + +safe_mode: + disabled: true diff --git a/tests/component_tests/safe_mode/test_safe_mode_no_shutdown_confirm.yaml b/tests/component_tests/safe_mode/test_safe_mode_no_shutdown_confirm.yaml new file mode 100644 index 0000000000..64df87b381 --- /dev/null +++ b/tests/component_tests/safe_mode/test_safe_mode_no_shutdown_confirm.yaml @@ -0,0 +1,9 @@ +--- +esphome: + name: test + +esp32: + board: nodemcu-32s + +safe_mode: + boot_is_good_on_shutdown: false diff --git a/tests/component_tests/web_server/test_private_network_access.py b/tests/component_tests/web_server/test_private_network_access.py new file mode 100644 index 0000000000..87911c5f9b --- /dev/null +++ b/tests/component_tests/web_server/test_private_network_access.py @@ -0,0 +1,86 @@ +"""Tests for web_server Private Network Access / allowed_origins validation.""" + +import pytest + +from esphome import config_validation as cv +from esphome.components.web_server import ( + CONF_ALLOWED_ORIGINS, + validate_origin, + validate_private_network_access, +) +from esphome.const import CONF_ENABLE_PRIVATE_NETWORK_ACCESS +from esphome.types import ConfigType + + +def test_pna_enabled_without_origins_fails() -> None: + """Enabling PNA without allowed_origins must fail validation.""" + config: ConfigType = {CONF_ENABLE_PRIVATE_NETWORK_ACCESS: True} + + with pytest.raises(cv.Invalid) as exc_info: + validate_private_network_access(config) + + error_msg = str(exc_info.value) + assert CONF_ALLOWED_ORIGINS in error_msg + assert "must be set" in error_msg + + +def test_pna_enabled_with_origins_passes() -> None: + """Enabling PNA with at least one allowed origin passes validation.""" + config: ConfigType = { + CONF_ENABLE_PRIVATE_NETWORK_ACCESS: True, + CONF_ALLOWED_ORIGINS: ["https://app.esphome.io"], + } + assert validate_private_network_access(config) == config + + +def test_origins_without_pna_passes() -> None: + """allowed_origins can be set without enabling PNA (they are independent).""" + config: ConfigType = { + CONF_ENABLE_PRIVATE_NETWORK_ACCESS: False, + CONF_ALLOWED_ORIGINS: ["https://app.esphome.io"], + } + assert validate_private_network_access(config) == config + + +def test_pna_disabled_without_origins_passes() -> None: + """PNA disabled and no origins specified passes validation.""" + config: ConfigType = {CONF_ENABLE_PRIVATE_NETWORK_ACCESS: False} + assert validate_private_network_access(config) == config + + +def test_validate_origin_wildcard() -> None: + """The '*' wildcard is accepted as-is.""" + assert validate_origin("*") == "*" + + +@pytest.mark.parametrize( + "value", + [ + "https://example.com", + "http://example.com:8080", + "https://192.168.1.5", + ], +) +def test_validate_origin_valid(value: str) -> None: + """Well-formed origins pass through unchanged.""" + assert validate_origin(value) == value + + +def test_validate_origin_lowercased() -> None: + """Scheme and host are normalized to lowercase to match the browser Origin header.""" + assert validate_origin("HTTPS://App.Example.com") == "https://app.example.com" + + +@pytest.mark.parametrize( + "value", + [ + "https://example.com/", # trailing slash + "https://example.com/path", # path segment + "example.com", # missing scheme + "", # empty + ], +) +def test_validate_origin_invalid(value: str) -> None: + """Malformed origins are rejected at config time instead of silently 403ing.""" + with pytest.raises(cv.Invalid, match="not a valid origin"): + validate_origin(value) diff --git a/tests/component_tests/web_server/test_web_server_auth.py b/tests/component_tests/web_server/test_web_server_auth.py new file mode 100644 index 0000000000..82635b26da --- /dev/null +++ b/tests/component_tests/web_server/test_web_server_auth.py @@ -0,0 +1,65 @@ +"""Tests for web_server authentication codegen.""" + +from collections.abc import Callable + +import pytest + +from esphome.core import CORE + +_DEFAULT_CHANGE_WARNING = "default will change to 'digest' in ESPHome 2027.1.0" + + +def _has_define(name: str) -> bool: + return any(d.name == name for d in CORE.defines) + + +def test_web_server_auth_default_is_basic_with_deprecation_warning( + generate_main: Callable[[str], str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Auth without an explicit type builds Basic and warns about the upcoming default change.""" + main_cpp = generate_main( + "tests/component_tests/web_server/web_server_auth_default.yaml" + ) + + assert '->set_auth_username("admin");' in main_cpp + assert '->set_auth_password("password");' in main_cpp + assert _has_define("USE_WEBSERVER_AUTH") + assert not _has_define("USE_WEBSERVER_AUTH_DIGEST") + assert _DEFAULT_CHANGE_WARNING in caplog.text + + +def test_web_server_auth_explicit_basic_no_warning( + generate_main: Callable[[str], str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Auth type basic builds Basic and does not warn.""" + generate_main("tests/component_tests/web_server/web_server_auth_basic.yaml") + + assert _has_define("USE_WEBSERVER_AUTH") + assert not _has_define("USE_WEBSERVER_AUTH_DIGEST") + assert _DEFAULT_CHANGE_WARNING not in caplog.text + + +def test_web_server_auth_explicit_digest( + generate_main: Callable[[str], str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Auth type digest builds Digest and does not warn.""" + generate_main("tests/component_tests/web_server/web_server_auth_digest.yaml") + + assert _has_define("USE_WEBSERVER_AUTH") + assert _has_define("USE_WEBSERVER_AUTH_DIGEST") + assert _DEFAULT_CHANGE_WARNING not in caplog.text + + +def test_web_server_without_auth( + generate_main: Callable[[str], str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Without an auth block, no auth is compiled in and no warning is emitted.""" + generate_main("tests/component_tests/web_server/web_server_no_auth.yaml") + + assert not _has_define("USE_WEBSERVER_AUTH") + assert not _has_define("USE_WEBSERVER_AUTH_DIGEST") + assert _DEFAULT_CHANGE_WARNING not in caplog.text diff --git a/tests/component_tests/web_server/web_server_auth_basic.yaml b/tests/component_tests/web_server/web_server_auth_basic.yaml new file mode 100644 index 0000000000..70180f9fbc --- /dev/null +++ b/tests/component_tests/web_server/web_server_auth_basic.yaml @@ -0,0 +1,18 @@ +--- +esphome: + name: test + +esp32: + board: nodemcu-32s + framework: + type: esp-idf + +wifi: + ssid: MySSID + password: password1 + +web_server: + auth: + username: admin + password: password + type: basic diff --git a/tests/component_tests/web_server/web_server_auth_default.yaml b/tests/component_tests/web_server/web_server_auth_default.yaml new file mode 100644 index 0000000000..076180ab79 --- /dev/null +++ b/tests/component_tests/web_server/web_server_auth_default.yaml @@ -0,0 +1,17 @@ +--- +esphome: + name: test + +esp32: + board: nodemcu-32s + framework: + type: esp-idf + +wifi: + ssid: MySSID + password: password1 + +web_server: + auth: + username: admin + password: password diff --git a/tests/component_tests/web_server/web_server_auth_digest.yaml b/tests/component_tests/web_server/web_server_auth_digest.yaml new file mode 100644 index 0000000000..f413787601 --- /dev/null +++ b/tests/component_tests/web_server/web_server_auth_digest.yaml @@ -0,0 +1,18 @@ +--- +esphome: + name: test + +esp32: + board: nodemcu-32s + framework: + type: esp-idf + +wifi: + ssid: MySSID + password: password1 + +web_server: + auth: + username: admin + password: password + type: digest diff --git a/tests/component_tests/web_server/web_server_no_auth.yaml b/tests/component_tests/web_server/web_server_no_auth.yaml new file mode 100644 index 0000000000..1c7823b4ae --- /dev/null +++ b/tests/component_tests/web_server/web_server_no_auth.yaml @@ -0,0 +1,14 @@ +--- +esphome: + name: test + +esp32: + board: nodemcu-32s + framework: + type: esp-idf + +wifi: + ssid: MySSID + password: password1 + +web_server: diff --git a/tests/component_tests/wireguard/__init__.py b/tests/component_tests/wireguard/__init__.py new file mode 100644 index 0000000000..82b57e8fef --- /dev/null +++ b/tests/component_tests/wireguard/__init__.py @@ -0,0 +1 @@ +"""Tests for the wireguard component.""" diff --git a/tests/component_tests/wireguard/test_init.py b/tests/component_tests/wireguard/test_init.py new file mode 100644 index 0000000000..556d14cd00 --- /dev/null +++ b/tests/component_tests/wireguard/test_init.py @@ -0,0 +1,44 @@ +"""Tests for the wireguard component schema.""" + +import pytest + +from esphome.components.wireguard import CONFIG_SCHEMA +from esphome.const import PlatformFramework +from esphome.yaml_util import SensitiveStr +from tests.component_tests.types import SetCoreConfigCallable + +# Any 42 base64 chars plus a valid terminator satisfies _WG_KEY_REGEX. +PRIVATE_KEY = "a" * 42 + "A=" +PEER_PUBLIC_KEY = "b" * 42 + "A=" +PEER_PRESHARED_KEY = "c" * 42 + "A=" + + +@pytest.mark.parametrize( + ("field", "value", "sensitive"), + [ + ("private_key", PRIVATE_KEY, True), + ("peer_preshared_key", PEER_PRESHARED_KEY, True), + ("peer_public_key", PEER_PUBLIC_KEY, False), + ], +) +def test_key_sensitivity( + field: str, + value: str, + sensitive: bool, + set_core_config: SetCoreConfigCallable, +) -> None: + """The private and preshared keys are secrets and must be tagged so dump + tooling redacts them deterministically; the peer's public key is not a + secret and must stay readable in redacted dumps (see issue #17718).""" + set_core_config(PlatformFramework.ESP32_IDF) + config = CONFIG_SCHEMA( + { + "address": "10.0.0.2", + "private_key": PRIVATE_KEY, + "peer_endpoint": "wg.example.com", + "peer_public_key": PEER_PUBLIC_KEY, + "peer_preshared_key": PEER_PRESHARED_KEY, + } + ) + assert isinstance(config[field], SensitiveStr) == sensitive + assert config[field] == value diff --git a/tests/components/README.md b/tests/components/README.md index 145a3440d2..be5e887767 100644 --- a/tests/components/README.md +++ b/tests/components/README.md @@ -28,6 +28,7 @@ create an `__init__.py` in your component's test directory and define `override_ ```python from tests.testing_helpers import ComponentManifestOverride + def override_manifest(manifest: ComponentManifestOverride) -> None: # Re-enable the component's own to_code (needed when the component must # emit C++ setup code that the test binary depends on at link time). @@ -39,6 +40,7 @@ Or supply a lightweight stub instead of the real `to_code`: ```python from tests.testing_helpers import ComponentManifestOverride + def override_manifest(manifest: ComponentManifestOverride) -> None: async def to_code_testing(config): # Only emit what the C++ tests actually need @@ -54,6 +56,7 @@ e.g. `tests/components/my_sensor/sensor/__init__.py`): ```python from tests.testing_helpers import ComponentManifestOverride + def override_manifest(manifest: ComponentManifestOverride) -> None: manifest.enable_codegen() ``` diff --git a/tests/components/adc/test.rp2040-pico2-ard.yaml b/tests/components/adc/test.rp2350-ard.yaml similarity index 100% rename from tests/components/adc/test.rp2040-pico2-ard.yaml rename to tests/components/adc/test.rp2350-ard.yaml diff --git a/tests/components/animation/common.yaml b/tests/components/animation/common.yaml index 8bb2a2f4d8..6790e8439b 100644 --- a/tests/components/animation/common.yaml +++ b/tests/components/animation/common.yaml @@ -1,23 +1,26 @@ -animation: - - id: rgb565_animation +image: + - platform: animation + id: rgb565_animation file: $component_dir/anim.gif type: RGB565 transparency: opaque resize: 50x50 - - id: rgb_animation + - platform: animation + id: rgb_animation file: $component_dir/anim.apng type: RGB transparency: chroma_key resize: 50x50 - - id: grayscale_animation + - platform: animation + id: grayscale_animation file: $component_dir/anim.apng type: grayscale display: lambda: |- id(rgb565_animation).next_frame(); - id(rgb_animation1).next_frame(); - id(grayscale_animation2).next_frame(); + id(rgb_animation).next_frame(); + id(grayscale_animation).next_frame(); it.image(0, 0, rgb565_animation); - it.image(120, 0, rgb_animation1); - it.image(240, 0, grayscale_animation2); + it.image(120, 0, rgb_animation); + it.image(240, 0, grayscale_animation); diff --git a/tests/components/animation/validate.host.yaml b/tests/components/animation/validate.host.yaml new file mode 100644 index 0000000000..d754f34688 --- /dev/null +++ b/tests/components/animation/validate.host.yaml @@ -0,0 +1,16 @@ +# Legacy top-level `animation:` form (deprecated; migrates to +# `platform: animation`). Config-only test exercising the deprecation path. +display: + - platform: sdl + id: animation_display + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +animation: + - id: legacy_animation + file: $component_dir/anim.gif + type: RGB565 + transparency: opaque + resize: 50x50 diff --git a/tests/components/api/test-dynamic-encryption.esp32-idf.yaml b/tests/components/api/test-dynamic-encryption.esp32-idf.yaml index 504871716b..7563e3e9df 100644 --- a/tests/components/api/test-dynamic-encryption.esp32-idf.yaml +++ b/tests/components/api/test-dynamic-encryption.esp32-idf.yaml @@ -1,5 +1,11 @@ -<<: !include common-base.yaml +packages: + common: !include common-base.yaml wifi: ssid: MySSID password: password1 + +# Encryption enabled without a key: compiles both frame helpers so the key +# can be provisioned at runtime (zero-PSK noise or deprecated plaintext) +api: + encryption: diff --git a/tests/components/aqi/benchmark.yaml b/tests/components/aqi/benchmark.yaml new file mode 100644 index 0000000000..d0d54c50b0 --- /dev/null +++ b/tests/components/aqi/benchmark.yaml @@ -0,0 +1,16 @@ +# Declares the component graph the C++ unit test build needs so that the aqi +# component's sources (which include sensor.h) compile. to_code is suppressed by +# the test harness; this only pulls the sensor + aqi source/include paths in. +# Loaded with plain yaml.safe_load, so avoid lambdas / ESPHome-tagged values here. +sensor: + - platform: template + id: pm25_sensor + name: "PM2.5" + - platform: template + id: pm10_sensor + name: "PM10" + - platform: aqi + name: "AQI" + pm_2_5: pm25_sensor + pm_10_0: pm10_sensor + calculation_type: AQI diff --git a/tests/components/aqi/common.yaml b/tests/components/aqi/common.yaml index 4c8cbbfa3f..cddc1f77cd 100644 --- a/tests/components/aqi/common.yaml +++ b/tests/components/aqi/common.yaml @@ -20,3 +20,10 @@ sensor: pm_2_5: pm25_sensor pm_10_0: pm10_sensor calculation_type: CAQI + + - platform: aqi + name: "Air Quality Index (AQI, extended)" + pm_2_5: pm25_sensor + pm_10_0: pm10_sensor + calculation_type: AQI + extended_range: true diff --git a/tests/components/aqi/test_aqi_calculator.cpp b/tests/components/aqi/test_aqi_calculator.cpp new file mode 100644 index 0000000000..ab95d3ac92 --- /dev/null +++ b/tests/components/aqi/test_aqi_calculator.cpp @@ -0,0 +1,85 @@ +#include + +#include "esphome/components/aqi/aqi_calculator.h" +#include "esphome/components/aqi/caqi_calculator.h" + +namespace esphome::aqi::testing { + +// US AQI (EPA 2024): PM2.5 225.5-500.4 -> 301-500, PM10 425-604 -> 301-500. + +TEST(USAQI, LowRangeUnaffectedByExtendedFlag) { + AQICalculator calc; + // PM2.5 25 drives over PM10 50; well below the top band, so the flag changes nothing. + EXPECT_EQ(calc.get_aqi(25.0f, 50.0f, false), 81); + EXPECT_EQ(calc.get_aqi(25.0f, 50.0f, true), 81); +} + +TEST(USAQI, HazardousInterpolatesNotPinnedAt301) { + AQICalculator calc; + // Regression guard: the old FLT_MAX top bucket collapsed every hazardous reading to 301. + EXPECT_EQ(calc.get_aqi(225.5f, 0.0f, false), 301); // band start + EXPECT_EQ(calc.get_aqi(250.0f, 0.0f, false), 319); // interpolated, not 301 + EXPECT_EQ(calc.get_aqi(500.4f, 0.0f, false), 500); // band top +} + +TEST(USAQI, DefaultClampsAtStandardMaximum) { + AQICalculator calc; + EXPECT_EQ(calc.get_aqi(600.0f, 0.0f, false), 500); + EXPECT_EQ(calc.get_aqi(1000.0f, 0.0f, false), 500); + EXPECT_EQ(calc.get_aqi(0.0f, 604.0f, false), 500); // PM10 top breakpoint +} + +TEST(USAQI, ExtendedRangeExtrapolatesBeyond500) { + AQICalculator calc; + EXPECT_EQ(calc.get_aqi(600.0f, 0.0f, true), 572); + EXPECT_EQ(calc.get_aqi(1000.0f, 0.0f, true), 862); + EXPECT_EQ(calc.get_aqi(0.0f, 700.0f, true), 607); // PM10 extrapolated past 500 +} + +TEST(USAQI, ExtendedRangeSaturatesUint16NoWraparound) { + AQICalculator calc; + // An absurd concentration would overflow uint16_t; it must saturate, not wrap to a small value. + EXPECT_EQ(calc.get_aqi(100000.0f, 0.0f, true), 65535); +} + +TEST(USAQI, WorseOfTwoPollutantsWins) { + AQICalculator calc; + // PM10 604 -> 500 dominates PM2.5 25 -> 81. + EXPECT_EQ(calc.get_aqi(25.0f, 604.0f, false), 500); +} + +// CAQI (CITEAIR): no maximum by spec -- the top ">100" class is open, so it is always unbounded +// and the extended_range flag does not apply. + +TEST(CAQI, LowRange) { + CAQICalculator calc; + EXPECT_EQ(calc.get_aqi(25.0f, 50.0f, false), 50); +} + +TEST(CAQI, ContinuousAt100NoPinAt101) { + CAQICalculator calc; + // Old code pinned everything above the top breakpoint to 101; now it reaches exactly 100. + EXPECT_EQ(calc.get_aqi(110.1f, 0.0f, false), 100); +} + +TEST(CAQI, UnboundedAboveTopBand) { + CAQICalculator calc; + EXPECT_EQ(calc.get_aqi(200.0f, 0.0f, false), 139); + EXPECT_EQ(calc.get_aqi(2000.0f, 0.0f, false), 925); +} + +TEST(CAQI, ExtendedRangeFlagIsIgnored) { + CAQICalculator calc; + // CAQI is always unbounded, so the flag must make no difference either way. + EXPECT_EQ(calc.get_aqi(200.0f, 0.0f, true), calc.get_aqi(200.0f, 0.0f, false)); + EXPECT_EQ(calc.get_aqi(2000.0f, 0.0f, true), calc.get_aqi(2000.0f, 0.0f, false)); +} + +TEST(CAQI, SaturatesUint16NoWraparound) { + CAQICalculator calc; + // CAQI is unbounded, so an extreme reading can extrapolate past uint16_t; it must saturate, + // not wrap around to a small (falsely "good") value. + EXPECT_EQ(calc.get_aqi(200000.0f, 0.0f, false), 65535); +} + +} // namespace esphome::aqi::testing diff --git a/tests/components/bk72xx_ble/common.yaml b/tests/components/bk72xx_ble/common.yaml new file mode 100644 index 0000000000..5ada71a141 --- /dev/null +++ b/tests/components/bk72xx_ble/common.yaml @@ -0,0 +1,2 @@ +bk72xx_ble: + enable_on_boot: true diff --git a/tests/components/bk72xx_ble/validate.bk72xx-ard.yaml b/tests/components/bk72xx_ble/validate.bk72xx-ard.yaml new file mode 100644 index 0000000000..e5009aa940 --- /dev/null +++ b/tests/components/bk72xx_ble/validate.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + bk72xx_ble: !include common.yaml diff --git a/tests/components/bk72xx_ble_tracker/common-boundary.yaml b/tests/components/bk72xx_ble_tracker/common-boundary.yaml new file mode 100644 index 0000000000..24844e18a5 --- /dev/null +++ b/tests/components/bk72xx_ble_tracker/common-boundary.yaml @@ -0,0 +1,11 @@ +bk72xx_ble_tracker: + id: ble_tracker + scan_parameters: + # Boundary coverage: the documented 2.5 ms floor on window (expressible only + # via the microsecond-accurate validation), a non-round interval exercising the + # 0.625 ms unit conversion without collapsing onto the window's unit count, + # and the non-continuous config path. + interval: 5000us + window: 2500us + duration: 5min + continuous: false diff --git a/tests/components/bk72xx_ble_tracker/common.yaml b/tests/components/bk72xx_ble_tracker/common.yaml new file mode 100644 index 0000000000..d8787a9347 --- /dev/null +++ b/tests/components/bk72xx_ble_tracker/common.yaml @@ -0,0 +1,7 @@ +bk72xx_ble_tracker: + id: ble_tracker + scan_parameters: + interval: 100ms + window: 30ms + duration: 5min + continuous: true diff --git a/tests/components/bk72xx_ble_tracker/validate-boundary.bk72xx-ard.yaml b/tests/components/bk72xx_ble_tracker/validate-boundary.bk72xx-ard.yaml new file mode 100644 index 0000000000..932eb4fb55 --- /dev/null +++ b/tests/components/bk72xx_ble_tracker/validate-boundary.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + bk72xx_ble_tracker: !include common-boundary.yaml diff --git a/tests/components/bk72xx_ble_tracker/validate.bk72xx-ard.yaml b/tests/components/bk72xx_ble_tracker/validate.bk72xx-ard.yaml new file mode 100644 index 0000000000..fc89479d06 --- /dev/null +++ b/tests/components/bk72xx_ble_tracker/validate.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + bk72xx_ble_tracker: !include common.yaml diff --git a/tests/components/ble_device_base/__init__.py b/tests/components/ble_device_base/__init__.py new file mode 100644 index 0000000000..1b041df8df --- /dev/null +++ b/tests/components/ble_device_base/__init__.py @@ -0,0 +1,12 @@ +import esphome.codegen as cg +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # resolve_irk() is compiled only when a sensor configures irk: + # (request_irk_support() emits USE_BLE_DEVICE_IRK). The unit-test build has + # no sensors, so emit the define here to put the real IRK path under test. + async def to_code_testing(config): + cg.add_define("USE_BLE_DEVICE_IRK") + + manifest.to_code = to_code_testing diff --git a/tests/components/ble_device_base/test_address.cpp b/tests/components/ble_device_base/test_address.cpp new file mode 100644 index 0000000000..9e4bca4c57 --- /dev/null +++ b/tests/components/ble_device_base/test_address.cpp @@ -0,0 +1,46 @@ +#include + +#include + +#include "esphome/components/ble_device_base/ble_device.h" + +namespace esphome::ble_device_base::testing { + +// from_scan_result() ingests BLE controller order (LSB-first); the public +// accessors must expose the historical esp32 semantics: address() in printable +// (MSB-first) order, address_uint64() with byte 0 in the LSB, address_str() +// printed MSB-first. +namespace { +// Device AA:BB:CC:DD:EE:FF — controller order delivers FF first. +const uint8_t MAC_LSB_FIRST[6] = {0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa}; +} // namespace + +TEST(BleDeviceAddress, AccessorsMatchEsp32Semantics) { + ESPBTDevice device; + device.from_scan_result(MAC_LSB_FIRST, -50, BLE_ADDR_TYPE_PUBLIC, nullptr, 0); + + const uint8_t *raw = device.address(); + EXPECT_EQ(raw[0], 0xaa); // MSB first, like ESP-IDF's bda + EXPECT_EQ(raw[5], 0xff); + + EXPECT_EQ(device.address_uint64(), 0xAABBCCDDEEFFULL); + + EXPECT_EQ(device.address_str(), "AA:BB:CC:DD:EE:FF"); +} + +// mac_lsb_first_to_uint64() packs the controller-order bytes a raw-advertisement +// callback delivers into the printable-order uint64 the native API speaks — the +// value esp32_ble::ble_addr_to_uint64() has always produced for that address. +TEST(BleDeviceAddress, MacLsbFirstToUint64MatchesWireValue) { + EXPECT_EQ(mac_lsb_first_to_uint64(MAC_LSB_FIRST), 0xAABBCCDDEEFFULL); +} + +// The helper and the parsed-device accessor are two routes to the same wire +// value: byte order must agree no matter which path an advertisement takes. +TEST(BleDeviceAddress, MacLsbFirstToUint64AgreesWithParsedDevice) { + ESPBTDevice device; + device.from_scan_result(MAC_LSB_FIRST, -50, BLE_ADDR_TYPE_PUBLIC, nullptr, 0); + EXPECT_EQ(mac_lsb_first_to_uint64(MAC_LSB_FIRST), device.address_uint64()); +} + +} // namespace esphome::ble_device_base::testing diff --git a/tests/components/ble_device_base/test_aes_ccm.cpp b/tests/components/ble_device_base/test_aes_ccm.cpp new file mode 100644 index 0000000000..39b2f81dcf --- /dev/null +++ b/tests/components/ble_device_base/test_aes_ccm.cpp @@ -0,0 +1,56 @@ +#include + +#include +#include + +#include "esphome/components/ble_device_base/ble_aes_ccm.h" + +namespace esphome::ble_device_base::testing { + +// Reference vector generated with Python `cryptography` AESCCM(tag_length=4), +// using the same AES-128-CCM parameters BTHome advertisements use: a 16-byte +// key, a 13-byte nonce, a 4-byte authentication tag and no associated data. +namespace { +const uint8_t KEY[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; +const uint8_t NONCE[13] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c}; +const uint8_t CIPHERTEXT[7] = {0x68, 0xb4, 0xf6, 0xc5, 0x2b, 0xf8, 0xaf}; +const uint8_t TAG[4] = {0x48, 0x4d, 0xaa, 0x56}; +const uint8_t PLAINTEXT[7] = {0x02, 0x01, 0x64, 0x03, 0x10, 0x8a, 0x01}; +} // namespace + +TEST(BleAesCcm, DecryptsAndAuthenticatesKnownVector) { + uint8_t out[sizeof(PLAINTEXT)] = {}; + EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG, + sizeof(TAG))); + EXPECT_EQ(0, memcmp(out, PLAINTEXT, sizeof(PLAINTEXT))); +} + +TEST(BleAesCcm, RejectsTamperedTag) { + uint8_t bad_tag[sizeof(TAG)]; + memcpy(bad_tag, TAG, sizeof(TAG)); + bad_tag[0] ^= 0x01; + uint8_t out[sizeof(PLAINTEXT)] = {}; + EXPECT_FALSE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, bad_tag, + sizeof(bad_tag))); +} + +TEST(BleAesCcm, RejectsTamperedCiphertext) { + uint8_t bad_ct[sizeof(CIPHERTEXT)]; + memcpy(bad_ct, CIPHERTEXT, sizeof(CIPHERTEXT)); + bad_ct[0] ^= 0x01; + uint8_t out[sizeof(PLAINTEXT)] = {}; + EXPECT_FALSE( + aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, bad_ct, sizeof(bad_ct), out, TAG, sizeof(TAG))); +} + +TEST(BleAesCcm, RejectsWrongKey) { + uint8_t bad_key[sizeof(KEY)]; + memcpy(bad_key, KEY, sizeof(KEY)); + bad_key[0] ^= 0xFF; + uint8_t out[sizeof(PLAINTEXT)] = {}; + EXPECT_FALSE(aes_ccm_auth_decrypt(bad_key, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG, + sizeof(TAG))); +} + +} // namespace esphome::ble_device_base::testing diff --git a/tests/components/ble_device_base/test_ble_uuid.cpp b/tests/components/ble_device_base/test_ble_uuid.cpp new file mode 100644 index 0000000000..45abd48479 --- /dev/null +++ b/tests/components/ble_device_base/test_ble_uuid.cpp @@ -0,0 +1,41 @@ +#include + +#include +#include + +#include "esphome/components/ble_device_base/ble_device.h" + +namespace esphome::ble_device_base::testing { + +// A 16- or 32-bit UUID must compare equal to its 128-bit Bluetooth Base UUID form, matching +// esp32_ble_tracker. The 128-bit raw is the base UUID (LSB-first) with the short value at +// bytes 12.. : here 0x1234 -> bytes [12]=0x34, [13]=0x12. +TEST(BleDeviceUuid, ShortFormMatchesEquivalentLongForm) { + const ESPBTUUID u16 = ESPBTUUID::from_uint16(0x1234); + const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, + 0x00, 0x10, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00}; + const ESPBTUUID u128 = ESPBTUUID::from_raw(raw128); + EXPECT_TRUE(u16 == u128); + EXPECT_TRUE(u128 == u16); // symmetric +} + +TEST(BleDeviceUuid, ThirtyTwoBitMatchesEquivalentLongForm) { + const ESPBTUUID u32 = ESPBTUUID::from_uint32(0x1122AAFF); + const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, + 0x00, 0x10, 0x00, 0x00, 0xFF, 0xAA, 0x22, 0x11}; + const ESPBTUUID u128 = ESPBTUUID::from_raw(raw128); + EXPECT_TRUE(u32 == u128); +} + +TEST(BleDeviceUuid, DifferentUuidsDoNotMatch) { + EXPECT_FALSE(ESPBTUUID::from_uint16(0x1234) == ESPBTUUID::from_uint16(0x1235)); + const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, + 0x00, 0x10, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00}; + // Same low bytes but a non-base prefix is a genuinely different 128-bit UUID. + uint8_t custom[16]; + memcpy(custom, raw128, 16); + custom[0] ^= 0x01; + EXPECT_FALSE(ESPBTUUID::from_uint16(0x1234) == ESPBTUUID::from_raw(custom)); +} + +} // namespace esphome::ble_device_base::testing diff --git a/tests/components/ble_device_base/test_irk.cpp b/tests/components/ble_device_base/test_irk.cpp new file mode 100644 index 0000000000..4f507968c6 --- /dev/null +++ b/tests/components/ble_device_base/test_irk.cpp @@ -0,0 +1,48 @@ +#include + +#include + +#include "esphome/components/ble_device_base/ble_device.h" + +namespace esphome::ble_device_base::testing { + +// Reference vector generated with Python `cryptography` AES-128-ECB following +// the RPA resolution procedure (Bluetooth Core, Vol 3 Part H §2.2.2): +// hash = e(IRK, prand), where prand is the top 3 address bytes and the hash +// must equal the low 3 address bytes. +namespace { +const uint8_t IRK[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; +// 4A:2B:7C:FB:7B:21 — prand 4A:2B:7C (two MSBs = 01, an RPA), hash FB:7B:21. +const uint8_t RPA_LSB_FIRST[6] = {0x21, 0x7b, 0xfb, 0x7c, 0x2b, 0x4a}; + +ESPBTDevice make_device(const uint8_t mac_lsb_first[6]) { + ESPBTDevice device; + device.from_scan_result(mac_lsb_first, /*rssi=*/-60, /*addr_type=*/BLE_ADDR_TYPE_RPA_RANDOM, nullptr, 0); + return device; +} +} // namespace + +TEST(BleIrk, ResolvesMatchingRpa) { + ESPBTDevice device = make_device(RPA_LSB_FIRST); + EXPECT_TRUE(device.resolve_irk(IRK)); +} + +TEST(BleIrk, RejectsWrongIrk) { + uint8_t wrong_irk[16]; + for (int i = 0; i < 16; i++) + wrong_irk[i] = IRK[i] ^ 0xff; + ESPBTDevice device = make_device(RPA_LSB_FIRST); + EXPECT_FALSE(device.resolve_irk(wrong_irk)); +} + +TEST(BleIrk, RejectsWrongAddress) { + uint8_t other_mac[6]; + for (int i = 0; i < 6; i++) + other_mac[i] = RPA_LSB_FIRST[i]; + other_mac[0] ^= 0x01; // corrupt one hash byte + ESPBTDevice device = make_device(other_mac); + EXPECT_FALSE(device.resolve_irk(IRK)); +} + +} // namespace esphome::ble_device_base::testing diff --git a/tests/components/ble_device_base/test_raw_callback.cpp b/tests/components/ble_device_base/test_raw_callback.cpp new file mode 100644 index 0000000000..cd18c3db59 --- /dev/null +++ b/tests/components/ble_device_base/test_raw_callback.cpp @@ -0,0 +1,101 @@ +#include + +#include + +#include "esphome/components/ble_device_base/ble_hub.h" + +namespace esphome::ble_device_base::testing { + +// Exercises the hub contract around RawAdvertisementCallback, not just the +// struct: a hub stores one slot via set_raw_advertisement_callback(), fires it +// only when set ("no subscriber" is the default-constructed slot), and a new +// registration replaces the old ("one consumer at a time"). +// +// The in-tree emit site (BK72xxBLETracker::on_scan_report) compiles against +// the Beken SDK and cannot run host-side, so the guard-and-fire semantics are +// pinned here through a minimal host BLEHub implementation instead. +namespace { + +class FakeHub : public BLEHub { + public: + void register_listener(ESPBTDeviceListener *listener) override {} + void set_raw_advertisement_callback(RawAdvertisementCallback callback) override { this->callback_ = callback; } + HubCapabilities get_capabilities() const override { return {false, false, false}; } + void get_adapter_mac(uint8_t out[6]) override {} + bool scan_running() override { return false; } + bool scan_active() override { return false; } + + /// The emit path every tracker implements: fire only when a subscriber is set. + void emit(const RawAdvertisement &adv) { + if (this->callback_.is_set()) + this->callback_.invoke(adv); + } + + protected: + RawAdvertisementCallback callback_; // default-constructed: no subscriber +}; + +struct CapturingSubscriber { + RawAdvertisement last{}; + int calls{0}; + + static void trampoline(void *self, const RawAdvertisement &adv) { + auto *sub = static_cast(self); + sub->last = adv; + sub->calls++; + } +}; + +// Device AA:BB:CC:DD:EE:FF — controller order delivers FF first. +const uint8_t MAC_LSB_FIRST[6] = {0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa}; +const uint8_t ADV_DATA[4] = {0x02, 0x01, 0x06, 0x00}; + +RawAdvertisement make_test_adv() { + return RawAdvertisement{ + .mac = MAC_LSB_FIRST, .data = ADV_DATA, .data_len = sizeof(ADV_DATA), .rssi = -63, .addr_type = 1}; +} + +} // namespace + +TEST(RawAdvertisementCallback, DefaultConstructedSlotIsNotSet) { + const RawAdvertisementCallback callback{}; + EXPECT_FALSE(callback.is_set()); +} + +TEST(RawAdvertisementCallback, SubscriberSeesFieldsUnchanged) { + FakeHub hub; + CapturingSubscriber subscriber; + hub.set_raw_advertisement_callback({&subscriber, CapturingSubscriber::trampoline}); + + hub.emit(make_test_adv()); + + ASSERT_EQ(subscriber.calls, 1); + EXPECT_EQ(subscriber.last.mac, MAC_LSB_FIRST); + EXPECT_EQ(subscriber.last.data, ADV_DATA); + EXPECT_EQ(subscriber.last.data_len, sizeof(ADV_DATA)); + EXPECT_EQ(subscriber.last.rssi, -63); + EXPECT_EQ(subscriber.last.addr_type, 1); +} + +TEST(RawAdvertisementCallback, NoSubscriberDoesNotFire) { + FakeHub hub; + // No set_raw_advertisement_callback(): emitting must be a guarded no-op, + // not a jump through a garbage pointer. + hub.emit(make_test_adv()); +} + +TEST(RawAdvertisementCallback, NewSubscriberReplacesOld) { + FakeHub hub; + CapturingSubscriber first; + CapturingSubscriber second; + hub.set_raw_advertisement_callback({&first, CapturingSubscriber::trampoline}); + hub.set_raw_advertisement_callback({&second, CapturingSubscriber::trampoline}); + + hub.emit(make_test_adv()); + + EXPECT_EQ(first.calls, 0); // one consumer at a time + ASSERT_EQ(second.calls, 1); + EXPECT_EQ(second.last.rssi, -63); +} + +} // namespace esphome::ble_device_base::testing diff --git a/tests/components/cc1101/common.yaml b/tests/components/cc1101/common.yaml index 9784bfce8b..4d2411e021 100644 --- a/tests/components/cc1101/common.yaml +++ b/tests/components/cc1101/common.yaml @@ -17,6 +17,15 @@ cc1101: sync0: 0x91 sync1: 0xD3 num_preamble: 2 + foc_bs_cs_gate: true + foc_pre_k: "2K" + foc_post_k: "K/2" + foc_limit: "BW/4" + bs_pre_ki: "3KI" + bs_pre_kp: "4KP" + bs_post_ki: "KI/2" + bs_post_kp: "KP" + bs_limit: "12.5%" on_packet: then: - lambda: |- diff --git a/tests/components/core/json_escape_test.cpp b/tests/components/core/json_escape_test.cpp new file mode 100644 index 0000000000..4db6dce2ea --- /dev/null +++ b/tests/components/core/json_escape_test.cpp @@ -0,0 +1,134 @@ +#include + +#include + +#include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" + +namespace esphome::testing { + +namespace { + +// Large enough that none of the inputs below are ever dropped. +constexpr size_t TEST_BUFFER_SIZE = 64 * JSON_ESCAPE_MAX_EXPANSION + 1; + +// Escape into a stack buffer and return the result as a string so the expectations stay readable. +std::string escape(const std::string &value) { + char buf[TEST_BUFFER_SIZE]; + return json_escape_into_buffer(buf, StringRef(value.c_str(), value.size())); +} + +// Same, but with the short control forms turned off. +std::string escape_long(const std::string &value) { + char buf[TEST_BUFFER_SIZE]; + return json_escape_into_buffer(buf, StringRef(value.c_str(), value.size()), false); +} + +} // namespace + +// Plain ASCII with no special characters is passed through unchanged. +TEST(JsonEscape, PlainStringUnchanged) { + EXPECT_EQ(escape("MyNetwork"), "MyNetwork"); + EXPECT_EQ(escape(""), ""); +} + +// A double quote is escaped so it does not terminate the surrounding JSON string. +TEST(JsonEscape, EscapesDoubleQuote) { + EXPECT_EQ(escape("a\"b"), "a\\\"b"); + // A double quote followed by other characters stays inside the JSON string. + EXPECT_EQ(escape("\">end"), "\\\">end"); +} + +// A backslash is doubled so it does not start an escape sequence in the output. +TEST(JsonEscape, EscapesBackslash) { + EXPECT_EQ(escape("a\\b"), "a\\\\b"); + // A trailing backslash must not escape the closing quote of the JSON string. + EXPECT_EQ(escape("net\\"), "net\\\\"); +} + +// The control characters with short JSON forms use those forms. +TEST(JsonEscape, EscapesShortFormControls) { + EXPECT_EQ(escape("\n"), "\\n"); + EXPECT_EQ(escape("\r"), "\\r"); + EXPECT_EQ(escape("\t"), "\\t"); + EXPECT_EQ(escape("\b"), "\\b"); + EXPECT_EQ(escape("\f"), "\\f"); +} + +// Other control characters (< 0x20) without a short form become \u00XX with lowercase hex. +TEST(JsonEscape, EscapesOtherControlsAsUnicode) { + EXPECT_EQ(escape(std::string("\x00", 1)), "\\u0000"); + EXPECT_EQ(escape("\x01"), "\\u0001"); + EXPECT_EQ(escape("\x10"), "\\u0010"); + EXPECT_EQ(escape("\x1f"), "\\u001f"); + // 0x7f (DEL) is >= 0x20, so it is NOT escaped by this helper. + EXPECT_EQ(escape("\x7f"), "\x7f"); +} + +// With the short forms turned off, every control character is written as \u00XX instead. +TEST(JsonEscape, LongControlEscapes) { + EXPECT_EQ(escape_long("\n"), "\\u000a"); + EXPECT_EQ(escape_long("\r"), "\\u000d"); + EXPECT_EQ(escape_long("\t"), "\\u0009"); + EXPECT_EQ(escape_long("\b"), "\\u0008"); + EXPECT_EQ(escape_long("\f"), "\\u000c"); + // Controls without a short form are unaffected by the flag. + EXPECT_EQ(escape_long("\x01"), "\\u0001"); +} + +// The flag only affects control characters. A quote or backslash is never written as \u00XX, because that form is +// no shorter and both modes have always emitted the two character escape. +TEST(JsonEscape, LongModeStillUsesTwoCharQuoteAndBackslash) { + EXPECT_EQ(escape_long("a\"b"), "a\\\"b"); + EXPECT_EQ(escape_long("a\\b"), "a\\\\b"); + // Ordinary text is untouched in either mode. + EXPECT_EQ(escape_long("MyDevice"), "MyDevice"); +} + +// Bytes >= 0x20, including multi-byte UTF-8 sequences, are passed through verbatim. +TEST(JsonEscape, PassesThroughUtf8) { + // "café" in UTF-8 (é == 0xC3 0xA9). + EXPECT_EQ(escape("caf\xc3\xa9"), "caf\xc3\xa9"); + // Emoji (📶, 4-byte UTF-8) survives unchanged. + EXPECT_EQ(escape("\xf0\x9f\x93\xb6"), "\xf0\x9f\x93\xb6"); +} + +// A mix of special and normal characters is escaped in place without disturbing the rest. +TEST(JsonEscape, MixedContent) { EXPECT_EQ(escape("a\"b\\c\nd"), "a\\\"b\\\\c\\nd"); } + +// A buffer sized at JSON_ESCAPE_MAX_EXPANSION bytes per input byte holds the worst case exactly. +TEST(JsonEscape, WorstCaseInputFitsExactly) { + constexpr size_t input_len = 8; + char buf[input_len * JSON_ESCAPE_MAX_EXPANSION + 1]; + const std::string input(input_len, '\x01'); + std::string expected; + for (size_t i = 0; i < input_len; i++) + expected += "\\u0001"; + EXPECT_EQ(json_escape_into_buffer(buf, StringRef(input.c_str(), input.size())), expected); +} + +// An escape sequence that would not fit is dropped whole rather than written partially, and the result stays null +// terminated. +TEST(JsonEscape, DropsEscapeThatWouldNotFit) { + // Room for one \u00XX sequence plus the null terminator, but two are requested. + char buf[JSON_ESCAPE_MAX_EXPANSION + 1]; + const std::string input(2, '\x01'); + const std::string result = json_escape_into_buffer(buf, StringRef(input.c_str(), input.size())); + EXPECT_EQ(result, "\\u0001"); + EXPECT_EQ(buf[JSON_ESCAPE_MAX_EXPANSION], '\0'); +} + +// Plain characters are truncated at the buffer size, leaving room for the null terminator. +TEST(JsonEscape, TruncatesPlainInput) { + char buf[5]; + const std::string input(20, 'a'); + EXPECT_STREQ(json_escape_into_buffer(buf, StringRef(input.c_str(), input.size())), "aaaa"); +} + +// A zero length buffer cannot even hold a null terminator, so an empty string is returned instead of writing. +TEST(JsonEscape, EmptyBufferIsSafe) { + const std::string input("test"); + EXPECT_STREQ(json_escape_into_buffer(std::span(), StringRef(input.c_str(), input.size())), ""); +} + +} // namespace esphome::testing diff --git a/tests/components/core/test_lock_free_queue.cpp b/tests/components/core/test_lock_free_queue.cpp new file mode 100644 index 0000000000..2f74278129 --- /dev/null +++ b/tests/components/core/test_lock_free_queue.cpp @@ -0,0 +1,96 @@ +// Exercises the no-atomics LockFreeQueue implementation (PlainAtomic indices — +// the path used on cores without atomic RMW instructions, currently BK72xx). +// The define is forced before the include so this TU deterministically compiles +// that path regardless of the host's default thread model; no other test TU +// instantiates this template, so the differing definition is confined here. +#define ESPHOME_THREAD_MULTI_NO_ATOMICS +#include "esphome/core/lock_free_queue.h" + +#include + +namespace esphome::core::testing { + +TEST(LockFreeQueueNoAtomics, EmptyPopReturnsNull) { + esphome::LockFreeQueue q; + EXPECT_EQ(q.pop(), nullptr); + EXPECT_TRUE(q.empty()); + EXPECT_FALSE(q.full()); + EXPECT_EQ(q.size(), 0u); +} + +TEST(LockFreeQueueNoAtomics, FifoOrder) { + esphome::LockFreeQueue q; + int a = 1, b = 2, c = 3; + EXPECT_TRUE(q.push(&a)); + EXPECT_TRUE(q.push(&b)); + EXPECT_TRUE(q.push(&c)); + EXPECT_EQ(q.size(), 3u); + EXPECT_EQ(q.pop(), &a); + EXPECT_EQ(q.pop(), &b); + EXPECT_EQ(q.pop(), &c); + EXPECT_EQ(q.pop(), nullptr); +} + +TEST(LockFreeQueueNoAtomics, CapacityIsSizeMinusOne) { + esphome::LockFreeQueue q; + int v[4] = {0, 1, 2, 3}; + EXPECT_TRUE(q.push(&v[0])); + EXPECT_TRUE(q.push(&v[1])); + EXPECT_TRUE(q.push(&v[2])); + EXPECT_TRUE(q.full()); + // Ring reserves one slot: the SIZEth push fails and is counted as dropped. + EXPECT_FALSE(q.push(&v[3])); + EXPECT_EQ(q.get_and_reset_dropped_count(), 1u); + EXPECT_EQ(q.get_and_reset_dropped_count(), 0u); // reset is sticky +} + +TEST(LockFreeQueueNoAtomics, NullPushRejected) { + esphome::LockFreeQueue q; + EXPECT_FALSE(q.push(nullptr)); + EXPECT_TRUE(q.empty()); +} + +TEST(LockFreeQueueNoAtomics, WrapAround) { + esphome::LockFreeQueue q; + int v[3] = {10, 20, 30}; + // Cycle several times the ring size to cross the wrap boundary repeatedly. + for (int cycle = 0; cycle < 10; cycle++) { + for (auto &value : v) + ASSERT_TRUE(q.push(&value)); + EXPECT_TRUE(q.full()); + for (auto &value : v) + ASSERT_EQ(q.pop(), &value); + EXPECT_TRUE(q.empty()); + } + EXPECT_EQ(q.get_and_reset_dropped_count(), 0u); +} + +TEST(LockFreeQueueNoAtomics, IncrementDroppedCount) { + esphome::LockFreeQueue q; + // Producer-side external drop accounting (pool exhausted before push). + q.increment_dropped_count(); + q.increment_dropped_count(); + EXPECT_EQ(q.get_and_reset_dropped_count(), 2u); +} + +TEST(LockFreeQueueNoAtomics, InterleavedPushPop) { + esphome::LockFreeQueue q; + int v[64]; + int popped = 0; + for (int i = 0; i < 64; i++) { + v[i] = i; + ASSERT_TRUE(q.push(&v[i])); + if (i % 2 == 1) { + int *first = q.pop(); + ASSERT_NE(first, nullptr); + EXPECT_EQ(*first, popped++); + int *second = q.pop(); + ASSERT_NE(second, nullptr); + EXPECT_EQ(*second, popped++); + } + } + EXPECT_TRUE(q.empty()); + EXPECT_EQ(popped, 64); +} + +} // namespace esphome::core::testing diff --git a/tests/components/cst328/common.yaml b/tests/components/cst328/common.yaml new file mode 100644 index 0000000000..286dbf587f --- /dev/null +++ b/tests/components/cst328/common.yaml @@ -0,0 +1,22 @@ +display: + - platform: ssd1306_i2c + i2c_id: i2c_bus + id: cst328_ssd1306_i2c_display + model: SSD1306_128X64 + reset_pin: ${display_reset_pin} + pages: + - id: cst328_page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: cst328 + i2c_id: i2c_bus + id: cst328_touchscreen + display: cst328_ssd1306_i2c_display + interrupt_pin: ${interrupt_pin} + reset_pin: ${reset_pin} + +binary_sensor: + - platform: cst328 + id: touch_key_cst328 diff --git a/tests/components/cst328/test.esp32-idf.yaml b/tests/components/cst328/test.esp32-idf.yaml new file mode 100644 index 0000000000..ac4ad140a8 --- /dev/null +++ b/tests/components/cst328/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + display_reset_pin: "4" + interrupt_pin: "20" + reset_pin: "21" + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + cst328: !include common.yaml diff --git a/tests/components/dallas_temp/common.yaml b/tests/components/dallas_temp/common.yaml index abd8e0cfa3..7f03ffa326 100644 --- a/tests/components/dallas_temp/common.yaml +++ b/tests/components/dallas_temp/common.yaml @@ -1,14 +1,18 @@ one_wire: - platform: gpio + id: ow_dallas_temp pin: ${one_wire_pin} sensor: - platform: dallas_temp + one_wire_id: ow_dallas_temp address: 0x1C0000031EDD2A28 name: Dallas Temperature 1 resolution: 9 - platform: dallas_temp + one_wire_id: ow_dallas_temp name: Dallas Temperature 2 - platform: dallas_temp + one_wire_id: ow_dallas_temp name: Dallas Temperature 3 index: 2 diff --git a/tests/components/deep_sleep/common-esp32-all.yaml b/tests/components/deep_sleep/common-esp32-all.yaml index b97eec76b9..9dc2f87258 100644 --- a/tests/components/deep_sleep/common-esp32-all.yaml +++ b/tests/components/deep_sleep/common-esp32-all.yaml @@ -6,9 +6,15 @@ deep_sleep: sleep_duration: 50s wakeup_pin: ${wakeup_pin} wakeup_pin_mode: INVERT_WAKEUP + on_wake: + - logger.log: + format: "Woke up, cause %d" + args: ["static_cast(cause)"] esp32_ext1_wakeup: pins: - - number: GPIO2 + - pin: GPIO2 + on_wake: + - logger.log: Woken by ext1 pin GPIO2 - number: GPIO13 mode: ANY_HIGH touch_wakeup: true diff --git a/tests/components/deep_sleep/common-esp32-ext1.yaml b/tests/components/deep_sleep/common-esp32-ext1.yaml index 9ed4279a33..c531d44743 100644 --- a/tests/components/deep_sleep/common-esp32-ext1.yaml +++ b/tests/components/deep_sleep/common-esp32-ext1.yaml @@ -5,8 +5,15 @@ deep_sleep: sleep_duration: 50s wakeup_pin: ${wakeup_pin} wakeup_pin_mode: INVERT_WAKEUP + on_wake: + - logger.log: + format: "Woke up, cause %d" + args: ["static_cast(cause)"] esp32_ext1_wakeup: pins: - - number: GPIO2 + - pin: + number: GPIO2 + on_wake: + - logger.log: Woken by ext1 pin GPIO2 - number: GPIO5 mode: ANY_HIGH diff --git a/tests/components/deep_sleep/common-esp32.yaml b/tests/components/deep_sleep/common-esp32.yaml index c20e1a902e..e670787cc0 100644 --- a/tests/components/deep_sleep/common-esp32.yaml +++ b/tests/components/deep_sleep/common-esp32.yaml @@ -5,3 +5,7 @@ deep_sleep: sleep_duration: 50s wakeup_pin: ${wakeup_pin} wakeup_pin_mode: INVERT_WAKEUP + on_wake: + - logger.log: + format: "Woke up, cause %d" + args: ["static_cast(cause)"] diff --git a/tests/components/deep_sleep/test-ota-rollback.esp32-idf.yaml b/tests/components/deep_sleep/test-ota-rollback.esp32-idf.yaml new file mode 100644 index 0000000000..bb24377675 --- /dev/null +++ b/tests/components/deep_sleep/test-ota-rollback.esp32-idf.yaml @@ -0,0 +1,19 @@ +# Deep sleep combined with OTA while bootloader rollback support is enabled +# (the default on ESP-IDF). Entering deep sleep runs the safe shutdown hooks, +# where safe_mode confirms the running app image so the bootloader does not +# roll back a fresh OTA update when the device goes to sleep before +# boot_is_good_after has elapsed. +substitutions: + wakeup_pin: GPIO4 + +packages: + deep_sleep: !include common.yaml + deep_sleep_esp32: !include common-esp32.yaml + +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: esphome + password: "superlongpasswordthatnoonewillknow" diff --git a/tests/components/deep_sleep/test-ota-rollback.nrf52-mcumgr.yaml b/tests/components/deep_sleep/test-ota-rollback.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..485490576d --- /dev/null +++ b/tests/components/deep_sleep/test-ota-rollback.nrf52-mcumgr.yaml @@ -0,0 +1,16 @@ +# Deep sleep combined with mcumgr OTA while MCUboot image rollback is enabled +# (the default on nRF52). Entering system-off deep sleep runs the safe +# shutdown hooks, where safe_mode confirms the running image so MCUboot does +# not revert a fresh OTA update on the next wake. +packages: + deep_sleep: !include common.yaml + +deep_sleep: + run_duration: 10s + +zephyr_ble_server: + +ota: + - platform: zephyr_mcumgr + transport: + ble: true diff --git a/tests/components/deep_sleep/test.bk72xx-ard.yaml b/tests/components/deep_sleep/test.bk72xx-ard.yaml index 2385fbb4db..bdbd27c902 100644 --- a/tests/components/deep_sleep/test.bk72xx-ard.yaml +++ b/tests/components/deep_sleep/test.bk72xx-ard.yaml @@ -1,6 +1,10 @@ deep_sleep: run_duration: 30s sleep_duration: 12h + on_wake: + - logger.log: + format: "Woke up, cause %d" + args: ["static_cast(cause)"] wakeup_pin: - pin: number: P6 diff --git a/tests/components/deep_sleep/test.esp8266-ard.yaml b/tests/components/deep_sleep/test.esp8266-ard.yaml index df08ec8a14..e4c592c095 100644 --- a/tests/components/deep_sleep/test.esp8266-ard.yaml +++ b/tests/components/deep_sleep/test.esp8266-ard.yaml @@ -1,5 +1,9 @@ deep_sleep: run_duration: 10s sleep_duration: 50s + on_wake: + - logger.log: + format: "Woke up, cause %d" + args: ["static_cast(cause)"] <<: !include common.yaml diff --git a/tests/components/ds2484/common.yaml b/tests/components/ds2484/common.yaml index 1e5dcd7dba..b6abc5bd76 100644 --- a/tests/components/ds2484/common.yaml +++ b/tests/components/ds2484/common.yaml @@ -1,6 +1,7 @@ one_wire: - platform: ds2484 - i2c_id: i2c_bus - address: 0x18 - active_pullup: true - strong_pullup: false + - platform: ds2484 + id: ow_ds2484 + i2c_id: i2c_bus + address: 0x18 + active_pullup: true + strong_pullup: false diff --git a/tests/components/ds248x/common.yaml b/tests/components/ds248x/common.yaml new file mode 100644 index 0000000000..53ae56f20a --- /dev/null +++ b/tests/components/ds248x/common.yaml @@ -0,0 +1,115 @@ +# Combined DS248x test covering all chip variants and options: +# - DS2482-100: active pullup, multiple sensors + index access +# - DS2482-101: sleep pin, bus_sleep / hub_sleep +# - DS2482-800: all 8 channels +# - DS2484: adjustable 1-Wire timing + RWPU pullup resistor selection +ds248x: + - id: ds2482_100 + address: 0x18 + type: ds2482-100 + active_pullup: true + - id: ds2482_101 + address: 0x19 + type: ds2482-101 + active_pullup: true + sleep_pin: + number: GPIO12 + inverted: false + bus_sleep: true + hub_sleep: true + - id: ds2482_800 + address: 0x1a + type: ds2482-800 + active_pullup: true + - id: ds2484_hub + address: 0x1b + type: ds2484 + active_pullup: true + # DS2484-specific 1-Wire timing parameters (optional fine-tuning) + reset_low_time: 8 # tRSTL: Reset low time + master_sample_time: 8 # tMSP: Master sample point + write_0_low_time: 8 # tW0L: Write-0 low time + recovery_time: 8 # tREC0: Recovery time + active_pullup_resistance: 1000ohm # RWPU: weak pullup resistor selection + +one_wire: + - platform: ds248x + ds248x_id: ds2482_100 + channel: 0 + id: ow_100 + - platform: ds248x + ds248x_id: ds2482_101 + channel: 0 + id: ow_101 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 0 + id: ow_800_0 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 1 + id: ow_800_1 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 2 + id: ow_800_2 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 3 + id: ow_800_3 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 4 + id: ow_800_4 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 5 + id: ow_800_5 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 6 + id: ow_800_6 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 7 + id: ow_800_7 + - platform: ds248x + ds248x_id: ds2484_hub + channel: 0 + id: ow_2484 + +sensor: + # DS2482-100: explicit address + index-based access on the same bus + - platform: dallas_temp + one_wire_id: ow_100 + address: 0x1c0000031edd2a28 + name: Temp 100 by address + resolution: 12 + - platform: dallas_temp + one_wire_id: ow_100 + index: 0 + name: Temp 100 by index + # DS2482-101 (sleep variant) + - platform: dallas_temp + one_wire_id: ow_101 + address: 0x578295491f64ff28 + name: Temp 101 + # DS2482-800: sensors on a few of the eight channels + - platform: dallas_temp + one_wire_id: ow_800_0 + address: 0x1c0000031edd2a28 + name: Temp 800 CH0 + - platform: dallas_temp + one_wire_id: ow_800_3 + index: 0 + name: Temp 800 CH3 by index + - platform: dallas_temp + one_wire_id: ow_800_7 + address: 0x2800000123456789 + name: Temp 800 CH7 + # DS2484 (adjustable timing) + - platform: dallas_temp + one_wire_id: ow_2484 + address: 0x1c0000031edd2a28 + name: Temp 2484 + resolution: 12 diff --git a/tests/components/ds248x/test.esp32-ard.yaml b/tests/components/ds248x/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c503b0ccb --- /dev/null +++ b/tests/components/ds248x/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/ds248x/test.esp32-idf.yaml b/tests/components/ds248x/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/ds248x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/ds248x/test.esp8266-ard.yaml b/tests/components/ds248x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/ds248x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/ds248x/test.rp2040-ard.yaml b/tests/components/ds248x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/ds248x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index bb771f2132..9cca528744 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -76,6 +76,14 @@ display: - platform: epaper_spi model: seeed-reterminal-e1002 + - platform: epaper_spi + model: seeed-reterminal-e1004 + cs_pin: 33 + cs1_pin: 34 + dc_pin: 35 + reset_pin: 36 + busy_pin: 37 + enable_pin: 39 - platform: epaper_spi model: seeed-ee04-mono-4.26 full_update_every: 10 @@ -204,6 +212,29 @@ display: it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK); it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0)); + # Soldered Inkplate 6COLOR 7-color e-paper (600x448, UC8159-family) + - platform: epaper_spi + spi_id: spi_bus + model: inkplate6color + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + inverted: true + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color(255, 0, 0)); + it.circle(it.get_width() / 2, it.get_height() / 2, 10, Color(255, 165, 0)); + # Waveshare 7.5" V2 BWR (800x480, UC8179 controller, EDP_7in5b_V2) - platform: epaper_spi spi_id: spi_bus diff --git a/tests/components/epaper_spi/validate-e1004.esp32-s3-idf.yaml b/tests/components/epaper_spi/validate-e1004.esp32-s3-idf.yaml new file mode 100644 index 0000000000..27066710e0 --- /dev/null +++ b/tests/components/epaper_spi/validate-e1004.esp32-s3-idf.yaml @@ -0,0 +1,38 @@ +esphome: + name: e1004-test + friendly_name: E1004 Test + +esp32: + board: esp32-s3-devkitc-1 + variant: esp32s3 + framework: + type: esp-idf + +psram: + mode: octal + +spi: + - id: epaper_spi_bus + clk_pin: GPIO7 + mosi_pin: GPIO9 + +display: + - platform: epaper_spi + spi_id: epaper_spi_bus + model: seeed-reterminal-e1004 + update_interval: never + lambda: |- + it.fill(Color::WHITE); + it.rectangle(10, 10, it.get_width() - 20, it.get_height() - 20, Color::BLACK); + it.print(it.get_width() / 2, it.get_height() / 2, id(my_font), Color::BLACK, TextAlign::CENTER, "E1004 Test"); + it.circle(100, 100, 30, Color(255, 0, 0)); + it.circle(200, 100, 30, Color(0, 255, 0)); + it.circle(300, 100, 30, Color(0, 0, 255)); + it.circle(400, 100, 30, Color(255, 255, 0)); + +font: + - file: "gfonts://Roboto" + id: my_font + size: 20 + +logger: diff --git a/tests/components/esp32/test-nvs_encryption.esp32-s3-idf.yaml b/tests/components/esp32/test-nvs_encryption.esp32-s3-idf.yaml new file mode 100644 index 0000000000..ab9001efec --- /dev/null +++ b/tests/components/esp32/test-nvs_encryption.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + nvs_encryption: + key_id: 0 + +<<: !include common.yaml diff --git a/tests/components/esp32/test-ota_downgrade_protection.esp32-s3-idf.yaml b/tests/components/esp32/test-ota_downgrade_protection.esp32-s3-idf.yaml new file mode 100644 index 0000000000..5d6ab455ac --- /dev/null +++ b/tests/components/esp32/test-ota_downgrade_protection.esp32-s3-idf.yaml @@ -0,0 +1,22 @@ +esphome: + project: + name: esphome.downgrade_test + version: "1.2.3" + +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + enable_ota_downgrade_protection: true + signed_ota_verification: + signing_key: ../../components/esp32/dummy_signing_key.pem + signing_scheme: rsa3072 + +# wifi + ota so the IDF OTA backend compiles with USE_OTA_DOWNGRADE_PROTECTION. +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: esphome diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index fd42fac5a3..c16869d06b 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -21,7 +21,7 @@ esp32: disable_fatfs: true ota: - platform: esphome + - platform: esphome wifi: ssid: MySSID diff --git a/tests/components/esp32/validate-signed_ota_external.esp32-s3-idf.yaml b/tests/components/esp32/validate-signed_ota_external.esp32-s3-idf.yaml new file mode 100644 index 0000000000..5b57993e87 --- /dev/null +++ b/tests/components/esp32/validate-signed_ota_external.esp32-s3-idf.yaml @@ -0,0 +1,11 @@ +# Secure Boot V2 schemes carry the public key inside each image's signature +# block, so verifying externally-signed binaries needs no key in the config: +# a bare block enables verification with the default rsa3072 scheme. +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + signed_ota_verification: + +<<: !include common.yaml diff --git a/tests/components/espnow/common.yaml b/tests/components/espnow/common.yaml index f05735e8f4..2f82e794c4 100644 --- a/tests/components/espnow/common.yaml +++ b/tests/components/espnow/common.yaml @@ -2,6 +2,7 @@ espnow: id: espnow_component auto_add_peer: false channel: 1 + max_payload_size: 1470 peers: - 11:22:33:44:55:66 on_receive: @@ -73,6 +74,7 @@ sensor: id: espnow_temp_sensor - platform: packet_transport + transport_id: transport1 provider: test-provider remote_id: espnow_temp_sensor id: remote_temp diff --git a/tests/components/file/common.yaml b/tests/components/file/common.yaml new file mode 100644 index 0000000000..e95c6b01f6 --- /dev/null +++ b/tests/components/file/common.yaml @@ -0,0 +1,17 @@ +image: + - platform: file + id: file_binary_image + file: ../../pnglogo.png + type: BINARY + dither: FloydSteinberg + - platform: file + id: file_rgb565_image + file: ../../pnglogo.png + type: RGB565 + transparency: alpha_channel + resize: 50x50 + - platform: file + id: file_mdi_image + file: mdi:alert-circle-outline + type: BINARY + resize: 24x24 diff --git a/tests/components/file/test.esp32-idf.yaml b/tests/components/file/test.esp32-idf.yaml new file mode 100644 index 0000000000..29822d7b4f --- /dev/null +++ b/tests/components/file/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + +display: + - platform: ili9xxx + id: file_main_lcd + spi_id: spi_bus + model: ili9342 + cs_pin: 15 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + +<<: !include common.yaml diff --git a/tests/components/file/test.host.yaml b/tests/components/file/test.host.yaml new file mode 100644 index 0000000000..76f9e5af85 --- /dev/null +++ b/tests/components/file/test.host.yaml @@ -0,0 +1,9 @@ +display: + - platform: sdl + id: file_display + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +<<: !include common.yaml diff --git a/tests/components/gsl3670/test.esp32-s3-idf.yaml b/tests/components/gsl3670/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..384e12eaba --- /dev/null +++ b/tests/components/gsl3670/test.esp32-s3-idf.yaml @@ -0,0 +1,22 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + +xl9535: + id: expander + +touchscreen: + # Firmware downloaded from the model's default release URL and cached. + - platform: gsl3670 + model: seeed-reterminal-d1001 + display: test_display_screen + interrupt_pin: 18 + # Explicit firmware URL + SHA-256 override. + - platform: gsl3670 + model: seeed-reterminal-d1001 + display: test_display_screen + reset_pin: 10 + interrupt_pin: 11 + firmware: + url: https://github.com/esphome-libs/gsl3670-firmware/releases/download/v1.0.0/seeed-d1001-fw.bin + sha256: 2e50501ad83656fb6fa3d92591f9f31add4d442c8e8a79f29f5c4d335bd127a4 diff --git a/tests/components/gt911/common.yaml b/tests/components/gt911/common.yaml index 0fc40737f0..24a67e2e45 100644 --- a/tests/components/gt911/common.yaml +++ b/tests/components/gt911/common.yaml @@ -1,19 +1,8 @@ -display: - - platform: ssd1306_i2c - i2c_id: i2c_bus - id: gt911_ssd1306_i2c_display - model: SSD1306_128X64 - reset_pin: ${display_reset_pin} - pages: - - id: gt911_page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - touchscreen: - platform: gt911 i2c_id: i2c_bus id: gt911_touchscreen - display: gt911_ssd1306_i2c_display + display: test_display_screen interrupt_pin: ${interrupt_pin} reset_pin: ${reset_pin} diff --git a/tests/components/gt911/test.esp32-idf.yaml b/tests/components/gt911/test.esp32-idf.yaml index 3bce86d9a3..9c2de1a425 100644 --- a/tests/components/gt911/test.esp32-idf.yaml +++ b/tests/components/gt911/test.esp32-idf.yaml @@ -1,9 +1,8 @@ substitutions: - display_reset_pin: "10" interrupt_pin: "20" reset_pin: "21" packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml - -<<: !include common.yaml + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + gt911: !include common.yaml diff --git a/tests/components/gt911/test.esp8266-ard.yaml b/tests/components/gt911/test.esp8266-ard.yaml index c3bc159b5b..59af399be8 100644 --- a/tests/components/gt911/test.esp8266-ard.yaml +++ b/tests/components/gt911/test.esp8266-ard.yaml @@ -1,9 +1,8 @@ substitutions: - display_reset_pin: "10" interrupt_pin: "12" reset_pin: "13" packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml - -<<: !include common.yaml + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + gt911: !include common.yaml diff --git a/tests/components/gt911/test.rp2040-ard.yaml b/tests/components/gt911/test.rp2040-ard.yaml index 0c7f0bc504..efd5d9c2b1 100644 --- a/tests/components/gt911/test.rp2040-ard.yaml +++ b/tests/components/gt911/test.rp2040-ard.yaml @@ -1,9 +1,8 @@ substitutions: - display_reset_pin: "10" interrupt_pin: "20" reset_pin: "21" packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml - -<<: !include common.yaml + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + gt911: !include common.yaml diff --git a/tests/components/http_request/http_request.yaml b/tests/components/http_request/http_request.yaml index 46d4b88ec5..4b3c2ca36b 100644 --- a/tests/components/http_request/http_request.yaml +++ b/tests/components/http_request/http_request.yaml @@ -59,6 +59,24 @@ esphome: id: test_regression_light brightness: 100% effect: "None" + - http_request.get: + url: https://esphome.io + capture_response: true + on_response: + then: + # Regression test: http_request.post with json: (dict variant) inside + # on_response of a capture_response: true request puts std::string& + # (body) into the nested action's Ts..., which exposes a + # const-correctness bug in HttpRequestSendAction::play() where + # encode_json_ receives const copies of non-const reference args. + - http_request.post: + url: https://esphome.io + json: + status: "ok" + # Same with json: lambda variant, exercises json_func_ path + - http_request.post: + url: https://esphome.io + json: !lambda "root[\"status\"] = \"ok\";" http_request: useragent: esphome/tagreader diff --git a/tests/components/image/common.yaml b/tests/components/image/common.yaml index 9819068970..5a8f938319 100644 --- a/tests/components/image/common.yaml +++ b/tests/components/image/common.yaml @@ -1,85 +1,104 @@ image: - - id: binary_image + - platform: file + id: binary_image file: ../../pnglogo.png type: BINARY dither: FloydSteinberg - - id: transparent_transparent_image + - platform: file + id: transparent_transparent_image file: ../../pnglogo.png type: BINARY transparency: chroma_key - - id: rgba_image + - platform: file + id: rgba_image file: ../../pnglogo.png type: RGB transparency: alpha_channel resize: 50x50 - - id: rgb24_image + - platform: file + id: rgb24_image file: ../../pnglogo.png type: RGB transparency: chroma_key - - id: rgb_image + - platform: file + id: rgb_image file: ../../pnglogo.png type: RGB transparency: opaque - - id: rgb565_image + - platform: file + id: rgb565_image file: ../../pnglogo.png type: RGB565 transparency: opaque - - id: rgb565_ck_image + - platform: file + id: rgb565_ck_image file: ../../pnglogo.png type: RGB565 transparency: chroma_key - - id: rgb565_alpha_image + - platform: file + id: rgb565_alpha_image file: ../../pnglogo.png type: RGB565 transparency: alpha_channel - - id: grayscale_alpha_image + - platform: file + id: grayscale_alpha_image file: ../../pnglogo.png type: grayscale transparency: alpha_channel resize: 50x50 - - id: grayscale_ck_image + - platform: file + id: grayscale_ck_image file: ../../pnglogo.png type: grayscale transparency: chroma_key - - id: grayscale_image + - platform: file + id: grayscale_image file: ../../pnglogo.png type: grayscale transparency: opaque - - id: web_svg_image + - platform: file + id: web_svg_image file: https://media.esphome.io/logo/logo.svg resize: 256x48 type: BINARY transparency: chroma_key - - id: web_tiff_image + - platform: file + id: web_tiff_image file: https://media.esphome.io/tests/images/SIPI_Jelly_Beans_4.1.07.tiff type: RGB resize: 48x48 - - id: web_redirect_image + - platform: file + id: web_redirect_image file: https://media.esphome.io/logo/logo.png type: RGB resize: 48x48 - - id: mdi_alert + - platform: file + id: mdi_alert type: BINARY file: mdi:alert-circle-outline resize: 50x50 - - id: another_alert_icon + - platform: file + id: another_alert_icon file: mdi:alert-outline type: BINARY - - file: mdil:arrange-bring-to-front + - platform: file + file: mdil:arrange-bring-to-front id: mdil_id resize: 50x50 type: binary transparency: chroma_key - - file: mdi:beer + - platform: file + file: mdi:beer id: mdi_id resize: 50x50 type: binary transparency: chroma_key - - file: memory:alert-octagon + - platform: file + file: memory:alert-octagon id: memory_id resize: 50x50 type: binary diff --git a/tests/components/image/test.esp8266-ard.yaml b/tests/components/image/test.esp8266-ard.yaml index 492b57c449..939a3ac39b 100644 --- a/tests/components/image/test.esp8266-ard.yaml +++ b/tests/components/image/test.esp8266-ard.yaml @@ -12,12 +12,11 @@ display: invert_colors: true image: - defaults: + - platform: file + id: test_image + file: ../../pnglogo.png type: rgb565 transparency: opaque byte_order: little_endian resize: 50x50 dither: FloydSteinberg - images: - - id: test_image - file: ../../pnglogo.png diff --git a/tests/components/image/test.host.yaml b/tests/components/image/test.host.yaml index aa45497088..455d41d0c2 100644 --- a/tests/components/image/test.host.yaml +++ b/tests/components/image/test.host.yaml @@ -7,43 +7,60 @@ display: height: 480 image: - binary: - - id: binary_image - file: ../../pnglogo.png - dither: FloydSteinberg - - id: transparent_transparent_image - file: ../../pnglogo.png - transparency: chroma_key - rgb: - alpha_channel: - - id: rgba_image - file: ../../pnglogo.png - resize: 50x50 - chroma_key: - - id: rgb24_image - file: ../../pnglogo.png - type: RGB - opaque: - - id: rgb_image - file: ../../pnglogo.png - rgb565: - - id: rgb565_image - file: ../../pnglogo.png - transparency: opaque - - id: rgb565_ck_image - file: ../../pnglogo.png - transparency: chroma_key - - id: rgb565_alpha_image - file: ../../pnglogo.png - transparency: alpha_channel - grayscale: - - id: grayscale_alpha_image - file: ../../pnglogo.png - transparency: alpha_channel - resize: 50x50 - - id: grayscale_ck_image - file: ../../pnglogo.png - transparency: chroma_key - - id: grayscale_image - file: ../../pnglogo.png - transparency: opaque + - platform: file + id: binary_image + file: ../../pnglogo.png + type: binary + dither: FloydSteinberg + - platform: file + id: transparent_transparent_image + file: ../../pnglogo.png + type: binary + transparency: chroma_key + - platform: file + id: rgba_image + file: ../../pnglogo.png + type: rgb + transparency: alpha_channel + resize: 50x50 + - platform: file + id: rgb24_image + file: ../../pnglogo.png + type: RGB + transparency: chroma_key + - platform: file + id: rgb_image + file: ../../pnglogo.png + type: rgb + transparency: opaque + - platform: file + id: rgb565_image + file: ../../pnglogo.png + type: rgb565 + transparency: opaque + - platform: file + id: rgb565_ck_image + file: ../../pnglogo.png + type: rgb565 + transparency: chroma_key + - platform: file + id: rgb565_alpha_image + file: ../../pnglogo.png + type: rgb565 + transparency: alpha_channel + - platform: file + id: grayscale_alpha_image + file: ../../pnglogo.png + type: grayscale + transparency: alpha_channel + resize: 50x50 + - platform: file + id: grayscale_ck_image + file: ../../pnglogo.png + type: grayscale + transparency: chroma_key + - platform: file + id: grayscale_image + file: ../../pnglogo.png + type: grayscale + transparency: opaque diff --git a/tests/components/image/validate-defaults.host.yaml b/tests/components/image/validate-defaults.host.yaml new file mode 100644 index 0000000000..16ea9e7b62 --- /dev/null +++ b/tests/components/image/validate-defaults.host.yaml @@ -0,0 +1,25 @@ +# Legacy top-level `image:` defaults/images form (deprecated; migrates to +# `platform: file`). Config-only test exercising the deprecation/migration path, +# including the per-type byte_order drop when an entry overrides to a non-endian +# type (binary). +display: + - platform: sdl + id: image_display + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +image: + defaults: + type: rgb565 + transparency: opaque + byte_order: little_endian + resize: 50x50 + dither: FloydSteinberg + images: + - id: legacy_defaults_image + file: ../../pnglogo.png + - id: legacy_defaults_binary + file: ../../pnglogo.png + type: binary diff --git a/tests/components/image/validate-grouped-single.host.yaml b/tests/components/image/validate-grouped-single.host.yaml new file mode 100644 index 0000000000..0b6ff3d576 --- /dev/null +++ b/tests/components/image/validate-grouped-single.host.yaml @@ -0,0 +1,24 @@ +# Legacy top-level `image:` structured form using single-dict (non-list) values +# for `images:`, a type group, and a transparency group -- the old `ensure_list` +# accepted a bare dict in each of these places. Deprecated; migrates to +# `platform: file`. Config-only test exercising the deprecation/migration path. +display: + - platform: sdl + id: image_display + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +image: + images: + id: legacy_images_single_dict + file: ../../pnglogo.png + type: rgb565 + rgb565: + id: legacy_grouped_type_single_dict + file: ../../pnglogo.png + rgb: + alpha_channel: + id: legacy_grouped_transparency_single_dict + file: ../../pnglogo.png diff --git a/tests/components/image/validate-grouped.host.yaml b/tests/components/image/validate-grouped.host.yaml new file mode 100644 index 0000000000..8f85aa7ca5 --- /dev/null +++ b/tests/components/image/validate-grouped.host.yaml @@ -0,0 +1,25 @@ +# Legacy top-level `image:` type-grouped form (deprecated; migrates to +# `platform: file`). Config-only test exercising the deprecation/migration path. +display: + - platform: sdl + id: image_display + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +image: + binary: + - id: legacy_grouped_binary + file: ../../pnglogo.png + rgb: + alpha_channel: + - id: legacy_grouped_rgba + file: ../../pnglogo.png + opaque: + - id: legacy_grouped_rgb + file: ../../pnglogo.png + rgb565: + - id: legacy_grouped_rgb565 + file: ../../pnglogo.png + transparency: chroma_key diff --git a/tests/components/image/validate-single.host.yaml b/tests/components/image/validate-single.host.yaml new file mode 100644 index 0000000000..52a945fb67 --- /dev/null +++ b/tests/components/image/validate-single.host.yaml @@ -0,0 +1,16 @@ +# Legacy top-level `image:` single-dict form (a bare image dict instead of a +# list; deprecated, migrates to `platform: file`). Config-only test exercising +# the deprecation/migration path. +display: + - platform: sdl + id: image_display + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +image: + id: legacy_single_image + file: ../../pnglogo.png + type: RGB565 + transparency: opaque diff --git a/tests/components/image/validate.host.yaml b/tests/components/image/validate.host.yaml new file mode 100644 index 0000000000..aa821ea7e2 --- /dev/null +++ b/tests/components/image/validate.host.yaml @@ -0,0 +1,18 @@ +# Legacy top-level `image:` list form (deprecated; migrates to `platform: file`). +# Config-only test exercising the deprecation/migration path. +display: + - platform: sdl + id: image_display + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +image: + - id: legacy_list_binary + file: ../../pnglogo.png + type: BINARY + - id: legacy_list_rgb565 + file: ../../pnglogo.png + type: RGB565 + transparency: alpha_channel diff --git a/tests/components/light/test_light_call_brightness.cpp b/tests/components/light/test_light_call_brightness.cpp new file mode 100644 index 0000000000..3c5dffd2f1 --- /dev/null +++ b/tests/components/light/test_light_call_brightness.cpp @@ -0,0 +1,120 @@ +#include + +#include "esphome/components/light/light_call.h" +#include "esphome/components/light/light_output.h" +#include "esphome/components/light/light_state.h" + +namespace esphome::light::testing { + +namespace { + +// A light that only supports ON_OFF, like the `binary` platform and `status_led`. +class OnOffOutput : public LightOutput { + public: + LightTraits get_traits() override { + LightTraits traits; + traits.set_supported_color_modes({ColorMode::ON_OFF}); + return traits; + } + void write_state(LightState *state) override {} +}; + +// A dimmable light, like the `monochromatic` platform. +class BrightnessOutput : public LightOutput { + public: + LightTraits get_traits() override { + LightTraits traits; + traits.set_supported_color_modes({ColorMode::BRIGHTNESS}); + return traits; + } + void write_state(LightState *state) override {} +}; + +// validate_() is where zero brightness is resolved against the light's capabilities. +class TestableLightCall : public LightCall { + public: + using LightCall::LightCall; + using LightCall::validate_; +}; + +bool as_binary(const LightColorValues &values) { + bool binary; + values.as_binary(&binary); + return binary; +} + +} // namespace + +// An ON/OFF light has no "on but dark" state, so a zero brightness -- how effects encode +// their dark phase -- must turn the light off. Regression test for +// https://github.com/esphome/esphome/issues/17873. +TEST(LightCallOnOff, ZeroBrightnessTurnsOutputOff) { + OnOffOutput output; + LightState state(&output); + TestableLightCall call(&state); + + call.set_state(true).set_brightness(0.0f); + auto values = call.validate_(); + + EXPECT_FALSE(as_binary(values)); +} + +// The zero must not be stored, or no later turn-on could clear it: the capability check in +// validate_() drops any brightness an ON/OFF light doesn't support, so a stored zero would +// leave the light permanently off. +TEST(LightCallOnOff, ZeroBrightnessIsNotStored) { + OnOffOutput output; + LightState state(&output); + + TestableLightCall dark_call(&state); + dark_call.set_state(true).set_brightness(0.0f); + state.remote_values = dark_call.validate_(); + + EXPECT_FLOAT_EQ(state.remote_values.get_brightness(), 1.0f); + + // A plain turn-on afterwards must switch the light back on. + TestableLightCall on_call(&state); + on_call.set_state(true); + auto values = on_call.validate_(); + + EXPECT_TRUE(as_binary(values)); +} + +// A plain turn-on with no brightness must still light up. +TEST(LightCallOnOff, PlainTurnOnIsVisible) { + OnOffOutput output; + LightState state(&output); + TestableLightCall call(&state); + + call.set_state(true); + auto values = call.validate_(); + + EXPECT_TRUE(as_binary(values)); +} + +TEST(LightCallOnOff, TurnOffTurnsOutputOff) { + OnOffOutput output; + LightState state(&output); + TestableLightCall call(&state); + + call.set_state(false); + auto values = call.validate_(); + + EXPECT_FALSE(as_binary(values)); +} + +// A dimmable light can represent "on but dark", so zero brightness must be kept as-is and +// must not be rewritten into a turn-off. +TEST(LightCallBrightness, ZeroBrightnessStaysOnButDark) { + BrightnessOutput output; + LightState state(&output); + TestableLightCall call(&state); + + call.set_state(true).set_brightness(0.0f); + auto values = call.validate_(); + + EXPECT_TRUE(values.is_on()); + EXPECT_FLOAT_EQ(values.get_brightness(), 0.0f); +} + +} // namespace esphome::light::testing diff --git a/tests/components/ln882h_ble/common.yaml b/tests/components/ln882h_ble/common.yaml new file mode 100644 index 0000000000..c466c2a4de --- /dev/null +++ b/tests/components/ln882h_ble/common.yaml @@ -0,0 +1,2 @@ +ln882h_ble: + enable_on_boot: true diff --git a/tests/components/ln882h_ble/test.ln882x-ard.yaml b/tests/components/ln882h_ble/test.ln882x-ard.yaml new file mode 100644 index 0000000000..08cb35b3d8 --- /dev/null +++ b/tests/components/ln882h_ble/test.ln882x-ard.yaml @@ -0,0 +1,2 @@ +packages: + ln882h_ble: !include common.yaml diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index f500002f40..b4d5fe0387 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -91,8 +91,8 @@ binary_sensor: animation: move_right time: 600ms - platform: lvgl - id: button_checker - name: LVGL button + id: common_button_checker + name: Common button widget: spin_up on_state: then: diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 7af058e6b8..ef8ba13e42 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -24,20 +24,6 @@ binary_sensor: name: Button A checked widget: button_a state: checked - - platform: lvgl - id: button_checker - name: LVGL button - widget: button_button - state: checked - on_state: - then: - - lvgl.checkbox.update: - id: checkbox_id - state: - checked: !lambda |- - auto y = x; // block inlining of one line return - return y; - - platform: lvgl id: button_presser name: Button pressed @@ -49,6 +35,9 @@ lvgl: rotation: 90 log_level: debug resume_on_input: true + paused: true + update_when_display_idle: true + refresh_interval: 30ms on_pause: - logger.log: LVGL is Paused - lvgl.display.set_rotation: 90 @@ -57,6 +46,40 @@ lvgl: - lvgl.display.set_rotation: rotation: 0 lvgl_id: lvgl_id + - lvgl.display.set_rotation: + rotation: !lambda "return 180;" + lvgl_id: lvgl_id + on_landscape: + - logger.log: LVGL display is now landscape + # Re-layout a container in response to orientation changes. The layout type + # and grid structure are fixed at creation; only the style options change. + - lvgl.widget.update: + id: grid_rows_only_shorthand + layout: + grid_column_align: center + grid_row_align: space_between + pad_row: 4px + - lvgl.update: + top_layer: + layout: + flex_flow: row + on_portrait: + - logger.log: LVGL display is now portrait + - lvgl.widget.update: + id: grid_rows_only_shorthand + layout: + grid_column_align: start + pad_row: 2px + # Top-level layout applies to the active screen + - lvgl.update: + layout: + flex_flow: column + pad_row: 8px + - lvgl.update: + top_layer: + layout: + flex_flow: column + flex_align_main: center on_boot: - logger.log: LVGL has started @@ -64,6 +87,12 @@ lvgl: id: meter_arc_indicator start_value: 0 end_value: 180 + - lvgl.animation.start: + id: + - anim_slide + - anim_color + duration: 3s + loop: true on_invalidate_area: logger.log: Invalidate area on_resolution_change: @@ -108,6 +137,52 @@ lvgl: - obj: bg_color: 0x000000 bg_opa: cover + top_layer: + widgets: + - obj: + id: anim_box + x: 0 + y: 0 + width: 50 + height: 50 + bg_color: 0xFF0000 + - label: + id: anim_label + text: anim + animations: + - id: anim_slide + duration: 1s + start_delay: 100ms + auto_start: true + loop: true + timing: ease_in_out + on_start: + - logger.log: anim started + on_stop: + - logger.log: anim stopped + widgets: + - id: anim_box + x: + from: 0 + to: 100 + y: + from: 0 + to: !lambda "return 80;" + opa: + from: 50% + to: 100% + - id: anim_color + duration: 2s + timing: + - round_trip + - type: gravity + bounce: 0.3 + acceleration: 0.8 + widgets: + - id: anim_label + text_color: + from: 0xFF0000 + to: color_id theme: dark_mode: true obj: @@ -210,13 +285,37 @@ lvgl: on_click: then: - lvgl.display.set_rotation: 0 + - lvgl.animation.stop: anim_slide + - lvgl.animation.stop: + id: + - anim_slide + - anim_color - lvgl.widget.hide: message_box - lvgl.style.update: id: style_test bg_color: blue bg_opa: !lambda return 0.5; + # `obj` is already themed above -- exercises updating an existing hidden style. + - lvgl.theme.update: + obj: + border_width: 2 + # `label` is never mentioned under `theme:` -- exercises lazily creating the + # hidden style and getting it attached to already-built label widgets. + - lvgl.theme.update: + label: + text_color: red + # `button` is never mentioned under `theme:`, and only a non-default state is + # targeted here -- exercises that no spurious, empty main/default style is + # created (and attached to every button) alongside the requested one. + - lvgl.theme.update: + button: + pressed: + bg_color: red - lvgl.image.update: id: lv_image + src: + mapping: image_map + value: !lambda return round(1.0); scale: !lambda return 512; rotation: !lambda return 100; pivot_x: !lambda return 20; @@ -342,9 +441,16 @@ lvgl: text_font: montserrat_40 border_post: true on_press: - lvgl.label.update: - id: hello_label - text: Goodbye + - lvgl.label.update: + id: hello_label + text: + mapping: lvgl_string_map + value: !lambda return 2; + - lvgl.label.update: + id: hello_label + text: + mapping: lvgl_string_map + value: 2 on_click: then: - lvgl.animimg.stop: anim_img @@ -1450,6 +1556,21 @@ image: invert_alpha: true transparency: alpha_channel +mapping: + - id: image_map + from: int + to: image + entries: + 0: cat_image + 1: dog_image + - id: lvgl_string_map + from: int + to: string + entries: + 0: "First" + 1: "Second" + 2: "Third" + color: - id: light_blue hex: "3340FF" diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index 79ea06f16a..d938017fd9 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -1,7 +1,8 @@ packages: - lvgl: !include lvgl-package.yaml + lvgl_package: !include lvgl-package.yaml spi: !include ../../test_build_components/common/spi/esp32-idf.yaml i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + lvgl: !include common.yaml sensor: - platform: rotary_encoder @@ -77,5 +78,3 @@ lvgl: - component.update: tft_display - delay: 60s - lvgl.resume: - -<<: !include common.yaml diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 6328648fe3..90cbb3c0a5 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -22,6 +22,36 @@ lvgl: displays: sdl0 rotation: 180 top_layer: + widgets: + - obj: + id: anim_box + x: 0 + y: 0 + width: 40 + height: 40 + bg_color: 0xFF0000 + animations: + - id: anim_slide + duration: 1s + start_delay: 100ms + auto_start: true + loop: true + timing: + - round_trip + - type: ease_in_out + weight: 3 + on_start: + - logger.log: anim started + on_stop: + - logger.log: anim stopped + widgets: + - id: anim_box + x: + from: 0 + to: !lambda "return 100;" + opa: + from: 50% + to: 100% - id: lvgl_1 displays: sdl1 @@ -42,7 +72,14 @@ lvgl: - label: text: Click ME on_click: - logger.log: Clicked + then: + - logger.log: Clicked + - lvgl.animation.stop: + id: anim_slide + lvgl_id: lvgl_0 + - lvgl.animation.start: + id: anim_slide + lvgl_id: lvgl_0 font: - file: "gfonts://Roboto" diff --git a/tests/components/md5/__init__.py b/tests/components/md5/__init__.py new file mode 100644 index 0000000000..cf4ad47363 --- /dev/null +++ b/tests/components/md5/__init__.py @@ -0,0 +1,9 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # md5's to_code calls cg.add_define("USE_MD5"), which gates md5.h. C++ unit + # test builds that pull md5 in transitively (e.g. ota's host backend, which + # has an md5::MD5Digest member) need that define, otherwise md5.h compiles to + # nothing and the dependent headers fail to find md5::MD5Digest. + manifest.enable_codegen() diff --git a/tests/components/mdns/test-fallback.esp32-idf.yaml b/tests/components/mdns/test-fallback.esp32-idf.yaml new file mode 100644 index 0000000000..b51dbb443f --- /dev/null +++ b/tests/components/mdns/test-fallback.esp32-idf.yaml @@ -0,0 +1,7 @@ +# No api, web_server or extra services so the fallback _http service +# (with version, mac and config_hash TXT records) is compiled. +wifi: + ssid: MySSID + password: password1 + +mdns: diff --git a/tests/components/mdns/test-webserver-no-api.esp32-idf.yaml b/tests/components/mdns/test-webserver-no-api.esp32-idf.yaml new file mode 100644 index 0000000000..23f3abdeb2 --- /dev/null +++ b/tests/components/mdns/test-webserver-no-api.esp32-idf.yaml @@ -0,0 +1,9 @@ +# web_server without the native api so the version, mac and config_hash +# TXT records are attached to the web_server _http service. +wifi: + ssid: MySSID + password: password1 + +web_server: + +mdns: diff --git a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml index b56ebee21e..12b45ee160 100644 --- a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml +++ b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml @@ -1,5 +1,5 @@ packages: - - !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml + i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml psram: mode: octal diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index 798f7283f6..45f7b65289 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -37,6 +37,9 @@ class MockUARTComponent : public uart::UARTComponent { MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(uart::UARTFlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); +#if defined(USE_ESP8266) || defined(USE_ESP32) + void load_settings(bool dump_config) override {} +#endif // defined(USE_ESP8266) || defined(USE_ESP32) }; class TestableMitsubishiCN105 : public MitsubishiCN105 { diff --git a/tests/components/modbus/common.h b/tests/components/modbus/common.h new file mode 100644 index 0000000000..659b72014c --- /dev/null +++ b/tests/components/modbus/common.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include "esphome/components/uart/uart_component.h" + +namespace esphome::modbus::testing { + +// A UART that discards all writes, for tests that never inspect the wire. +class NullUART : public uart::UARTComponent { + public: + NullUART() { this->set_baud_rate(115200); } + void write_array(const uint8_t *data, size_t len) override {} + bool peek_byte(uint8_t *data) override { return false; } + bool read_array(uint8_t *data, size_t len) override { return false; } + size_t available() override { return 0; } + uart::UARTFlushResult flush() override { return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } +#if defined(USE_ESP8266) || defined(USE_ESP32) + void load_settings(bool dump_config) override {} +#endif + void check_logger_conflict() override {} +}; + +} // namespace esphome::modbus::testing diff --git a/tests/components/modbus/heap_probe_test.cpp b/tests/components/modbus/heap_probe_test.cpp new file mode 100644 index 0000000000..ddf905a8df --- /dev/null +++ b/tests/components/modbus/heap_probe_test.cpp @@ -0,0 +1,223 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "esphome/components/modbus/modbus.h" + +// The allocation counters rely on AddressSanitizer's malloc hooks. The cpp_unit_test harness always +// builds with ASan, so this is exercised in CI; the fallback only applies to out-of-harness builds. +#ifndef __has_feature +#define __has_feature(x) 0 +#endif +#if defined(__SANITIZE_ADDRESS__) || __has_feature(address_sanitizer) +#define HEAP_PROBE_HAS_ASAN +#endif + +#ifdef HEAP_PROBE_HAS_ASAN + +// Allocation counters fed by ASan's malloc hooks; sampled tightly around the calls under test. +static std::atomic g_alloc_count{0}; +static std::atomic g_alloc_bytes{0}; + +static void malloc_hook(const volatile void *, size_t size) { + g_alloc_count++; + g_alloc_bytes += size; +} +static void free_hook(const volatile void *) {} + +extern "C" int __sanitizer_install_malloc_and_free_hooks(void (*malloc_hook)(const volatile void *, size_t), + void (*free_hook)(const volatile void *)); + +[[maybe_unused]] static const int g_hooks_installed = __sanitizer_install_malloc_and_free_hooks(malloc_hook, free_hook); + +namespace esphome::modbus::testing { + +namespace { + +// A UART the test can inject received bytes into; sent bytes are discarded. +class InjectableUART : public uart::UARTComponent { + public: + void write_array(const uint8_t *data, size_t len) override {} + bool peek_byte(uint8_t *data) override { + if (this->rx_.empty()) + return false; + *data = this->rx_.front(); + return true; + } + bool read_array(uint8_t *data, size_t len) override { + if (len > this->rx_.size()) + return false; + memcpy(data, this->rx_.data(), len); + this->rx_.erase(this->rx_.begin(), this->rx_.begin() + len); + return true; + } + size_t available() override { return this->rx_.size(); } + uart::UARTFlushResult flush() override { return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } + void check_logger_conflict() override {} + + void inject_frame(uint8_t address, std::span pdu) { + // Wire frame: address + PDU + CRC16(low, high) + size_t start = this->rx_.size(); + this->rx_.push_back(address); + this->rx_.insert(this->rx_.end(), pdu.begin(), pdu.end()); + uint16_t crc = crc16(this->rx_.data() + start, this->rx_.size() - start); + this->rx_.push_back(crc & 0xFF); + this->rx_.push_back(crc >> 8); + } + + private: + std::vector rx_; +}; + +class NullDevice : public ModbusClientDevice { + public: + using ModbusClientDevice::ModbusClientDevice; + void on_response(std::span request_pdu, std::span response_pdu) override { + this->responses++; + } + int responses{0}; +}; + +struct Sample { + size_t count; + size_t bytes; +}; + +template Sample sample(F &&f) { + size_t c0 = g_alloc_count.load(), b0 = g_alloc_bytes.load(); + f(); + return {g_alloc_count.load() - c0, g_alloc_bytes.load() - b0}; +} + +} // namespace + +// Typical frames (reads and single-register/coil writes are exactly address + 5-byte PDU + CRC = 8 +// bytes) fit the SmallInlineBuffer and are built with zero heap allocations; only larger frames spill +// to a single allocation. +TEST(HeapProbe, TypicalFrameConstructionIsAllocationFree) { + const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02}; // 5 bytes -> 8-byte frame, inline + Sample typical = sample([&] { + ModbusFrame frame(0x02, read_pdu, sizeof(read_pdu)); + (void) frame; + }); + printf("HEAPPROBE frame_typical count=%zu bytes=%zu\n", typical.count, typical.bytes); + EXPECT_EQ(typical.count, 0u); + + uint8_t large_pdu[250] = {0x10}; // multi-register write -> 253-byte frame, spills once + Sample large = sample([&] { + ModbusFrame frame(0x02, large_pdu, sizeof(large_pdu)); + (void) frame; + }); + printf("HEAPPROBE frame_large count=%zu bytes=%zu\n", large.count, large.bytes); + EXPECT_EQ(large.count, 1u); +} + +// Queueing typical commands is allocation-free within the deque's first block: the frame fits the +// inline buffer, every entry is a plain append (ordering lives in selection, not storage), and the +// first block is already allocated when the hub is constructed. A 512-byte deque block holds +// 512 / sizeof(ModbusDeviceCommand) entries (16 on the 64-bit host); a deeper queue allocates more. +TEST(HeapProbe, QueueingTypicalCommandsIsAllocationFree) { + ModbusClientHub hub; + ModbusClientDevice device(&hub, 0x02); + + StaticVector req; + const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + req.assign(read_pdu, read_pdu + sizeof(read_pdu)); + + constexpr int n = 12; + static_assert(n * sizeof(ModbusDeviceCommand) < 512, "keep n within one deque block so the probe stays meaningful"); + size_t total = 0; + for (int i = 0; i != n; i++) { + req[2] = static_cast(i); // distinct start addresses: identical frames would dedup, not enqueue + total += sample([&] { device.send_pdu(req); }).count; + } + printf("HEAPPROBE queue_%d_typical_commands total_allocs=%zu\n", n, total); + EXPECT_EQ(total, 0u); +} + +// A WRITE arriving behind queued reads is a plain append too - the old priority front-insert (and +// its possible front-block allocation) is gone; the write wins transmit SELECTION instead. +TEST(HeapProbe, WriteBehindQueuedReadsAppendsAllocationFree) { + ModbusClientHub hub; + ModbusClientDevice device(&hub, 0x02); + + StaticVector req; + const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + req.assign(read_pdu, read_pdu + sizeof(read_pdu)); + for (int i = 0; i != 3; i++) { + req[2] = static_cast(i); // distinct start addresses: identical frames would dedup, not enqueue + device.send_pdu(req); + } + + const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF}; + Sample append = sample([&] { device.send_pdu(write_pdu); }); + printf("HEAPPROBE write_append count=%zu bytes=%zu\n", append.count, append.bytes); + EXPECT_EQ(append.count, 0u); +} + +// End to end: bytes injected at the UART travel through receive, frame parsing, response matching and +// device dispatch. The first response may grow the hub's rx buffer once; after that warm-up, handling a +// response performs zero heap allocations all the way to the device callback. +TEST(HeapProbe, ResponseHandlingIsAllocationFreeAfterWarmup) { + InjectableUART uart; + uart.set_baud_rate(115200); // tx timing math divides by the baud rate + ModbusClientHub hub; + hub.set_uart_parent(&uart); + hub.setup(); // computes frame timing from the baud rate + NullDevice device(&hub, 0x02); + + StaticVector req; + const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + req.assign(read_pdu, read_pdu + sizeof(read_pdu)); + + // Largest possible read response first, so the rx buffer warm-up covers every later size. + uint8_t large_resp[252] = {0x03, 250}; + const uint8_t small_resp[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + + auto round_trip = [&](std::span response_pdu) { + device.send_pdu(req); + hub.loop(); // transmit; the tx queue is empty during the measured receive below + uart.inject_frame(0x02, response_pdu); + return sample([&] { hub.loop(); }); // receive + parse + match + dispatch + }; + + Sample warmup = round_trip(std::span(large_resp, sizeof(large_resp))); + Sample steady_large = round_trip(std::span(large_resp, sizeof(large_resp))); + Sample steady_small = round_trip(small_resp); + + printf("HEAPPROBE warmup count=%zu bytes=%zu\n", warmup.count, warmup.bytes); + printf("HEAPPROBE steady_large count=%zu bytes=%zu\n", steady_large.count, steady_large.bytes); + printf("HEAPPROBE steady_small count=%zu bytes=%zu\n", steady_small.count, steady_small.bytes); + + EXPECT_EQ(device.responses, 3); + EXPECT_LE(warmup.count, 1u); // at most the one-time rx buffer growth + EXPECT_EQ(steady_large.count, 0u); + EXPECT_EQ(steady_small.count, 0u); +} + +} // namespace esphome::modbus::testing + +#else // !HEAP_PROBE_HAS_ASAN + +// Stub every ASan-gated test name, so the suite's test list is identical in every build configuration. +namespace esphome::modbus::testing { +TEST(HeapProbe, TypicalFrameConstructionIsAllocationFree) { + GTEST_SKIP() << "allocation counting requires an AddressSanitizer build"; +} +TEST(HeapProbe, QueueingTypicalCommandsIsAllocationFree) { + GTEST_SKIP() << "allocation counting requires an AddressSanitizer build"; +} +TEST(HeapProbe, WriteBehindQueuedReadsAppendsAllocationFree) { + GTEST_SKIP() << "allocation counting requires an AddressSanitizer build"; +} +TEST(HeapProbe, ResponseHandlingIsAllocationFreeAfterWarmup) { + GTEST_SKIP() << "allocation counting requires an AddressSanitizer build"; +} +} // namespace esphome::modbus::testing + +#endif // HEAP_PROBE_HAS_ASAN diff --git a/tests/components/modbus/modbus_client_device_test.cpp b/tests/components/modbus/modbus_client_device_test.cpp new file mode 100644 index 0000000000..8638c37688 --- /dev/null +++ b/tests/components/modbus/modbus_client_device_test.cpp @@ -0,0 +1,344 @@ +#include + +#include +#include +#include +#include + +#include "esphome/components/modbus/modbus.h" + +namespace esphome::modbus::testing { + +namespace { + +// Records every typed callback so tests can assert on the dispatch performed by the default +// on_response()/on_error() implementations. +class RecordingDevice : public ModbusClientDevice { + public: + struct ReadRegistersCall { + uint16_t start_address; + std::vector registers; + ResponseStatus status; + }; + struct ReadBitsCall { + uint16_t start_address; + uint16_t count; + std::vector packed; + ResponseStatus status; + }; + struct WriteCall { + uint16_t address; + uint16_t value; + ResponseStatus status; + }; + + void on_read_holding_registers(uint16_t start_address, std::span registers, + ResponseStatus status) override { + this->holding_calls.push_back({start_address, {registers.begin(), registers.end()}, status}); + } + void on_read_input_registers(uint16_t start_address, std::span registers, + ResponseStatus status) override { + this->input_calls.push_back({start_address, {registers.begin(), registers.end()}, status}); + } + void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) override { + this->coil_calls.push_back({start_address, bits.size(), {bits.bytes().begin(), bits.bytes().end()}, status}); + } + void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status) override { + this->discrete_calls.push_back({start_address, bits.size(), {bits.bytes().begin(), bits.bytes().end()}, status}); + } + void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status) override { + this->write_single_register_calls.push_back({address, value, status}); + } + void on_write_single_coil(uint16_t address, bool value, ResponseStatus status) override { + this->write_single_coil_calls.push_back({address, static_cast(value), status}); + } + void on_write_multiple_registers(uint16_t start_address, std::span registers, + ResponseStatus status) override { + this->write_multiple_registers_calls.push_back({start_address, {registers.begin(), registers.end()}, status}); + } + void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) override { + this->write_multiple_coils_calls.push_back( + {start_address, bits.size(), {bits.bytes().begin(), bits.bytes().end()}, status}); + } + void on_custom_response(std::span request_pdu, std::span response_pdu, + ResponseStatus status) override { + this->custom_requests.emplace_back(request_pdu.begin(), request_pdu.end()); + this->custom_responses.emplace_back(response_pdu.begin(), response_pdu.end()); + this->custom_statuses.push_back(status); + } + + std::vector holding_calls; + std::vector input_calls; + std::vector coil_calls; + std::vector discrete_calls; + std::vector write_single_register_calls; + std::vector write_single_coil_calls; + std::vector write_multiple_registers_calls; + std::vector write_multiple_coils_calls; + std::vector> custom_requests; + std::vector> custom_responses; + std::vector custom_statuses; +}; + +// Overrides only the generic callbacks to verify the typed defaults delegate to them. +class GenericDevice : public ModbusClientDevice { + public: + void on_read_registers(EntityType register_type, uint16_t start_address, std::span registers, + ResponseStatus status) override { + this->register_type = register_type; + this->start_address = start_address; + this->registers.assign(registers.begin(), registers.end()); + this->calls++; + } + void on_read_bits(EntityType register_type, uint16_t start_address, PackedBits bits, ResponseStatus status) override { + this->register_type = register_type; + this->start_address = start_address; + this->bit_count = bits.size(); + this->calls++; + } + EntityType register_type{EntityType::CUSTOM}; + uint16_t start_address{0}; + uint16_t bit_count{0}; + std::vector registers; + int calls{0}; +}; + +} // namespace + +TEST(ModbusClientDeviceFanOut, ReadHoldingRegistersSuccess) { + RecordingDevice device; + const uint8_t request[] = {0x03, 0x01, 0x00, 0x00, 0x02}; // read 2 regs at 0x100 + const uint8_t response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; // 0x002A, 0x0100 + device.on_response(request, response); + + ASSERT_EQ(device.holding_calls.size(), 1u); + const auto &call = device.holding_calls.front(); + EXPECT_EQ(call.start_address, 0x100); + EXPECT_EQ(call.registers, (std::vector{0x002A, 0x0100})); + EXPECT_FALSE(call.status.has_value()); +} + +TEST(ModbusClientDeviceFanOut, ReadInputRegistersDelegateToGeneric) { + GenericDevice device; + const uint8_t request[] = {0x04, 0x00, 0x10, 0x00, 0x01}; + const uint8_t response[] = {0x04, 0x02, 0x12, 0x34}; + device.on_response(request, response); + + EXPECT_EQ(device.calls, 1); + EXPECT_EQ(device.register_type, EntityType::INPUT_REGISTER); + EXPECT_EQ(device.start_address, 0x10); + EXPECT_EQ(device.registers, (std::vector{0x1234})); +} + +TEST(ModbusClientDeviceFanOut, ReadDiscreteInputsDelegateToGenericBits) { + GenericDevice device; + const uint8_t request[] = {0x02, 0x00, 0x20, 0x00, 0x05}; // 5 inputs at 0x20 + const uint8_t response[] = {0x02, 0x01, 0x15}; + device.on_response(request, response); + + EXPECT_EQ(device.calls, 1); + EXPECT_EQ(device.register_type, EntityType::DISCRETE_INPUT); + EXPECT_EQ(device.start_address, 0x20); + EXPECT_EQ(device.bit_count, 5); +} + +// A CRC-valid response whose length does not match its request cannot be decoded per the +// function-code contract: it goes to the catch-all with the raw PDUs, not to the typed callback. +TEST(ModbusClientDeviceFanOut, ReadRegistersMismatchedLengthGoesToCatchAll) { + RecordingDevice device; + // Request asks for 4 registers but the response only carries 1. + const uint8_t request[] = {0x03, 0x00, 0x00, 0x00, 0x04}; + const uint8_t response[] = {0x03, 0x02, 0xBE, 0xEF}; + device.on_response(request, response); + + EXPECT_TRUE(device.holding_calls.empty()); + ASSERT_EQ(device.custom_responses.size(), 1u); + EXPECT_EQ(device.custom_responses.front(), (std::vector(response, response + sizeof(response)))); +} + +// Coil responses are validated the same way: byte count must be ceil(count / 8). +TEST(ModbusClientDeviceFanOut, ReadCoilsMismatchedLengthGoesToCatchAll) { + RecordingDevice device; + const uint8_t request[] = {0x01, 0x00, 0x13, 0x00, 0x13}; // 19 coils -> 3 packed bytes + const uint8_t response[] = {0x01, 0x02, 0xCD, 0x6B}; // only 2 + device.on_response(request, response); + + EXPECT_TRUE(device.coil_calls.empty()); + EXPECT_EQ(device.custom_responses.size(), 1u); +} + +TEST(ModbusClientDeviceFanOut, ReadCoilsSuccess) { + RecordingDevice device; + const uint8_t request[] = {0x01, 0x00, 0x13, 0x00, 0x13}; // 19 coils at 0x13 + const uint8_t response[] = {0x01, 0x03, 0xCD, 0x6B, 0x05}; + device.on_response(request, response); + + ASSERT_EQ(device.coil_calls.size(), 1u); + const auto &call = device.coil_calls.front(); + EXPECT_EQ(call.start_address, 0x13); + EXPECT_EQ(call.count, 19); + EXPECT_EQ(call.packed, (std::vector{0xCD, 0x6B, 0x05})); + EXPECT_FALSE(call.status.has_value()); + // first coil = bit 0 of byte 0 + EXPECT_TRUE(helpers::bit_from_packed(0, call.packed)); + EXPECT_FALSE(helpers::bit_from_packed(1, call.packed)); +} + +TEST(ModbusClientDeviceFanOut, WriteSingleRegisterSuccess) { + RecordingDevice device; + const uint8_t request[] = {0x06, 0x00, 0x01, 0x00, 0x03}; + device.on_response(request, request); // echo + + ASSERT_EQ(device.write_single_register_calls.size(), 1u); + const auto &call = device.write_single_register_calls.front(); + EXPECT_EQ(call.address, 1); + EXPECT_EQ(call.value, 3); + EXPECT_FALSE(call.status.has_value()); +} + +TEST(ModbusClientDeviceFanOut, WriteSingleCoilSuccess) { + RecordingDevice device; + const uint8_t request[] = {0x05, 0x00, 0xAC, 0xFF, 0x00}; + device.on_response(request, request); + + ASSERT_EQ(device.write_single_coil_calls.size(), 1u); + EXPECT_EQ(device.write_single_coil_calls.front().address, 0xAC); + EXPECT_EQ(device.write_single_coil_calls.front().value, 1u); +} + +TEST(ModbusClientDeviceFanOut, WriteErrorReportsRequestArgumentsAndStatus) { + RecordingDevice device; + const uint8_t request[] = {0x06, 0x00, 0x01, 0x00, 0x03}; + const uint8_t exception[] = {0x86, 0x02}; // ILLEGAL_DATA_ADDRESS + device.on_error(request, static_cast(exception[1])); + + ASSERT_EQ(device.write_single_register_calls.size(), 1u); + const auto &call = device.write_single_register_calls.front(); + EXPECT_EQ(call.address, 1); + EXPECT_EQ(call.value, 3); + EXPECT_EQ(call.status, ExceptionCode::ILLEGAL_DATA_ADDRESS); +} + +TEST(ModbusClientDeviceFanOut, ReadErrorReportsEmptyDataAndStatus) { + RecordingDevice device; + const uint8_t request[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + const uint8_t exception[] = {0x83, 0x02}; + device.on_error(request, static_cast(exception[1])); + + ASSERT_EQ(device.holding_calls.size(), 1u); + const auto &call = device.holding_calls.front(); + EXPECT_EQ(call.start_address, 0x100); + EXPECT_TRUE(call.registers.empty()); + EXPECT_EQ(call.status, ExceptionCode::ILLEGAL_DATA_ADDRESS); +} + +TEST(ModbusClientDeviceFanOut, CustomFunctionCodeGoesToCatchAll) { + RecordingDevice device; + const uint8_t request[] = {0x47, 0x01, 0x02, 0x03, 0x04}; + const uint8_t response[] = {0x47, 0xAA, 0xBB}; + device.on_response(request, response); + + ASSERT_EQ(device.custom_requests.size(), 1u); + EXPECT_EQ(device.custom_requests.front(), (std::vector{0x47, 0x01, 0x02, 0x03, 0x04})); + EXPECT_EQ(device.custom_responses.front(), (std::vector{0x47, 0xAA, 0xBB})); + EXPECT_FALSE(device.custom_statuses.front().has_value()); + EXPECT_TRUE(device.holding_calls.empty()); + + // On failure the catch-all receives an empty response and the status (the exception code). + const uint8_t exception[] = {0xC7, 0x02}; + device.on_error(request, static_cast(exception[1])); + ASSERT_EQ(device.custom_statuses.size(), 2u); + EXPECT_EQ(device.custom_statuses.back(), ExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_TRUE(device.custom_responses.back().empty()); +} + +// A write ack only echoes the start address and count, so the data that was written is decoded from the +// request PDU: [0] function code, [1..2] start address, [3..4] count, [5] byte count, [6..] data. +TEST(ModbusClientDeviceFanOut, WriteMultipleAcksReportStartAndData) { + RecordingDevice device; + // Write 2 registers (0x0001, 0x0002) at 0x0020: byte count 4, data from offset 6. + const uint8_t reg_request[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01, 0x00, 0x02}; + const uint8_t reg_ack[] = {0x10, 0x00, 0x20, 0x00, 0x02}; + device.on_response(reg_request, reg_ack); + // Write 10 coils at 0x0030: byte count 2, packed bits 0xFF 0x03 from offset 6. + const uint8_t coil_request[] = {0x0F, 0x00, 0x30, 0x00, 0x0A, 0x02, 0xFF, 0x03}; + const uint8_t coil_ack[] = {0x0F, 0x00, 0x30, 0x00, 0x0A}; + device.on_response(coil_request, coil_ack); + + ASSERT_EQ(device.write_multiple_registers_calls.size(), 1u); + EXPECT_EQ(device.write_multiple_registers_calls.front().start_address, 0x20); + EXPECT_EQ(device.write_multiple_registers_calls.front().registers, (std::vector{0x0001, 0x0002})); + ASSERT_EQ(device.write_multiple_coils_calls.size(), 1u); + EXPECT_EQ(device.write_multiple_coils_calls.front().start_address, 0x30); + EXPECT_EQ(device.write_multiple_coils_calls.front().count, 10); + EXPECT_EQ(device.write_multiple_coils_calls.front().packed, (std::vector{0xFF, 0x03})); +} + +// A truncated request (byte-count header promises more data than the PDU carries) is not a standard +// write-multiple, so it is diverted to on_custom_response() - never clamped and delivered as if complete. +TEST(ModbusClientDeviceFanOut, WriteMultipleTruncatedRequestDispatchesAsCustom) { + RecordingDevice device; + // Header claims 2 registers / 4 data bytes, but only one register's worth is present. + const uint8_t reg_request[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01}; + const uint8_t reg_ack[] = {0x10, 0x00, 0x20, 0x00, 0x02}; + device.on_response(reg_request, reg_ack); + + EXPECT_TRUE(device.write_multiple_registers_calls.empty()); + ASSERT_EQ(device.custom_requests.size(), 1u); + EXPECT_EQ(device.custom_requests.front(), (std::vector{0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01})); +} + +// A request whose byte-count header disagrees with its own quantity field (here: 2 registers but a +// byte count of 2 instead of 4, with matching data) is non-standard and diverted to the catch-all. +TEST(ModbusClientDeviceFanOut, WriteMultipleInconsistentByteCountDispatchesAsCustom) { + RecordingDevice device; + const uint8_t reg_request[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x02, 0x00, 0x01}; + const uint8_t reg_ack[] = {0x10, 0x00, 0x20, 0x00, 0x02}; + device.on_response(reg_request, reg_ack); + + EXPECT_TRUE(device.write_multiple_registers_calls.empty()); + EXPECT_EQ(device.custom_requests.size(), 1u); +} + +// An exception on a read still dispatches to the typed callback (empty data, status set): the gate must +// not require a standard response on the failure path, because on_error() delivers an empty response by +// design. +TEST(ModbusClientDeviceFanOut, ReadErrorWithEmptyResponseStillDispatchesTyped) { + RecordingDevice device; + const uint8_t read_request[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + device.on_error(read_request, ExceptionCode::ILLEGAL_DATA_ADDRESS); + + ASSERT_EQ(device.holding_calls.size(), 1u); + EXPECT_TRUE(device.holding_calls.front().registers.empty()); + EXPECT_EQ(device.holding_calls.front().status, ExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_TRUE(device.custom_requests.empty()); +} + +// An error on a coil read must deliver a PackedBits view whose size() is zero - the count must never +// promise bits that have no bytes behind them (operator[] is unchecked). +TEST(ModbusClientDeviceFanOut, ReadCoilsErrorDeliversZeroCountBits) { + RecordingDevice device; + const uint8_t read_request[] = {0x01, 0x01, 0x00, 0x00, 0x0A}; + device.on_error(read_request, ExceptionCode::SERVICE_DEVICE_FAILURE); + + ASSERT_EQ(device.coil_calls.size(), 1u); + EXPECT_EQ(device.coil_calls.front().count, 0); + EXPECT_TRUE(device.coil_calls.front().packed.empty()); +} + +// Single-write acks: on success the delivered value is the device's echo (real read-back); +// on an exception it falls back to the request copy. +TEST(ModbusTypedDispatch, SingleWriteAckPrefersTheResponseEcho) { + RecordingDevice device; + const uint8_t request[] = {0x06, 0x00, 0x10, 0x00, 0x2A}; + const uint8_t echo_clamped[] = {0x06, 0x00, 0x10, 0x00, 0x28}; // device clamped 42 -> 40 + device.on_response(request, echo_clamped); + ASSERT_EQ(device.write_single_register_calls.size(), 1u); + EXPECT_EQ(device.write_single_register_calls.front().value, 0x0028); // the echo, not the request + + device.on_error(request, ExceptionCode::ILLEGAL_DATA_VALUE); + ASSERT_EQ(device.write_single_register_calls.size(), 2u); + EXPECT_EQ(device.write_single_register_calls.back().value, 0x002A); // exception: request copy +} + +} // namespace esphome::modbus::testing diff --git a/tests/components/modbus/modbus_client_hub_test.cpp b/tests/components/modbus/modbus_client_hub_test.cpp new file mode 100644 index 0000000000..4d5b4e7ee8 --- /dev/null +++ b/tests/components/modbus/modbus_client_hub_test.cpp @@ -0,0 +1,1966 @@ +#include + +#include +#include +#include +#include + +#include "common.h" +#include "esphome/components/modbus/modbus.h" +#include "esphome/core/hal.h" + +namespace esphome::modbus::testing { + +namespace { + +// Exposes the frame state machine so tests can drive it without a UART (force_send_next(), +// timeout_waiting(), sweep_for_test() stand in for the loop() transmit/watchdog/sweep steps). +class NoResponseProbeHub : public ModbusClientHub { + public: + // The old "queue" view: entries awaiting transmission, in STORAGE order (selection order is + // what the engine transmits by; use next_ready() for that). + size_t queued_frames() const { + size_t count = 0; + for (const auto &cmd : this->tx_buffer_) { + if (cmd.state == FrameState::READY) + count++; + } + return count; + } + // A never-null placeholder to return when a lookup fails, so a tripped EXPECT/ADD_FAILURE reports + // the assertion instead of dereferencing null / an empty deque and segfaulting the whole suite. + static const ModbusDeviceCommand &dummy_command() { + static const uint8_t DUMMY_PDU[1] = {0x00}; + static ModbusDeviceCommand cmd(nullptr, 0, std::span(DUMMY_PDU, 1)); + return cmd; + } + const ModbusDeviceCommand &queued(size_t i) const { + for (const auto &cmd : this->tx_buffer_) { + if (cmd.state == FrameState::READY && i-- == 0) + return cmd; + } + ADD_FAILURE() << "no READY entry at that index"; + return dummy_command(); + } + size_t entries() const { return this->tx_buffer_.size(); } + const ModbusDeviceCommand *next_ready() { return this->select_next_ready_(); } + bool waiting() const { return this->waiting_for_response_; } + const ModbusDeviceCommand &waiting_command() { + ModbusDeviceCommand *cmd = this->find_waiting_(); + EXPECT_NE(cmd, nullptr); + return cmd != nullptr ? *cmd : dummy_command(); + } + + void sweep_for_test() { this->sweep_(); } + void send_next_for_test() { + this->send_next_frame_(); + this->sweep_(); // a transmit failure's on_not_sent() is delivered by the loop's sweep + } + void force_send_next() { + ModbusDeviceCommand *cmd = this->select_next_ready_(); + ASSERT_NE(cmd, nullptr) << "no READY entry to send"; + cmd->state = FrameState::WAITING; + this->waiting_for_response_ = true; + } + // Drives the real response/interruption branches, followed by the loop's sweep. + void receive_frame_for_test(uint8_t address, std::span pdu) { + this->process_modbus_server_frame(address, pdu); + this->sweep_(); + } + void timeout_waiting() { + this->sweep_(); // deliver anything already owed (e.g. an interruption's on_no_response) + this->expire_waiting_(); + this->sweep_(); + } +}; + +// A device with a scripted answer to on_no_response(). +class RetryingDevice : public ModbusClientDevice { + public: + RetryingDevice(ModbusClientHub *hub, uint8_t address, bool retry) : ModbusClientDevice(hub, address), retry_(retry) {} + bool on_no_response(std::span request_pdu) override { + this->no_response_count_++; + return this->retry_; + } + int no_response_count_{0}; + + protected: + bool retry_{false}; +}; + +// A device that clears its own queued traffic from inside the no-response callback, then asks for a retry. +class ClearingRetryDevice : public ModbusClientDevice { + public: + ClearingRetryDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + bool on_no_response(std::span request_pdu) override { + this->no_response_count_++; + this->clear_tx_queue_for_device(); // detaches this device from the waiting slot mid-callback + return true; // and still requests a retry + } + int no_response_count_{0}; +}; + +constexpr uint8_t READ_PDU[] = {0x03, 0x01, 0x00, 0x00, 0x02}; // read 2 holding registers at 0x100 + +StaticVector read_pdu() { + StaticVector pdu; + pdu.assign(READ_PDU, READ_PDU + sizeof(READ_PDU)); + return pdu; +} + +} // namespace + +// A device that requests a retry gets the frame the hub was holding re-queued on its behalf, +// byte-identical and still routed to the same device. +TEST(ModbusClientHubNoResponse, RetryRequeuesWaitingFrame) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/true); + + device.send_pdu(read_pdu()); + ASSERT_EQ(hub.queued_frames(), 1u); + hub.force_send_next(); + ASSERT_EQ(hub.queued_frames(), 0u); + ASSERT_TRUE(hub.waiting()); + + hub.timeout_waiting(); + + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_FALSE(hub.waiting()); + ASSERT_EQ(hub.queued_frames(), 1u); + const ModbusDeviceCommand &requeued = hub.queued(0); + EXPECT_EQ(requeued.device, &device); + // address + PDU + CRC + ASSERT_EQ(requeued.frame.size(), sizeof(READ_PDU) + 3); + EXPECT_EQ(requeued.frame.address(), 0x02); + ASSERT_EQ(requeued.frame.pdu().size(), sizeof(READ_PDU)); + EXPECT_EQ(0, memcmp(requeued.frame.pdu().data(), READ_PDU, sizeof(READ_PDU))); +} + +// A device that declines the retry has the frame dropped. +TEST(ModbusClientHubNoResponse, NoRetryDropsWaitingFrame) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + + hub.timeout_waiting(); + + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.queued_frames(), 0u); +} + +// After the device is detached from the waiting frame (e.g. clear_tx_queue_for_device on +// destruction), a timeout must not deliver a callback or re-queue anything. +TEST(ModbusClientHubNoResponse, DetachedDeviceIsNotNotified) { + NoResponseProbeHub hub; + { + RetryingDevice device(&hub, 0x02, /*retry=*/true); + device.send_pdu(read_pdu()); + hub.force_send_next(); + // device destructor clears its queue entries, including the waiting frame's device pointer + } + ASSERT_TRUE(hub.waiting()); + EXPECT_EQ(hub.waiting_command().device, nullptr); + + hub.timeout_waiting(); + + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.queued_frames(), 0u); +} + +// An unexpected frame interrupts the transaction: the entry becomes an INTERRUPTED shell that +// ignores this transaction and blocks tx until the send-wait timeout, where it gets its single +// on_no_response() - a granted retry is requeued there, like any other timeout. +TEST(ModbusClientHubNoResponse, RetryBehindInterruptedShell) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/true); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + + // A frame from the wrong address (0x07, expected 0x02) hits the unexpected-frame branch. + const uint8_t stray_pdu[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x07, stray_pdu); + hub.sweep_for_test(); + + EXPECT_EQ(device.no_response_count_, 0); // not notified early: it waits out the timeout + ASSERT_TRUE(hub.waiting()); // and keeps blocking the bus + EXPECT_EQ(hub.waiting_command().state, FrameState::INTERRUPTED); + EXPECT_EQ(hub.waiting_command().pending, 1u); + + // The send-wait timeout delivers on_no_response and requeues the granted retry. + hub.timeout_waiting(); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(device.no_response_count_, 1); + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).device, &device); +} + +// The declined-retry interrupted shell blocks until the send-wait timeout, then gets its single +// on_no_response() there and retires with nothing left to send. +TEST(ModbusClientHubNoResponse, InterruptedShellDeclinedRetryRetiresOnRelease) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + + const uint8_t stray_pdu[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x07, stray_pdu); // wrong address: interrupts the transaction + hub.sweep_for_test(); + + EXPECT_EQ(device.no_response_count_, 0); // not notified early + ASSERT_TRUE(hub.waiting()); // the shell still blocks the wire + EXPECT_EQ(hub.waiting_command().state, FrameState::INTERRUPTED); + EXPECT_EQ(hub.waiting_command().pending, 1u); + + hub.timeout_waiting(); // on_no_response (declined), then the shell retires + + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.queued_frames(), 0u); + EXPECT_EQ(device.no_response_count_, 1); +} + +// A callback that detaches the device (clear_tx_queue_for_device()) wins over its own retry request: +// no orphaned frame with a null device is re-queued. +TEST(ModbusClientHubNoResponse, MidCallbackClearCancelsRetry) { + NoResponseProbeHub hub; + ClearingRetryDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + hub.timeout_waiting(); + + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_EQ(hub.queued_frames(), 0u); // the retry was not re-queued for a detached device + EXPECT_FALSE(hub.waiting()); +} + +// Writes jump ahead of queued reads; reads keep FIFO order among themselves. +TEST(ModbusClientHubPriority, WritesSendBeforeQueuedReads) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + const uint8_t read_a[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + const uint8_t read_b[] = {0x03, 0x02, 0x00, 0x00, 0x02}; + const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF}; + device.send_pdu(read_a); + device.send_pdu(read_b); + device.send_pdu(write_pdu); + + ASSERT_EQ(hub.queued_frames(), 3u); + hub.force_send_next(); + EXPECT_EQ(hub.waiting_command().frame.pdu()[0], 0x06); // the write transmits first + hub.timeout_waiting(); + hub.force_send_next(); + EXPECT_EQ(hub.waiting_command().frame.pdu()[1], 0x01); // reads follow in FIFO order + hub.timeout_waiting(); + hub.force_send_next(); + EXPECT_EQ(hub.waiting_command().frame.pdu()[1], 0x02); +} + +// Re-requesting a queued frame is absorbed into the existing entry instead of queueing a duplicate. +TEST(ModbusClientHubPriority, DuplicateQueuedFrameAbsorbedNotDuplicated) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + device.send_pdu(read_pdu()); + device.send_pdu(read_pdu()); + + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).pending, 2u); // one entry standing for two accepted requests +} + +// Re-requesting the frame currently waiting is absorbed into the waiting entry; after a +// no-response timeout the absorbed request still gets its run even though the device declines a +// retry, and a second timeout does not run it again. +TEST(ModbusClientHubPriority, InFlightDuplicateRunsOnceMore) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + device.send_pdu(read_pdu()); // duplicate of the waiting frame + + EXPECT_EQ(hub.queued_frames(), 0u); // not queued twice + EXPECT_EQ(hub.waiting_command().pending, 2u); + + hub.timeout_waiting(); + ASSERT_EQ(hub.queued_frames(), 1u); // the timeout resolved one request; the absorbed one runs + EXPECT_EQ(hub.queued(0).pending, 1u); + + hub.force_send_next(); + hub.timeout_waiting(); + EXPECT_EQ(hub.queued_frames(), 0u); // the last request resolved; nothing left to run +} + +// An entry with an absorbed extra request that times out while the device asks to retry: the +// retry is not a resolution, so BOTH requests remain pending rather than one being dropped - +// which would leave that caller without a resolution. +TEST(ModbusClientHubPriority, AbsorbedRequestSurvivesDeviceRetry) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/true); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + device.send_pdu(read_pdu()); // duplicate of the waiting frame -> absorbed + ASSERT_EQ(hub.waiting_command().pending, 2u); + + hub.timeout_waiting(); // no response; the device requests a retry + + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).pending, 2u); // preserved: the retry resolved nothing +} + +// A continuous read re-queues itself (at the lowest priority) after each successful response, +// but not after an exception response. +TEST(ModbusClientHubPriority, ContinuousReadRequeuesOnSuccessOnly) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + device.read_holding_registers(0x100, 2, {.continuous = true}); + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_TRUE(hub.queued(0).continuous); + hub.force_send_next(); + + // A matching successful response cycles the continuous entry back to READY. + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_TRUE(hub.queued(0).continuous); + + // An exception response ends the poll. + hub.force_send_next(); + const uint8_t exception_response[] = {0x83, 0x02}; + hub.receive_frame_for_test(0x02, exception_response); + EXPECT_EQ(hub.queued_frames(), 0u); +} + +// A continuous read that gets no response and is retried stays continuous: an explicit retry of a +// continuous poll is assumed to still want continuous polling (the entry stays continuous). +TEST(ModbusClientHubPriority, RetriedContinuousReadStaysContinuous) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/true); + + device.read_holding_registers(0x100, 2, {.continuous = true}); + ASSERT_EQ(hub.queued_frames(), 1u); + ASSERT_TRUE(hub.queued(0).continuous); + hub.force_send_next(); + + hub.timeout_waiting(); // no response -> device requests retry + + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_TRUE(hub.queued(0).continuous); // the retried poll stays continuous +} + +// A one-shot duplicate downgrades a continuous poll to a one-shot (the mirror of a continuous +// duplicate upgrading a one-shot): the entry runs one more cycle to serve the request, then stops. +TEST(ModbusClientHubPriority, DuplicateSendDowngradesContinuous) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + device.read_holding_registers(0x100, 2, {.continuous = true}); + ASSERT_EQ(hub.queued_frames(), 1u); + ASSERT_TRUE(hub.queued(0).continuous); + + device.read_holding_registers(0x100, 2); // one-shot duplicate downgrades the poll + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_FALSE(hub.queued(0).continuous); + EXPECT_EQ(hub.queued(0).pending, 1u); + + // It runs one more cycle to serve the request, then stops - not re-queued as a poll. + hub.force_send_next(); + EXPECT_FALSE(hub.waiting_command().continuous); + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); + EXPECT_EQ(hub.queued_frames(), 0u); + EXPECT_EQ(hub.entries(), 0u); +} + +namespace { +// Re-sends its frame once as a one-shot from inside on_error(), to exercise the downgrade branch +// when the poll it duplicates has already reached a terminal (pending drained to 0). +class ResendOnErrorDevice : public ModbusClientDevice { + public: + ResendOnErrorDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_error(std::span request_pdu, ExceptionCode exception_code) override { + this->error_count_++; + if (this->resend_) { + this->resend_ = false; + this->read_holding_registers(0x100, 2); // one-shot re-send from inside the failure callback + } + } + int error_count_{0}; + bool resend_{true}; +}; +} // namespace + +// A one-shot re-send issued from inside a continuous poll's failure callback must still run. The +// poll's exception terminal has already drained pending to 0, so the re-send absorbs into that entry +// via the downgrade branch - which must restore the debt, or the sweep erases the entry with the +// request never sent and no callback delivered. +TEST(ModbusClientHubPriority, DowngradeAfterTerminalKeepsRequestAlive) { + NoResponseProbeHub hub; + ResendOnErrorDevice device(&hub, 0x02); + + device.read_holding_registers(0x100, 2, {.continuous = true}); + ASSERT_EQ(hub.queued_frames(), 1u); + ASSERT_TRUE(hub.queued(0).continuous); + + hub.force_send_next(); + const uint8_t exception_response[] = {0x83, 0x02}; + hub.receive_frame_for_test(0x02, exception_response); // exception ends the poll; on_error re-sends + + EXPECT_EQ(device.error_count_, 1); // one terminal delivered so far + ASSERT_EQ(hub.queued_frames(), 1u); // the re-send survived the sweep instead of being erased + EXPECT_FALSE(hub.queued(0).continuous); // downgraded to a one-shot + EXPECT_EQ(hub.queued(0).pending, 1u); // debt restored so the request runs + + // And it runs to its own terminal - a good response this time - then the entry is gone. + hub.force_send_next(); + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); + EXPECT_EQ(hub.queued_frames(), 0u); + EXPECT_EQ(hub.entries(), 0u); +} + +// Requesting continuous polling for a frame that is already queued as a one-shot turns that entry +// into the continuous poll instead of leaving a promotion that never polls. +TEST(ModbusClientHubPriority, ContinuousRequestUpgradesQueuedDuplicate) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + device.read_holding_registers(0x100, 2); + ASSERT_EQ(hub.queued_frames(), 1u); + ASSERT_FALSE(hub.queued(0).continuous); + + device.read_holding_registers(0x100, 2, {.continuous = true}); + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_TRUE(hub.queued(0).continuous); + + // And it behaves as a poll from here: success cycles it back to READY. + hub.force_send_next(); + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_TRUE(hub.queued(0).continuous); +} + +// The transmit order is one key with three levels: writes, then one-shot reads, then continuous +// polls - a poll only gets the bus when nothing else wants it. +TEST(ModbusClientHubPriority, WritesThenOneShotReadsThenContinuousPolls) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + // Queued oldest-first in the opposite order to the one they must transmit in, so age cannot be + // what produces the expected sequence. + device.read_holding_registers(0x100, 2, {.continuous = true}); + const uint8_t one_shot[] = {0x03, 0x02, 0x00, 0x00, 0x01}; + const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF}; + device.send_pdu(one_shot); + device.send_pdu(write_pdu); + ASSERT_EQ(hub.queued_frames(), 3u); + EXPECT_EQ(hub.queued(0).priority(), CommandPriority::CONTINUOUS); + EXPECT_EQ(hub.queued(1).priority(), CommandPriority::READ); + EXPECT_EQ(hub.queued(2).priority(), CommandPriority::WRITE); + + hub.force_send_next(); + EXPECT_EQ(hub.waiting_command().frame.pdu()[0], 0x06); // the write goes first + hub.timeout_waiting(); + hub.force_send_next(); + EXPECT_EQ(hub.waiting_command().frame.pdu()[1], 0x02); // then the one-shot read + hub.timeout_waiting(); + hub.force_send_next(); + EXPECT_TRUE(hub.waiting_command().continuous); // and the poll takes what is left +} + +// continuous is ignored for writes: the frame still sends at WRITE priority, once. +TEST(ModbusClientHubPriority, ContinuousIgnoredForWrites) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF}; + device.send_pdu(write_pdu, {.continuous = true}); + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).priority(), CommandPriority::WRITE); + EXPECT_FALSE(hub.queued(0).continuous); +} + +// A queued continuous poll does not count against immediate-send readiness: it ranks below every +// one-shot, so a new one-shot goes out ahead of it. A queued one-shot does count. +TEST(ModbusClientHubPriority, ContinuousPollDoesNotBlockImmediateSend) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + EXPECT_TRUE(hub.tx_buffer_empty()); // nothing queued + device.read_holding_registers(0x100, 2, {.continuous = true}); + ASSERT_TRUE(hub.queued(0).continuous); + EXPECT_TRUE(hub.tx_buffer_empty()); // a READY continuous poll still leaves room to send now + + device.read_holding_registers(0x200, 2); // a one-shot does count + EXPECT_FALSE(hub.tx_buffer_empty()); +} + +// A device whose sent/not-sent callbacks are counted. +namespace { +class SentCountingDevice : public ModbusClientDevice { + public: + SentCountingDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_sent(std::span request_pdu) override { + this->sent_count_++; + this->last_sent_pdu_.assign(request_pdu.begin(), request_pdu.end()); + } + void on_not_sent(std::span request_pdu) override { + this->not_sent_count_++; + this->last_not_sent_pdu_.assign(request_pdu.begin(), request_pdu.end()); + } + int sent_count_{0}; + int not_sent_count_{0}; + std::vector last_sent_pdu_; + std::vector last_not_sent_pdu_; +}; +} // namespace + +// A write is never requeueable, so its entry can serve exactly one request: a duplicate of a +// queued write is refused at the door rather than earning the write an extra transmission. +TEST(ModbusClientHubPriority, DuplicateQueuedWriteRefused) { + NoResponseProbeHub hub; + SentCountingDevice device(&hub, 0x02); + + const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF}; + EXPECT_TRUE(device.send_pdu(write_pdu)); + EXPECT_FALSE(device.send_pdu(write_pdu)); // duplicate write: refused + hub.sweep_for_test(); + + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).priority(), CommandPriority::WRITE); + EXPECT_EQ(hub.queued(0).pending, 1u); // a write's cap + EXPECT_EQ(device.not_sent_count_, 0); // refusals are returned, never delivered +} + +// Requeueability is an allow-list of the standard reads: a custom function code's idempotency is +// unknown, so its duplicate is refused like a write's instead of earning a silent re-send. +TEST(ModbusClientHubPriority, DuplicateCustomFunctionCodeRefused) { + NoResponseProbeHub hub; + SentCountingDevice device(&hub, 0x02); + + const uint8_t custom_pdu[] = {0x41, 0x01, 0x02}; // user-defined function code + EXPECT_TRUE(device.send_pdu(custom_pdu)); + EXPECT_FALSE(device.send_pdu(custom_pdu)); // duplicate custom command: refused + hub.sweep_for_test(); + + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).pending, 1u); // the non-requeueable cap of one run + EXPECT_EQ(device.not_sent_count_, 0); +} + +// An anonymous duplicate (no device - the YAML-lambda path) is always dropped, never promoted: +// with no callback there is no lifecycle to absorb into and no owner to route a re-run to. +TEST(ModbusClientHubPriority, AnonymousDuplicateDroppedNotPromoted) { + NoResponseProbeHub hub; + + const uint8_t read[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + hub.send_pdu(0x02, read); + hub.send_pdu(0x02, read); // anonymous duplicate: dropped + + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).pending, 1u); // never absorbed for a null owner +} + +// A retried entry is re-stamped to the queue tail: reads that arrived while it was waiting get +// their turn before the retry, so a frame that keeps timing out cannot starve the rest of the bus. +TEST(ModbusClientHubPriority, RetriedReadGoesBehindFreshReads) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/true); + + device.send_pdu(read_pdu()); + hub.force_send_next(); // the frame that will time out and retry + device.send_pdu(read_pdu()); // waiting duplicate: absorbed into the waiting entry + const uint8_t fresh_a[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + const uint8_t fresh_b[] = {0x03, 0x00, 0x20, 0x00, 0x01}; + device.send_pdu(fresh_a); + device.send_pdu(fresh_b); + ASSERT_EQ(hub.queued_frames(), 2u); + + hub.timeout_waiting(); // device retries; the entry returns to READY behind the fresh reads + + ASSERT_EQ(hub.queued_frames(), 3u); + const ModbusDeviceCommand *next = hub.next_ready(); + ASSERT_NE(next, nullptr); + EXPECT_EQ(next->frame.pdu()[2], 0x10); // fresh reads keep FIFO order ahead of the retry + hub.force_send_next(); + hub.timeout_waiting(); + hub.force_send_next(); + EXPECT_EQ(hub.waiting_command().frame.pdu()[2], 0x20); + hub.timeout_waiting(); + hub.force_send_next(); // the retry gets its turn last, both requests still on the entry + EXPECT_TRUE(std::equal(hub.waiting_command().frame.pdu().begin(), hub.waiting_command().frame.pdu().end(), READ_PDU)); + EXPECT_EQ(hub.waiting_command().pending, 2u); +} + +// An absorbed duplicate does not move the entry back in line: seq belongs to the entry, and only +// re-entering the line (retry, resolved request, continuous cycle) re-stamps it. +TEST(ModbusClientHubPriority, AbsorbedDuplicateKeepsPlaceInLine) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/false); + + const uint8_t read_a[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + const uint8_t read_b[] = {0x03, 0x00, 0x20, 0x00, 0x01}; + device.send_pdu(read_a); + device.send_pdu(read_b); + device.send_pdu(read_a); // duplicate of the older entry: absorbed, place unchanged + ASSERT_EQ(hub.queued_frames(), 2u); + + const ModbusDeviceCommand *next = hub.next_ready(); + ASSERT_NE(next, nullptr); + EXPECT_EQ(next->frame.pdu()[2], 0x10); // read_a still transmits first + EXPECT_EQ(next->pending, 2u); +} + +// A write that is retried after a no-response keeps the WRITE class, so it stays ahead of reads, +// and a later duplicate still resolves against it instead of queueing twice. +TEST(ModbusClientHubPriority, RetriedWriteKeepsWritePriorityAndStaysNonRequeueable) { + NoResponseProbeHub hub; + RetryingDevice device(&hub, 0x02, /*retry=*/true); + + const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF}; + device.send_pdu(write_pdu); + hub.force_send_next(); + hub.timeout_waiting(); // no response -> device requests retry -> back to READY + + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).priority(), CommandPriority::WRITE); // retry preserves the WRITE class + + device.send_pdu(write_pdu); // duplicate of the retried write + ASSERT_EQ(hub.queued_frames(), 1u); // still not queued twice... + EXPECT_EQ(hub.queued(0).priority(), CommandPriority::WRITE); + hub.sweep_for_test(); + EXPECT_EQ(hub.queued(0).pending, 1u); // ...the duplicate was refused at the door (write cap is 1) +} + +namespace { +// A hub that is never free to transmit. +class AlwaysBlockedHub : public NoResponseProbeHub { + public: + bool tx_blocked() override { return true; } +}; +} // namespace + +// Transmitting cannot fail, so a hub that is busy simply does not transmit: the frame keeps its +// place in the queue and goes out on a later loop, with no callback and no lifecycle change. (The +// caller owns the tx_blocked() check; send_frame_() has no gate of its own to refuse at.) +TEST(ModbusClientHubSent, BlockedHubDefersInsteadOfFailing) { + AlwaysBlockedHub hub; + SentCountingDevice device(&hub, 0x02); + + EXPECT_TRUE(device.send_pdu(read_pdu())); + hub.send_next_for_test(); + + EXPECT_EQ(device.sent_count_, 0); + EXPECT_EQ(device.not_sent_count_, 0); // nothing failed - it has not been attempted + ASSERT_EQ(hub.queued_frames(), 1u); // still queued, still owed exactly one terminal + EXPECT_EQ(hub.queued(0).pending, 1u); + EXPECT_FALSE(hub.waiting()); +} + +// on_sent() fires when the frame goes onto the wire, not when it is queued. +TEST(ModbusClientHubSent, FiresOnWireNotOnQueue) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); // frame timing derives from the baud rate + SentCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + EXPECT_EQ(device.sent_count_, 0); // queued only - nothing sent yet + + hub.send_next_for_test(); + EXPECT_EQ(device.sent_count_, 1); + EXPECT_EQ(device.not_sent_count_, 0); + // The callback identifies which command transmitted: it carries the request PDU. + EXPECT_EQ(device.last_sent_pdu_, (std::vector(READ_PDU, READ_PDU + sizeof(READ_PDU)))); + EXPECT_TRUE(hub.waiting()); +} + +namespace { +// tx_blocked() clear for send_next_frame_'s gate, then blocked for send_frame_'s post-delay re-check. +class RejectPostDelayHub : public NoResponseProbeHub { + public: + bool tx_blocked() override { return this->tx_blocked_calls_++ > 0; } + int tx_blocked_calls_{0}; +}; +} // namespace + +// A byte arriving during send_frame_'s pre-send delay blocks transmission after the caller's gate +// already passed. send_frame_ rejects, and send_next_frame_ leaves the frame READY to retry - it is +// not marked WAITING and the bus is not claimed. +TEST(ModbusClientHubSent, SendRejectedAfterDelayLeavesFrameReady) { + NullUART uart; + RejectPostDelayHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + SentCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.send_next_for_test(); // gate passes, send_frame_ rejects on the post-delay re-check + + EXPECT_EQ(device.sent_count_, 0); // nothing transmitted + EXPECT_FALSE(hub.waiting()); // the frame was left untouched, bus not claimed + ASSERT_EQ(hub.entries(), 1u); + EXPECT_EQ(hub.queued(0).state, FrameState::READY); // still selectable next loop +} + +// Counts response deliveries so requeue semantics can be pinned end to end. +namespace { +class DataCountingDevice : public ModbusClientDevice { + public: + DataCountingDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_response(std::span request_pdu, std::span response_pdu) override { + this->data_count_++; + } + void on_error(std::span request_pdu, ExceptionCode exception_code) override { this->error_count_++; } + bool on_no_response(std::span request_pdu) override { + this->no_response_count_++; + this->last_no_response_pdu_.assign(request_pdu.begin(), request_pdu.end()); + if (this->retries_ == 0) + return false; + this->retries_--; + return true; + } + void on_not_sent(std::span request_pdu) override { + this->not_sent_count_++; + this->last_not_sent_pdu_.assign(request_pdu.begin(), request_pdu.end()); + } + void on_sent(std::span request_pdu) override { this->sent_count_++; } + int terminals() const { + return this->data_count_ + this->error_count_ + this->no_response_count_ + this->not_sent_count_; + } + int data_count_{0}; + int error_count_{0}; + int no_response_count_{0}; + int not_sent_count_{0}; + int sent_count_{0}; + int retries_{0}; + std::vector last_not_sent_pdu_; + std::vector last_no_response_pdu_; +}; + +// Runs full send/respond cycles until the queue drains; returns the number of cycles executed. +int drain_with_responses(NoResponseProbeHub &hub, std::span response_pdu, int max_cycles = 10) { + int cycles = 0; + while (hub.queued_frames() != 0 && cycles < max_cycles) { + hub.force_send_next(); + hub.receive_frame_for_test(0x02, response_pdu); + cycles++; + } + return cycles; +} +} // namespace + +constexpr uint8_t OK_RESPONSE[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + +// One request produces exactly one data callback. +TEST(ModbusClientHubCallbackCount, SingleReadSingleCallback) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + drain_with_responses(hub, OK_RESPONSE); + + EXPECT_EQ(device.data_count_, 1); + EXPECT_EQ(device.not_sent_count_, 0); + EXPECT_EQ(hub.queued_frames(), 0u); + EXPECT_FALSE(hub.waiting()); +} + +// Requesting the same read twice while queued yields exactly two callbacks: +// the promoted entry completes, re-queues once (demoted), completes again, and stops. +TEST(ModbusClientHubCallbackCount, DuplicateReadExactlyTwoCallbacks) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + device.send_pdu(read_pdu()); + int cycles = drain_with_responses(hub, OK_RESPONSE); + + EXPECT_EQ(cycles, 2); + EXPECT_EQ(device.data_count_, 2); + EXPECT_EQ(device.not_sent_count_, 0); // both requests were served + EXPECT_EQ(hub.queued_frames(), 0u); +} + +namespace { +// Clears the address queue from inside its first response callback, so a duplicate still owed on the +// same entry has to be resolved (or, as things stand, is dropped) by that clear. +class ClearOnFirstResponseDevice : public DataCountingDevice { + public: + ClearOnFirstResponseDevice(ModbusClientHub *hub, uint8_t address) : DataCountingDevice(hub, address) {} + void on_response(std::span request_pdu, std::span response_pdu) override { + this->data_count_++; + if (this->data_count_ == 1) + this->clear_tx_queue_for_address(); // clear mid-completion, from inside the first response + } +}; +} // namespace + +// A duplicate read absorbs into one entry (pending 2). The first response resolves one request, and +// its callback clears the address queue mid-completion. The still-owed duplicate is a second accepted +// request, so it must get its own terminal - on_not_sent() - not be dropped silently. +TEST(ModbusClientHubCallbackCount, ClearFromResponseResolvesDuplicateWithNotSent) { + NoResponseProbeHub hub; + ClearOnFirstResponseDevice device(&hub, 0x02); + + EXPECT_TRUE(device.send_pdu(read_pdu())); + EXPECT_TRUE(device.send_pdu(read_pdu())); // absorbed: one entry, pending 2 + hub.force_send_next(); + hub.receive_frame_for_test(0x02, OK_RESPONSE); // response -> on_response -> clear, then sweep + + EXPECT_EQ(device.data_count_, 1); // exactly one response delivered + EXPECT_EQ(device.not_sent_count_, 1); // the duplicate resolved with a terminal, not dropped + EXPECT_EQ(device.terminals(), 2); // one terminal per accepted request + EXPECT_EQ(hub.queued_frames(), 0u); +} + +// A read entry serves two requests (this run plus one re-run), so the third identical request is +// refused at the door: two data callbacks, and no terminal for the request that was never taken. +TEST(ModbusClientHubCallbackCount, TripleReadRefusesTheThird) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + EXPECT_TRUE(device.send_pdu(read_pdu())); + EXPECT_TRUE(device.send_pdu(read_pdu())); + EXPECT_FALSE(device.send_pdu(read_pdu())); // the entry is already at its cap + hub.sweep_for_test(); + EXPECT_EQ(device.not_sent_count_, 0); // refused synchronously, nothing owed + int cycles = drain_with_responses(hub, OK_RESPONSE); + + EXPECT_EQ(cycles, 2); + EXPECT_EQ(device.data_count_, 2); + EXPECT_EQ(device.terminals(), 2); // exactly one per accepted request + EXPECT_EQ(hub.queued_frames(), 0u); +} + +// A duplicate write is refused at the door and the original write sends once - the caller learns +// immediately, and no lifecycle is created for the request that was never taken. +TEST(ModbusClientHubCallbackCount, DuplicateWriteRefusedWithoutLifecycle) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF}; + EXPECT_TRUE(device.send_pdu(write_pdu)); + EXPECT_FALSE(device.send_pdu(write_pdu)); + hub.sweep_for_test(); + + EXPECT_EQ(device.terminals(), 0); // the accepted write has not resolved; the other never existed + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).priority(), CommandPriority::WRITE); +} + +// An exception response is a terminal on its own: exactly one on_error(), no others, +// preceded by exactly one on_sent(). +TEST(ModbusClientHubCallbackCount, ErrorResponseIsSoleTerminal) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.send_next_for_test(); + const uint8_t exception_response[] = {0x83, 0x02}; + hub.receive_frame_for_test(0x02, exception_response); + + EXPECT_EQ(device.error_count_, 1); + EXPECT_EQ(device.terminals(), 1); + EXPECT_EQ(device.sent_count_, 1); +} + +// A timeout is a terminal on its own: exactly one on_no_response(), preceded by one +// on_sent(); a refused duplicate ends in on_not_sent() with NO on_sent(). +TEST(ModbusClientHubCallbackCount, NoResponseIsSoleTerminalAndNotSentHasNoSent) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.send_next_for_test(); + hub.timeout_waiting(); + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_EQ(device.terminals(), 1); + EXPECT_EQ(device.sent_count_, 1); + + // Unabsorbable duplicate: the second identical write is refused at the door - no lifecycle, no + // terminal, nothing sent. + const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF}; + EXPECT_TRUE(device.send_pdu(write_pdu)); + EXPECT_FALSE(device.send_pdu(write_pdu)); + hub.sweep_for_test(); + EXPECT_EQ(device.not_sent_count_, 0); + EXPECT_EQ(device.terminals(), 1); // still just the read's timeout + EXPECT_EQ(device.sent_count_, 1); + + // Drain the accepted write: its echo response is the data terminal, and the books balance. + hub.send_next_for_test(); + hub.receive_frame_for_test(0x02, write_pdu); + EXPECT_EQ(device.data_count_, 1); + EXPECT_EQ(device.terminals(), 2); // 2 accepted lifecycles, 2 terminals + EXPECT_EQ(device.sent_count_, 2); // 2 transmissions; the refused duplicate never sent +} + +// A device-requested retry starts a new lifecycle: each transmission gets its own sent + terminal. +TEST(ModbusClientHubCallbackCount, RetryLifecyclesEachGetSentAndTerminal) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + DataCountingDevice device(&hub, 0x02); + device.retries_ = 1; // ask for exactly one retry + + device.send_pdu(read_pdu()); + hub.send_next_for_test(); + hub.timeout_waiting(); // lifecycle 1: sent + no_response (retry requested -> re-queued) + ASSERT_EQ(hub.queued_frames(), 1u); + hub.send_next_for_test(); + hub.timeout_waiting(); // lifecycle 2: sent + no_response (retry declined -> done) + + EXPECT_EQ(device.no_response_count_, 2); + EXPECT_EQ(device.terminals(), 2); + EXPECT_EQ(device.sent_count_, 2); + EXPECT_EQ(hub.queued_frames(), 0u); + // The retried lifecycle's timeout carries the SAME request PDU as the first attempt. + EXPECT_EQ(device.last_no_response_pdu_, std::vector(READ_PDU, READ_PDU + sizeof(READ_PDU))); +} + +// A retry is a state flip on an existing entry, never a new insertion, so a full queue can't refuse +// it: fill the queue, time out the waiting frame with a retry, and it survives as READY. +TEST(ModbusClientHubCallbackCount, RetryIsNeverRefusedByFullQueue) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + DataCountingDevice device(&hub, 0x02); + device.retries_ = 1; + SentCountingDevice filler(&hub, 0x05); + + device.send_pdu(read_pdu()); + hub.force_send_next(); // waiting + device.send_pdu(read_pdu()); // absorbed: two requests pending + // Fill the remaining live capacity with distinct frames. + for (uint16_t i = 0; hub.entries() < MODBUS_TX_BUFFER_SIZE; i++) { + const uint8_t fill[] = {0x03, static_cast(i >> 8), static_cast(i & 0xFF), 0x00, 0x01}; + filler.send_pdu(fill); + } + + hub.timeout_waiting(); // retry requested; the entry flips back to READY regardless of capacity + + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_EQ(device.not_sent_count_, 0); // nothing was refused + ASSERT_EQ(hub.queued_frames(), MODBUS_TX_BUFFER_SIZE); + const ModbusDeviceCommand *next = hub.next_ready(); + ASSERT_NE(next, nullptr); + EXPECT_EQ(next->device, &filler); // round-robin: the retry re-stamped behind the fillers + // The retried entry survives as READY with both absorbed requests intact. + bool found = false; + for (size_t i = 0; i < hub.queued_frames(); i++) { + const ModbusDeviceCommand &cmd = hub.queued(i); + if (cmd.device == &device) { + EXPECT_EQ(cmd.pending, 2u); + found = true; + } + } + EXPECT_TRUE(found); +} + +// The deprecated device-side send_raw() reports an unusable payload the same way every other +// refused send does: false at the call site, with no queue entry and no callback. +namespace { +class NotSentCountingRawDevice : public ModbusClientDevice { + public: + NotSentCountingRawDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_not_sent(std::span request_pdu) override { this->not_sent_count_++; } + int not_sent_count_{0}; +}; +} // namespace + +TEST(ModbusClientHubQueue, SendRawTooShortIsRefusedAtTheDoor) { + NoResponseProbeHub hub; + NotSentCountingRawDevice device(&hub, 0x02); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + EXPECT_FALSE(device.send_raw({})); // too short to contain a PDU +#pragma GCC diagnostic pop + EXPECT_EQ(device.not_sent_count_, 0); // refusals are returned, never delivered + EXPECT_TRUE(hub.tx_buffer_empty()); +} + +// A continuous read: every wire transmission pairs one sent with one terminal, ending on the error. +TEST(ModbusClientHubCallbackCount, ContinuousLifecyclesBalance) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + DataCountingDevice device(&hub, 0x02); + + device.read_holding_registers(0x100, 2, {.continuous = true}); + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + const uint8_t exception_response[] = {0x83, 0x02}; + hub.send_next_for_test(); + hub.receive_frame_for_test(0x02, ok_response); // lifecycle 1 -> requeued + hub.send_next_for_test(); + hub.receive_frame_for_test(0x02, ok_response); // lifecycle 2 -> requeued + hub.send_next_for_test(); + hub.receive_frame_for_test(0x02, exception_response); // lifecycle 3 -> stops + + EXPECT_EQ(device.data_count_, 2); + EXPECT_EQ(device.error_count_, 1); + EXPECT_EQ(device.terminals(), 3); + EXPECT_EQ(device.sent_count_, 3); + EXPECT_EQ(hub.queued_frames(), 0u); +} + +namespace { +// A device that stops itself (clears its own queue) from inside on_response(). +class ClearOnDataDevice : public ModbusClientDevice { + public: + ClearOnDataDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_response(std::span request_pdu, std::span response_pdu) override { + this->clear_tx_queue_for_device(); + } +}; + +// A device that chains a follow-up send from inside on_sent(). +class ChainOnSentDevice : public ModbusClientDevice { + public: + ChainOnSentDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_sent(std::span request_pdu) override { + if (!this->chained_) { + this->chained_ = true; + const uint8_t follow[] = {0x03, 0x00, 0x09, 0x00, 0x01}; // read holding 0x0009 x1 + this->send_pdu(follow); + } + } + bool chained_{false}; +}; +} // namespace + +// clear_tx_queue_for_address() resolves every dropped frame via its owner's on_not_sent(), so a device +// sharing the address with the clearer (e.g. a modbus_client action alongside an offline controller) +// observes the drop; frames for other addresses are untouched. +TEST(ModbusClientHubQueue, ClearAddressQueueNotifiesEveryOwner) { + NoResponseProbeHub hub; + SentCountingDevice controller_like(&hub, 0x02); + SentCountingDevice bystander_same(&hub, 0x02); + SentCountingDevice bystander_other(&hub, 0x03); + + const uint8_t read_a[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + const uint8_t read_b[] = {0x03, 0x02, 0x00, 0x00, 0x02}; + const uint8_t read_c[] = {0x03, 0x03, 0x00, 0x00, 0x02}; + controller_like.send_pdu(read_a); + bystander_same.send_pdu(read_b); + bystander_other.send_pdu(read_c); + ASSERT_EQ(hub.queued_frames(), 3u); + + controller_like.clear_tx_queue_for_address(); + hub.sweep_for_test(); // the loop's sweep delivers the owed terminals and erases the entries + + ASSERT_EQ(hub.queued_frames(), 1u); // only the other-address frame remains + EXPECT_EQ(hub.queued(0).frame.address(), 0x03); + EXPECT_EQ(controller_like.not_sent_count_, 1); + EXPECT_EQ(bystander_same.not_sent_count_, 1); + EXPECT_EQ(bystander_other.not_sent_count_, 0); + // each owner saw its own request PDU + EXPECT_EQ(bystander_same.last_not_sent_pdu_, std::vector(std::begin(read_b), std::end(read_b))); +} + +// A cleared entry resolves with one on_not_sent() per accepted request it stood for, so the +// books balance for owners counting outstanding requests - all within the one sweep. +TEST(ModbusClientHubQueue, ClearAddressDeliversOneTerminalPerAcceptedRequest) { + NoResponseProbeHub hub; + SentCountingDevice device(&hub, 0x02); + + const uint8_t read[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + device.send_pdu(read); + device.send_pdu(read); // duplicate: absorbed into the queued entry + ASSERT_EQ(hub.queued_frames(), 1u); + ASSERT_EQ(hub.queued(0).pending, 2u); + + hub.clear_tx_queue_for_address(0x02); + hub.sweep_for_test(); + + EXPECT_EQ(device.not_sent_count_, 2); // one terminal per accepted request + EXPECT_EQ(hub.queued_frames(), 0u); + EXPECT_EQ(hub.entries(), 0u); // fully drained and erased +} + +// A duplicate read absorbs into one waiting entry (pending 2 once the first is sent). A clear with +// clear_sent detaches the in-flight frame as a silent shell, but the duplicate - a second accepted +// request that would have re-run - was never transmitted, so it must still get its on_not_sent(). +TEST(ModbusClientHubQueue, ClearSentOnInFlightDuplicateStillNotifiesTheDuplicate) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + device.send_pdu(read_pdu()); // absorbed: one entry, pending 2 + ASSERT_EQ(hub.queued(0).pending, 2u); + hub.force_send_next(); // the frame is sent (WAITING); pending still 2 + ASSERT_TRUE(hub.waiting()); + + hub.clear_tx_queue_for_address(0x02); + hub.sweep_for_test(); + + EXPECT_EQ(device.not_sent_count_, 1); // the un-transmitted duplicate is resolved, not dropped +} + +// A clear does not abandon the in-flight frame: it becomes a WAITING_RETIRED shell that keeps the +// bus and still delivers the in-flight request's usual callback (here on_response) when the reply +// arrives. Only un-run duplicates are turned into on_not_sent(); a lone in-flight frame has none. +TEST(ModbusClientHubQueue, ClearWhileInFlightStillDeliversTheResponse) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.force_send_next(); // sent, now WAITING + ASSERT_TRUE(hub.waiting()); + + hub.clear_tx_queue_for_address(0x02); + hub.sweep_for_test(); + + EXPECT_EQ(device.not_sent_count_, 0); // no un-run duplicate to resolve + ASSERT_TRUE(hub.waiting()); // still waiting for a response, holding the bus + ASSERT_EQ(hub.entries(), 1u); // entry preserved as a cleared shell + EXPECT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED); + + // the in-flight request still gets its usual callback when the response finally arrives + hub.receive_frame_for_test(0x02, OK_RESPONSE); + EXPECT_EQ(device.data_count_, 1); + EXPECT_EQ(device.not_sent_count_, 0); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +namespace { +// Re-sends its frame once from inside on_not_sent - the re-queued frame must survive the sweep. +class ResendOnNotSentDevice : public ModbusClientDevice { + public: + ResendOnNotSentDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_not_sent(std::span request_pdu) override { + this->not_sent_count_++; + if (this->not_sent_count_ == 1) { + const uint8_t again[] = {0x06, 0x00, 0x40, 0x00, 0x01}; // a write: ranked first at selection, not by position + this->send_pdu(again); + } + } + int not_sent_count_{0}; +}; +} // namespace + +// A handler that re-sends to the same address from inside on_not_sent() neither corrupts the sweep +// nor loops it: the fresh entry starts within its cap, so the sweep never touches it. +TEST(ModbusClientHubQueue, ClearAddressReentrantResendSurvives) { + NoResponseProbeHub hub; + ResendOnNotSentDevice device(&hub, 0x02); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + device.send_pdu(read); + ASSERT_EQ(hub.queued_frames(), 1u); + + hub.clear_tx_queue_for_address(0x02); + hub.sweep_for_test(); + + // The original frame resolved via on_not_sent; the re-send from inside that callback remains queued. + EXPECT_EQ(device.not_sent_count_, 1); + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).frame.address(), 0x02); +} + +// The hard case for the sweep: the notified handler re-queues a WRITE to the cleared address. The +// fresh entry must be neither dropped nor re-notified - and the bystander's frame at the other +// address survives untouched, while the write still wins transmit selection. +TEST(ModbusClientHubQueue, ClearAddressReentrantResendNotSwept) { + NoResponseProbeHub hub; + ResendOnNotSentDevice resender(&hub, 0x02); + SentCountingDevice bystander_other(&hub, 0x03); + + const uint8_t read_victim[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + const uint8_t read_other[] = {0x03, 0x00, 0x20, 0x00, 0x01}; + resender.send_pdu(read_victim); + bystander_other.send_pdu(read_other); + ASSERT_EQ(hub.queued_frames(), 2u); + + hub.clear_tx_queue_for_address(0x02); + hub.sweep_for_test(); + + EXPECT_EQ(resender.not_sent_count_, 1); // notified once, never re-notified for the re-send + ASSERT_EQ(hub.queued_frames(), 2u); // the re-queued write AND the other-address read survive + const ModbusDeviceCommand *next = hub.next_ready(); + ASSERT_NE(next, nullptr); + EXPECT_EQ(next->frame.address(), 0x02); // the WRITE class wins selection over the older read + EXPECT_EQ(next->frame.pdu()[0], 0x06); +} + +namespace { +// Re-sends its own frame from EVERY on_not_sent. There is no serve/absorb treadmill: a duplicate at +// the servable cap is refused at the door, and a re-send issued while the entry is retiring queues a +// fresh entry beyond the sweep's captured work_set (served next sweep), never re-absorbing the one draining. +class AlwaysResendDevice : public ModbusClientDevice { + public: + AlwaysResendDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_not_sent(std::span request_pdu) override { + this->not_sent_count_++; + const uint8_t again[] = {0x03, 0x00, 0x50, 0x00, 0x01}; + this->send_pdu(again); + } + int not_sent_count_{0}; +}; + +// From inside on_not_sent, clears ANOTHER address - those victims must still be notified (the per-device +// guard suppresses deliveries only to a device already inside its own on_not_sent()). +class ClearOtherOnNotSentDevice : public ModbusClientDevice { + public: + ClearOtherOnNotSentDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_not_sent(std::span request_pdu) override { + this->not_sent_count_++; + this->parent_->clear_tx_queue_for_address(0x03); + } + int not_sent_count_{0}; +}; +} // namespace + +// pending can never exceed what the entry can serve, so the old serve/absorb treadmill is +// impossible by construction: the surplus request is refused at the door instead of being absorbed +// and resolved later, and a handler that re-sends gets false rather than another lifecycle. +TEST(ModbusClientHubQueue, PendingNeverExceedsTheServableCap) { + NoResponseProbeHub hub; + AlwaysResendDevice device(&hub, 0x02); + + const uint8_t read[] = {0x03, 0x00, 0x50, 0x00, 0x01}; + EXPECT_TRUE(device.send_pdu(read)); + EXPECT_TRUE(device.send_pdu(read)); + EXPECT_FALSE(device.send_pdu(read)); // at the cap: refused + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).pending, 2u); + + hub.sweep_for_test(); // nothing is owed, so the handler never runs + + EXPECT_EQ(device.not_sent_count_, 0); + EXPECT_EQ(hub.queued(0).pending, 2u); +} + +// A full queue refuses at the door: false at the call site, no entry, no callback - so the +// refusal cannot re-enter the hub at all and needs no recursion bound of its own. +TEST(ModbusClientHubQueue, FullQueueRefusesWithoutCallbacks) { + NoResponseProbeHub hub; + SentCountingDevice filler(&hub, 0x05); + SentCountingDevice device(&hub, 0x02); + + // Fill the queue with distinct frames (distinct start addresses keep the dedup from absorbing them). + for (uint16_t i = 0; i < MODBUS_TX_BUFFER_SIZE; i++) { + const uint8_t fill[] = {0x03, static_cast(i >> 8), static_cast(i & 0xFF), 0x00, 0x01}; + filler.send_pdu(fill); + } + ASSERT_EQ(hub.queued_frames(), MODBUS_TX_BUFFER_SIZE); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + EXPECT_FALSE(device.send_pdu(read)); // refused synchronously + hub.sweep_for_test(); + + EXPECT_EQ(device.not_sent_count_, 0); // nothing was accepted, so nothing is owed + EXPECT_EQ(hub.queued_frames(), MODBUS_TX_BUFFER_SIZE); + EXPECT_EQ(hub.entries(), MODBUS_TX_BUFFER_SIZE); // and no refusal bookkeeping was stored +} + +namespace { +// From inside on_not_sent, clears its OWN address - its remaining queued frames resolve silently +// (the guard suppresses self-deliveries), while other owners on the address are still notified. +class ClearOwnAddressOnNotSentDevice : public ModbusClientDevice { + public: + ClearOwnAddressOnNotSentDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_not_sent(std::span request_pdu) override { + this->not_sent_count_++; + this->clear_tx_queue_for_address(); + } + int not_sent_count_{0}; +}; +} // namespace + +// An address clear issued from inside on_not_sent() resolves EVERY dropped request with its own +// terminal at the sweep - including the clearer's (the sweep delivers from a quiescent hub, so the +// old stack-nesting silence no longer applies; use clear_tx_queue_for_device() for silent teardown). +TEST(ModbusClientHubQueue, SelfClearFromNotSentResolvesEveryRequest) { + NoResponseProbeHub hub; + ClearOwnAddressOnNotSentDevice clearer(&hub, 0x02); + SentCountingDevice bystander(&hub, 0x02); + + const uint8_t read_a[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + const uint8_t read_b[] = {0x03, 0x00, 0x20, 0x00, 0x01}; + const uint8_t read_c[] = {0x03, 0x00, 0x30, 0x00, 0x01}; + clearer.send_pdu(read_a); + clearer.send_pdu(read_b); + bystander.send_pdu(read_c); + ASSERT_EQ(hub.queued_frames(), 3u); + + EXPECT_FALSE(clearer.send_pdu(std::span{})); // empty: refused, no callback + clearer.clear_tx_queue_for_address(); // the clear the handler used to make + + hub.sweep_for_test(); + + EXPECT_EQ(clearer.not_sent_count_, 2); // one per cleared request of its own + EXPECT_EQ(bystander.not_sent_count_, 1); // the bystander's cleared frame is notified too + EXPECT_EQ(hub.queued_frames(), 0u); + EXPECT_EQ(hub.entries(), 0u); +} + +// A clear issued from inside on_not_sent() still delivers its victims' notifications in the same sweep: +// the newly-retired entries set sweep_needed_ and the sweep's restart loop drains them before it ends. +TEST(ModbusClientHubQueue, NestedClearFromNotSentStillNotifiesVictims) { + NoResponseProbeHub hub; + ClearOtherOnNotSentDevice clearer(&hub, 0x02); + SentCountingDevice victim(&hub, 0x03); + + const uint8_t read_a[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + const uint8_t read_b[] = {0x03, 0x00, 0x20, 0x00, 0x01}; + clearer.send_pdu(read_a); + victim.send_pdu(read_b); + ASSERT_EQ(hub.queued_frames(), 2u); + + hub.clear_tx_queue_for_address(0x02); // clearer's on_not_sent clears address 0x03 in turn + hub.sweep_for_test(); + + EXPECT_EQ(clearer.not_sent_count_, 1); + EXPECT_EQ(victim.not_sent_count_, 1); // the nested clear's victim resolves in the same sweep + EXPECT_EQ(hub.queued_frames(), 0u); +} + +namespace { +// From on_not_sent (delivered by the sweep), re-sends a frame identical to ANOTHER doomed queued +// frame; the dedup must not absorb into the doomed entry. +class ResendSecondFrameDevice : public ModbusClientDevice { + public: + ResendSecondFrameDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_not_sent(std::span request_pdu) override { + this->not_sent_count_++; + if (this->not_sent_count_ == 1) { + const uint8_t same_as_r2[] = {0x03, 0x00, 0x22, 0x00, 0x01}; + this->send_pdu(same_as_r2); + } + } + int not_sent_count_{0}; +}; +} // namespace + +// A send during a sweep that matches a DELETED (doomed) frame must queue fresh, not absorb into +// the doomed entry - absorption would tie the new request to a frame the sweep is draining. +TEST(ModbusClientHubQueue, SweepDedupSkipsDeletedFrames) { + NoResponseProbeHub hub; + ResendSecondFrameDevice device(&hub, 0x02); + + const uint8_t r1[] = {0x03, 0x00, 0x21, 0x00, 0x01}; + const uint8_t r2[] = {0x03, 0x00, 0x22, 0x00, 0x01}; + device.send_pdu(r1); + device.send_pdu(r2); + ASSERT_EQ(hub.queued_frames(), 2u); + + hub.clear_tx_queue_for_address(0x02); + hub.sweep_for_test(); // r1 and r2 both resolve; r1's handler re-sends a frame identical to r2 + // Without the dedup's dead-state skip the re-send would be absorbed into r2 and drained with it; + // with the skip it queues fresh and survives. + + EXPECT_EQ(device.not_sent_count_, 2); // r1 and r2 both resolved + ASSERT_EQ(hub.queued_frames(), 1u); // the re-send survives + EXPECT_EQ(hub.queued(0).frame.pdu()[2], 0x22); +} + +namespace { +// The worst-case handler: from every on_not_sent() it both re-sends and clears its own address, so +// each delivery manufactures a fresh entry AND a fresh terminal debt. +class ResendAndClearOnNotSentDevice : public ModbusClientDevice { + public: + ResendAndClearOnNotSentDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_not_sent(std::span request_pdu) override { + this->not_sent_count_++; + const uint8_t again[] = {0x03, 0x00, 0x70, 0x00, 0x01}; + this->send_pdu(again); + this->clear_tx_queue_for_address(); + } + int not_sent_count_{0}; +}; +} // namespace + +// Sweep-termination worst case: a handler re-sending AND clearing from every on_not_sent() still +// can't extend the sweep, since it serves only the entries it started with (new debt waits). +TEST(ModbusClientHubQueue, ResendAndClearFromNotSentCannotExtendTheSweep) { + NoResponseProbeHub hub; + ResendAndClearOnNotSentDevice device(&hub, 0x02); + + const uint8_t read[] = {0x03, 0x00, 0x70, 0x00, 0x01}; + device.send_pdu(read); + hub.clear_tx_queue_for_address(0x02); + + hub.sweep_for_test(); + EXPECT_EQ(device.not_sent_count_, 1); // exactly the one terminal that was owed on entry + EXPECT_EQ(hub.entries(), 1u); // the frame the handler queued (and then cleared itself) + + hub.sweep_for_test(); + EXPECT_EQ(device.not_sent_count_, 2); // its terminal comes on the next loop, not this sweep + EXPECT_EQ(hub.entries(), 1u); // and the container is still not growing + + hub.sweep_for_test(); + EXPECT_EQ(device.not_sent_count_, 3); + EXPECT_EQ(hub.entries(), 1u); +} + +// clear_tx_queue_for_device() drops queued frames SILENTLY - no terminal callback (the documented +// exception to the exactly-one-terminal contract; used during teardown/offline handling). +TEST(ModbusClientHubQueue, ClearDeviceQueueDropsSilently) { + NoResponseProbeHub hub; + SentCountingDevice device(&hub, 0x02); + + const uint8_t read_a[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + const uint8_t read_b[] = {0x03, 0x02, 0x00, 0x00, 0x02}; + device.send_pdu(read_a); + device.send_pdu(read_b); + ASSERT_EQ(hub.queued_frames(), 2u); + + device.clear_tx_queue_for_device(); + + EXPECT_EQ(hub.queued_frames(), 0u); + EXPECT_EQ(device.not_sent_count_, 0); // silent drop: no terminal callback +} + +// A send_pdu() from inside on_sent() enqueues behind the waiting frame rather than sending +// immediately or corrupting the waiting transaction. +TEST(ModbusClientHubSent, ReentrantSendFromOnSentQueues) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + ChainOnSentDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.send_next_for_test(); // first frame is sent -> on_sent chains a follow-up + + EXPECT_TRUE(hub.waiting()); // first frame is waiting + ASSERT_EQ(hub.queued_frames(), 1u); // the follow-up queued behind it, not sent + EXPECT_EQ(hub.queued(0).frame.pdu()[2], 0x09); // it is the chained read (start address 0x0009) +} + +// "Stop polling now" from inside on_response() works: the completing command is exposed to the +// clear routines, which detach it, cancelling the pending continuous re-queue. +TEST(ModbusClientHubPriority, ClearDeviceDuringDataCancelsContinuousRequeue) { + NoResponseProbeHub hub; + ClearOnDataDevice device(&hub, 0x02); + + device.read_holding_registers(0x100, 2, {.continuous = true}); + hub.force_send_next(); + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); + + // "Stop polling now" from inside on_response() works: the completing command is detached, so the + // continuous re-queue is cancelled. + EXPECT_EQ(hub.queued_frames(), 0u); +} + +namespace { +// A device that stops polling for its address (clear by address) from inside on_response(). +class ClearAddressOnDataDevice : public ModbusClientDevice { + public: + ClearAddressOnDataDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_response(std::span request_pdu, std::span response_pdu) override { + this->clear_tx_queue_for_address(); + } +}; +} // namespace + +// The address-scoped clear cancels the mid-completion re-queue the same way the device-scoped one does. +TEST(ModbusClientHubPriority, ClearAddressDuringDataCancelsContinuousRequeue) { + NoResponseProbeHub hub; + ClearAddressOnDataDevice device(&hub, 0x02); + + device.read_holding_registers(0x100, 2, {.continuous = true}); + hub.force_send_next(); + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); + + EXPECT_EQ(hub.queued_frames(), 0u); +} + +namespace { +// Overrides only the DEPRECATED on_modbus_* names: the new-name default implementations must forward, so +// external devices written against the old names keep working through the deprecation window. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +class LegacyNameDevice : public ModbusClientDevice { + public: + LegacyNameDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_modbus_not_sent() override { this->legacy_not_sent_++; } + bool on_modbus_no_response() override { + this->legacy_no_response_++; + return false; + } + int legacy_not_sent_{0}; + int legacy_no_response_{0}; +}; +#pragma GCC diagnostic pop +} // namespace + +TEST(ModbusClientHubCompat, LegacyCallbackNamesStillForward) { + NoResponseProbeHub hub; + LegacyNameDevice device(&hub, 0x02); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + device.send_pdu(read); + hub.force_send_next(); + hub.timeout_waiting(); // no reply -> on_no_response -> forwards to on_modbus_no_response + EXPECT_EQ(device.legacy_no_response_, 1); + + // A refused send returns false with no callback, so exercise the forward through an accepted + // request instead: a cleared queue entry delivers on_not_sent(), which forwards to the old name. + EXPECT_FALSE(device.send_pdu(std::span())); // empty PDU: refused at the door + EXPECT_EQ(device.legacy_not_sent_, 0); + const uint8_t queued[] = {0x03, 0x00, 0x11, 0x00, 0x01}; + EXPECT_TRUE(device.send_pdu(queued)); + hub.clear_tx_queue_for_address(0x02); + hub.sweep_for_test(); + EXPECT_EQ(device.legacy_not_sent_, 1); +} + +// The send_pdu() capacity bound: a PDU larger than MAX_PDU_SIZE would build a frame past the RTU +// 256-byte limit, so it is refused up front - false at the call site, no entry, no callback. +TEST(ModbusClientHub, OversizedPduIsRefusedAtTheDoor) { + NoResponseProbeHub hub; + LegacyNameDevice device(&hub, 0x02); + std::vector big(MAX_PDU_SIZE + 1, 0x41); + EXPECT_FALSE(device.send_pdu(big)); + EXPECT_EQ(device.legacy_not_sent_, 0); // refusals are returned, never delivered + EXPECT_TRUE(hub.tx_buffer_empty()); + EXPECT_EQ(hub.entries(), 0u); +} + +// --- ModbusDevice compatibility shim ------------------------------------------------------------ +// External components written against the pre-2026.8 API subclass ModbusDevice and override the +// old callbacks; the shim adapts the span-based hooks back to those signatures. +namespace { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +class LegacyApiDevice : public ModbusDevice { + public: + LegacyApiDevice(ModbusClientHub *hub, uint8_t address) : ModbusDevice(hub, address) {} + void on_modbus_data(const std::vector &data) override { this->last_data_ = data; } + void on_modbus_error(uint8_t function_code, uint8_t exception_code) override { + this->last_error_fc_ = function_code; + this->last_error_code_ = exception_code; + } + std::vector last_data_; + int last_error_fc_{-1}; + int last_error_code_{-1}; +}; +#pragma GCC diagnostic pop +} // namespace + +TEST(ModbusDeviceShim, LegacyCallbacksReceiveTheOldShapes) { + NoResponseProbeHub hub; + LegacyApiDevice device(&hub, 0x02); + + // Read response: on_modbus_data() historically received the payload after the function code and + // the byte-count byte, as an owning vector. + const uint8_t read_req[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + device.send_pdu(read_req); + hub.force_send_next(); + const uint8_t response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, response); + const std::vector expected{0x00, 0x2A, 0x01, 0x00}; + EXPECT_EQ(device.last_data_, expected); + + // Write echo: no byte-count byte, so the payload is everything after the function code. + const uint8_t write_req[] = {0x06, 0x00, 0x10, 0x00, 0x2A}; + device.send_pdu(write_req); + hub.force_send_next(); + hub.receive_frame_for_test(0x02, write_req); // single-write responses echo the request + const std::vector expected_echo{0x00, 0x10, 0x00, 0x2A}; + EXPECT_EQ(device.last_data_, expected_echo); + + // Exception response: on_modbus_error() received the masked function code and the exception code. + device.send_pdu(read_req); + hub.force_send_next(); + const uint8_t error[] = {0x83, 0x02}; + hub.receive_frame_for_test(0x02, error); + EXPECT_EQ(device.last_error_fc_, 0x03); + EXPECT_EQ(device.last_error_code_, 0x02); +} + +// --- typed send helpers -------------------------------------------------------------------------- +// Each helper is a one-line forward onto a merged builder; these pin the function code and wire +// bytes each one queues, so a swapped code or transposed field cannot survive review silently. +TEST(ModbusTypedSendHelpers, HelpersQueueExpectedPdus) { + NoResponseProbeHub hub; + ModbusClientDevice device(&hub, 0x02); + auto check = [&](const std::vector &expected) { + ASSERT_EQ(hub.queued_frames(), 1u); + auto pdu = hub.queued(0).frame.pdu(); + EXPECT_EQ(std::vector(pdu.begin(), pdu.end()), expected); + hub.force_send_next(); + hub.timeout_waiting(); // default on_no_response() declines the retry, dropping the frame + }; + + device.read_holding_registers(0x0102, 3); + check({0x03, 0x01, 0x02, 0x00, 0x03}); + device.read_input_registers(0x0010, 2); + check({0x04, 0x00, 0x10, 0x00, 0x02}); + device.read_coils(0x0020, 10); + check({0x01, 0x00, 0x20, 0x00, 0x0A}); + device.read_discrete_inputs(0x0030, 1); + check({0x02, 0x00, 0x30, 0x00, 0x01}); + device.write_single_register(0x0040, 0xABCD); + check({0x06, 0x00, 0x40, 0xAB, 0xCD}); + device.write_single_coil(0x0041, true); + check({0x05, 0x00, 0x41, 0xFF, 0x00}); + device.write_single_coil(0x0041, false); + check({0x05, 0x00, 0x41, 0x00, 0x00}); + const uint16_t regs[] = {0x000B, 0x0016}; + device.write_multiple_registers(0x0050, regs); + check({0x10, 0x00, 0x50, 0x00, 0x02, 0x04, 0x00, 0x0B, 0x00, 0x16}); + const bool coils[] = {true, false, true}; + device.write_multiple_coils(0x0060, coils); + check({0x0F, 0x00, 0x60, 0x00, 0x03, 0x01, 0x05}); + const uint8_t packed[] = {0x05}; + device.write_multiple_coils(0x0060, PackedBits(packed, 3)); // packed overload, same wire bytes + check({0x0F, 0x00, 0x60, 0x00, 0x03, 0x01, 0x05}); +} + +TEST(ModbusTypedSendHelpers, ReadEntitiesDispatchesByTypeAndRejectsInvalid) { + NoResponseProbeHub hub; + ModbusClientDevice device(&hub, 0x02); + + device.read_entities(EntityType::HOLDING, 0x0001, 1); + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).frame.pdu()[0], 0x03); + hub.force_send_next(); + hub.timeout_waiting(); + + device.read_entities(EntityType::DISCRETE_INPUT, 0x0001, 1); + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).frame.pdu()[0], 0x02); + hub.force_send_next(); + hub.timeout_waiting(); + + device.read_entities(EntityType::CUSTOM, 0x0001, 1); // no read function: logged and not queued + EXPECT_EQ(hub.queued_frames(), 0u); +} + +// A rejected read_entities() returns false like every other refused send. +namespace { +class NotSentCountingDevice : public ModbusClientDevice { + public: + NotSentCountingDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_not_sent(std::span request_pdu) override { this->not_sent_++; } + int not_sent_{0}; +}; +} // namespace + +TEST(ModbusTypedSendHelpers, InvalidReadEntitiesIsRefusedAtTheDoor) { + NoResponseProbeHub hub; + NotSentCountingDevice device(&hub, 0x02); + EXPECT_FALSE(device.read_entities(EntityType::CUSTOM, 0x0001, 1)); + EXPECT_EQ(device.not_sent_, 0); // refused sends report through the return value + EXPECT_EQ(hub.queued_frames(), 0u); +} + +namespace { +// Re-sends its own frame from inside on_response() - matching the command mid-completion. +class ResendOnDataDevice : public ModbusClientDevice { + public: + ResendOnDataDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_response(std::span request_pdu, std::span response_pdu) override { + this->send_pdu(std::vector(request_pdu.begin(), request_pdu.end())); + } + void send_pdu(const std::vector &pdu) { ModbusClientDevice::send_pdu(pdu); } +}; +} // namespace + +// A send from inside on_response() that matches the RECEIVED (completing) entry is absorbed into it, +// never a fresh twin. Here it is a one-shot re-send of a continuous poll, so it also downgrades the +// poll to a one-shot: one entry on the queue afterwards, now non-continuous. +TEST(ModbusClientHubPriority, ResendFromOnResponseAbsorbsIntoCompletingCommand) { + NoResponseProbeHub hub; + ResendOnDataDevice device(&hub, 0x02); + + device.read_holding_registers(0x100, 2, {.continuous = true}); + hub.force_send_next(); + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); // handler re-sends the identical frame mid-completion + + ASSERT_EQ(hub.queued_frames(), 1u); // absorbed into the same entry, not a fresh twin + EXPECT_FALSE(hub.queued(0).continuous); // the one-shot re-send downgraded the poll +} + +// An exception-flagged function code is never silently re-sendable, even though the read check +// masks the exception bit: its duplicate takes the drop path like any other non-read. +TEST(ModbusClientHubPriority, ExceptionFlaggedDuplicateDroppedNotPromoted) { + NoResponseProbeHub hub; + SentCountingDevice device(&hub, 0x02); + + const uint8_t weird[] = {0x83, 0x01, 0x00, 0x00, 0x02}; // read-shaped but exception-flagged + EXPECT_TRUE(device.send_pdu(weird)); + EXPECT_FALSE(device.send_pdu(weird)); // non-requeueable: cap of one, so the duplicate is refused + hub.sweep_for_test(); + + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_EQ(hub.queued(0).pending, 1u); + EXPECT_EQ(device.not_sent_count_, 0); + + // The write-shaped twin (0x86 masks to WRITE_SINGLE_REGISTER) must not take WRITE-class + // ordering either: exception-flagged codes are excluded from the mutates classification. + const uint8_t weird_write[] = {0x86, 0x00, 0x10, 0xBE, 0xEF}; + device.send_pdu(weird_write); + ASSERT_EQ(hub.queued_frames(), 2u); + EXPECT_EQ(hub.queued(1).priority(), CommandPriority::READ); // not WRITE + const ModbusDeviceCommand *next = hub.next_ready(); + ASSERT_NE(next, nullptr); + EXPECT_EQ(next->frame.pdu()[0], 0x83); // FIFO by age: it did not jump the older entry +} + +namespace { +// From inside the sweep's on_not_sent, re-sends the frame that is currently WAITING. +class ResendInFlightOnNotSentDevice : public ModbusClientDevice { + public: + ResendInFlightOnNotSentDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + void on_not_sent(std::span request_pdu) override { + this->not_sent_count_++; + if (this->not_sent_count_ == 1) { + const uint8_t same_as_waiting[] = {0x03, 0x01, 0x00, 0x00, 0x02}; // == READ_PDU + this->send_pdu(same_as_waiting); + } + } + int not_sent_count_{0}; +}; +} // namespace + +// A clear turns the waiting entry into a WAITING_RETIRED shell. The shell keeps its device (so the +// in-flight request still gets its callback), but the dedup skips it, so a sweep handler re-sending +// that frame queues fresh instead of being absorbed into the cleared shell and drained as on_not_sent. +TEST(ModbusClientHubQueue, SweepResendAfterClearQueuesFreshNotAbsorbedIntoShell) { + NoResponseProbeHub hub; + ResendInFlightOnNotSentDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.force_send_next(); // READ_PDU now waiting + const uint8_t queued_read[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + device.send_pdu(queued_read); // a queued frame for the sweep to notify + ASSERT_EQ(hub.queued_frames(), 1u); + + hub.clear_tx_queue_for_address(0x02); + hub.sweep_for_test(); + + EXPECT_EQ(device.not_sent_count_, 1); // only the cleared queued frame, not the re-send + ASSERT_EQ(hub.queued_frames(), 1u); // the handler's re-send queued fresh... + EXPECT_EQ(hub.queued(0).pending, 1u); // ...not absorbed into the cleared shell + EXPECT_TRUE(std::equal(hub.queued(0).frame.pdu().begin(), hub.queued(0).frame.pdu().end(), READ_PDU)); + EXPECT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED); // in-flight one still awaiting a reply +} + +namespace { +// Gives up after a timeout by clearing its address from inside on_no_response() - the natural +// "device is dead, drop my traffic" pattern, and the reentrant case the address clear must handle. +class ClearAddressOnNoResponseDevice : public ModbusClientDevice { + public: + ClearAddressOnNoResponseDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {} + bool on_no_response(std::span request_pdu) override { + this->no_response_count_++; + this->clear_tx_queue_for_address(); + return false; // gave up + } + void on_not_sent(std::span request_pdu) override { this->not_sent_count_++; } + int terminals() const { return this->no_response_count_ + this->not_sent_count_; } + int no_response_count_{0}; + int not_sent_count_{0}; +}; + +} // namespace + +// A clear issued from inside on_no_response() must not cause the request to be resolved twice: +// that callback already was its terminal, so the entry it hijacks owes nothing more. +TEST(ModbusClientHubNoResponse, SelfClearFromNoResponseDoesNotDoubleResolve) { + NoResponseProbeHub hub; + ClearAddressOnNoResponseDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + hub.timeout_waiting(); + + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_EQ(device.terminals(), 1); // exactly one terminal for the one accepted request + EXPECT_EQ(hub.entries(), 0u); +} + +// The same entry standing for two accepted requests: the timeout resolves one, and the clear that +// cancels the re-run must resolve exactly the other. +TEST(ModbusClientHubNoResponse, SelfClearFromNoResponseResolvesTheAbsorbedRequestOnce) { + NoResponseProbeHub hub; + ClearAddressOnNoResponseDevice device(&hub, 0x02); + + EXPECT_TRUE(device.send_pdu(read_pdu())); + EXPECT_TRUE(device.send_pdu(read_pdu())); // absorbed: one entry, two requests + hub.force_send_next(); + hub.timeout_waiting(); + + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_EQ(device.terminals(), 2); // one per accepted request, no more + EXPECT_EQ(hub.entries(), 0u); +} + +// A cleared in-flight frame must release the bus by both exits and still deliver the in-flight +// request's usual callback (on_response here, on_no_response on timeout); no on_not_sent, no duplicate. +TEST(ModbusClientHubQueue, ClearedShellReleasesTheBusOnLateResponse) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + hub.clear_tx_queue_for_address(0x02); + ASSERT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED); + + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); // the late reply for the cleared frame + + EXPECT_FALSE(hub.waiting()); // the bus is free again + EXPECT_EQ(hub.entries(), 0u); // the shell is gone + EXPECT_EQ(device.data_count_, 1); // the in-flight request still got its response callback + EXPECT_EQ(device.not_sent_count_, 0); // no un-run duplicate +} + +TEST(ModbusClientHubQueue, ClearedShellReleasesTheBusOnTimeout) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + hub.clear_tx_queue_for_address(0x02); + ASSERT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED); + + hub.timeout_waiting(); // no reply ever arrives; the watchdog releases the shell + + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); + EXPECT_EQ(device.no_response_count_, 1); // the in-flight request got its on_no_response + EXPECT_EQ(device.not_sent_count_, 0); // no un-run duplicate +} + +// Clearing an interrupted (not-yet-notified) frame keeps its distrust: it becomes an +// INTERRUPTED_RETIRED shell that still ends in on_no_response at the timeout - never delivering a +// late response as on_response. No duplicate here, so no on_not_sent. +TEST(ModbusClientHubQueue, ClearInterruptedFrameGetsNoResponseAtTimeout) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); // declines the retry (retries_ == 0) + + device.send_pdu(read_pdu()); + hub.force_send_next(); + const uint8_t stray_pdu[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x07, stray_pdu); // wrong address: interrupts the transaction + hub.sweep_for_test(); + ASSERT_EQ(hub.waiting_command().state, FrameState::INTERRUPTED); + + hub.clear_tx_queue_for_address(0x02); + ASSERT_EQ(hub.waiting_command().state, FrameState::INTERRUPTED_RETIRED); + + // A late MATCHING response is ignored (distrust survives the clear), not delivered as on_response. + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); + EXPECT_EQ(device.data_count_, 0); + ASSERT_TRUE(hub.waiting()); // still held; the ignored response did not free the wire + + hub.timeout_waiting(); + + EXPECT_EQ(device.no_response_count_, 1); // the interrupted request's usual terminal, at the timeout + EXPECT_EQ(device.not_sent_count_, 0); // no un-run duplicate + EXPECT_EQ(hub.queued_frames(), 0u); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// The other order: clear a WAITING frame, THEN an unexpected frame arrives. The distrust must still +// take hold - the cleared shell becomes INTERRUPTED_RETIRED and a later matching frame is ignored. +TEST(ModbusClientHubQueue, InterruptAfterClearStillDistrusts) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + hub.clear_tx_queue_for_address(0x02); + ASSERT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED); + + const uint8_t stray_pdu[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x07, stray_pdu); // unexpected frame interrupts the cleared shell + ASSERT_EQ(hub.waiting_command().state, FrameState::INTERRUPTED_RETIRED); + + const uint8_t ok_response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + hub.receive_frame_for_test(0x02, ok_response); // now-distrusted late response is ignored + EXPECT_EQ(device.data_count_, 0); + + hub.timeout_waiting(); + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// A cleared waiting duplicate (pending 2) that times out: the duplicate drains to on_not_sent and +// the in-flight request gets on_no_response, with nothing re-transmitted (sweep runs before timeout). +TEST(ModbusClientHubQueue, ClearedInFlightDuplicateTimesOutWithoutRerunning) { + NoResponseProbeHub hub; + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + device.send_pdu(read_pdu()); // absorbed: one entry, pending 2 + ASSERT_EQ(hub.queued(0).pending, 2u); + hub.force_send_next(); // sent, pending still 2 + hub.clear_tx_queue_for_address(0x02); + + hub.timeout_waiting(); + + EXPECT_EQ(device.not_sent_count_, 1); // the un-run duplicate + EXPECT_EQ(device.no_response_count_, 1); // the in-flight request's usual terminal + EXPECT_EQ(hub.queued_frames(), 0u); // nothing re-transmitted + EXPECT_EQ(hub.entries(), 0u); // fully drained and erased + EXPECT_FALSE(hub.waiting()); +} + +// An absorbed extra request also gets its run after an error response - the re-request was +// explicit, so it runs once more whether this attempt succeeded or not. +TEST(ModbusClientHubCallbackCount, AbsorbedRequestRunsAfterErrorResponse) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + DataCountingDevice device(&hub, 0x02); + + device.send_pdu(read_pdu()); + hub.force_send_next(); + device.send_pdu(read_pdu()); // waiting duplicate: absorbed + const uint8_t exception_response[] = {0x83, 0x02}; + hub.receive_frame_for_test(0x02, exception_response); // error terminal for request 1 + + EXPECT_EQ(device.error_count_, 1); + ASSERT_EQ(hub.queued_frames(), 1u); // request 2's run still queued + EXPECT_EQ(hub.queued(0).pending, 1u); +} + +// Read-modify-write function codes mutate registers, so they rank as WRITE for transmit ordering. +TEST(ModbusClientHubPriority, ReadModifyWritesRankAsWrites) { + NoResponseProbeHub hub; + SentCountingDevice device(&hub, 0x02); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01}; + const uint8_t mask_write[] = {0x16, 0x00, 0x10, 0x00, 0xFF, 0x00, 0x01}; + device.send_pdu(read); + device.send_pdu(mask_write); + + ASSERT_EQ(hub.queued_frames(), 2u); + const ModbusDeviceCommand *next = hub.next_ready(); + ASSERT_NE(next, nullptr); + EXPECT_EQ(next->priority(), CommandPriority::WRITE); // 0x16 wins selection over the queued read + EXPECT_EQ(next->frame.pdu()[0], 0x16); +} +} // namespace esphome::modbus::testing diff --git a/tests/components/modbus/modbus_helpers_test.cpp b/tests/components/modbus/modbus_helpers_test.cpp index ecdca4df6d..49de4f9d14 100644 --- a/tests/components/modbus/modbus_helpers_test.cpp +++ b/tests/components/modbus/modbus_helpers_test.cpp @@ -1,10 +1,12 @@ #include +#include + #include "esphome/components/modbus/modbus_helpers.h" namespace esphome::modbus::helpers { -using FC = ModbusFunctionCode; +using FC = FunctionCode; // --- server_frame_length --------------------------------------------------- // Frame layout: address(1) + function(1) + ... + CRC(2). Fixtures borrowed from @@ -83,6 +85,13 @@ TEST(ModbusClientFrameLength, WriteMultipleByteCountCapped) { EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9 + MAX_NUM_OF_REGISTERS_TO_WRITE * 2); } +TEST(ModbusClientFrameLength, ReadWriteMultipleByteCountCappedAtSpecLimit) { + // FC 0x17's write byte count caps at the spec 6.17 limit of 121 registers (242 bytes), deliberately + // tighter than FC 0x10's 123, so a corrupt byte count cannot make the parser wait past the real frame. + const uint8_t pdu[] = {0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xFF}; // claims 255 bytes + EXPECT_EQ(client_pdu_length(pdu, sizeof(pdu)), 10 + MAX_NUM_OF_REGISTERS_TO_WRITE_RW * 2); +} + TEST(ModbusClientFrameLength, WriteMultipleMissingByteCount) { const uint8_t frame[] = {0x01, 0x10, 0x00, 0x00, 0x00, 0x02}; EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9); @@ -97,6 +106,124 @@ TEST(ModbusClientFrameLength, MiscFixedAndUnknown) { EXPECT_EQ(client_frame_length(unknown, sizeof(unknown)), MIN_FRAME_SIZE); } +// --- file-record length cap -------------------------------------------------- +// FC 0x14/0x15 are parsed only to keep the frame parser in sync; the byte count caps at 251 +// (MAX_PDU_SIZE - 2), reproducing the released frame-relative bound of MAX_FRAME_SIZE - 5. + +TEST(ModbusFileRecordCap, PduLengthCapsByteCountAt251) { + const uint8_t pdu[] = {static_cast(FC::READ_FILE_RECORD), 0xFF}; // claims 255 bytes + EXPECT_EQ(server_pdu_length(pdu, sizeof(pdu)), 2 + (MAX_PDU_SIZE - 2)); + EXPECT_EQ(client_pdu_length(pdu, sizeof(pdu)), 2 + (MAX_PDU_SIZE - 2)); + // Frame wrappers: address(1) + PDU + CRC(2) stays within the RTU 256-byte frame limit. + const uint8_t frame[] = {0x01, static_cast(FC::WRITE_FILE_RECORD), 0xFF}; + EXPECT_EQ(server_frame_length(frame, sizeof(frame)), MAX_FRAME_SIZE); + EXPECT_EQ(client_frame_length(frame, sizeof(frame)), MAX_FRAME_SIZE); +} + +TEST(ModbusFileRecordCap, StandardChecksAcceptUpTo251) { + // A full-length PDU at the cap: function(1) + byte count(1) + 251 data bytes = MAX_PDU_SIZE. + std::vector at_cap(MAX_PDU_SIZE, 0x00); + at_cap[0] = static_cast(FC::READ_FILE_RECORD); + at_cap[1] = MAX_PDU_SIZE - 2; + EXPECT_TRUE(is_server_pdu_standard(at_cap.data(), at_cap.size())); + EXPECT_TRUE(is_client_pdu_standard(at_cap.data(), at_cap.size())); + // Byte count 252 in the same 253-byte buffer: the parsed length still matches (capped), so this + // exercises the byte-count bound itself rather than the length identity. + at_cap[1] = MAX_PDU_SIZE - 1; + EXPECT_FALSE(is_server_pdu_standard(at_cap.data(), at_cap.size())); + EXPECT_FALSE(is_client_pdu_standard(at_cap.data(), at_cap.size())); +} + +// --- is_client_pdu_standard / is_server_pdu_standard ------------------------- +// The gatekeepers for the typed client dispatch: a PDU must be exactly its function code's standard +// shape, with byte count, quantity, and address range all consistent. + +TEST(ModbusPduStandard, ClientReadAndWriteConformant) { + const uint8_t read_regs[] = {0x03, 0x01, 0x00, 0x00, 0x02}; + EXPECT_TRUE(is_client_pdu_standard(read_regs, sizeof(read_regs))); + const uint8_t write_regs[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01, 0x00, 0x02}; + EXPECT_TRUE(is_client_pdu_standard(write_regs, sizeof(write_regs))); + // 10 coils pack into 2 data bytes - the coil formula, not the register one. + const uint8_t write_coils[] = {0x0F, 0x00, 0x30, 0x00, 0x0A, 0x02, 0xFF, 0x03}; + EXPECT_TRUE(is_client_pdu_standard(write_coils, sizeof(write_coils))); +} + +TEST(ModbusPduStandard, ClientRejectsNonConformant) { + // Truncated: header claims 4 data bytes, only 2 present. + const uint8_t truncated[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01}; + EXPECT_FALSE(is_client_pdu_standard(truncated, sizeof(truncated))); + // Byte count disagrees with quantity (2 registers need 4 bytes, header says 2). + const uint8_t inconsistent[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x02, 0x00, 0x01}; + EXPECT_FALSE(is_client_pdu_standard(inconsistent, sizeof(inconsistent))); + // Coil write using the register byte-count formula (10 coils with 20 data bytes). + const uint8_t coil_as_regs[] = {0x0F, 0x00, 0x30, 0x00, 0x0A, 0x14, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + EXPECT_FALSE(is_client_pdu_standard(coil_as_regs, sizeof(coil_as_regs))); + // Quantity zero and quantity beyond the per-function-code maximum. + const uint8_t zero_qty[] = {0x03, 0x01, 0x00, 0x00, 0x00}; + EXPECT_FALSE(is_client_pdu_standard(zero_qty, sizeof(zero_qty))); + const uint8_t too_many[] = {0x03, 0x01, 0x00, 0x00, 0x7E}; // 126 > 125 + EXPECT_FALSE(is_client_pdu_standard(too_many, sizeof(too_many))); + // Address range overflow: 0xFFFF + 2 registers exceeds the 16-bit register space. + const uint8_t wraps[] = {0x03, 0xFF, 0xFF, 0x00, 0x02}; + EXPECT_FALSE(is_client_pdu_standard(wraps, sizeof(wraps))); +} + +TEST(ModbusPduStandard, ServerReadResponses) { + const uint8_t ok[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00}; + EXPECT_TRUE(is_server_pdu_standard(ok, sizeof(ok))); + // Byte-count header disagrees with the actual length. + const uint8_t lying[] = {0x03, 0x06, 0x00, 0x2A, 0x01, 0x00}; + EXPECT_FALSE(is_server_pdu_standard(lying, sizeof(lying))); + // An empty PDU (the on_error path) is not a standard response. + EXPECT_FALSE(is_server_pdu_standard(ok, 0)); +} + +TEST(ModbusPduStandard, ServerResponsesRejectDegenerateShapes) { + // A read response always carries data: byte count zero is non-conformant. + const uint8_t zero_bc[] = {0x03, 0x00}; + EXPECT_FALSE(is_server_pdu_standard(zero_bc, sizeof(zero_bc))); + // Registers are 2 bytes each: an odd byte count would silently truncate a register. + const uint8_t odd_bc[] = {0x03, 0x03, 0x00, 0x01, 0x02}; + EXPECT_FALSE(is_server_pdu_standard(odd_bc, sizeof(odd_bc))); + // Bit reads have no parity requirement: one packed byte is a fine coil response. + const uint8_t coil_one_byte[] = {0x01, 0x01, 0x05}; + EXPECT_TRUE(is_server_pdu_standard(coil_one_byte, sizeof(coil_one_byte))); + // A write-multiple echo claiming 65535 registers written is bounded like the request side. + const uint8_t wild_echo[] = {0x10, 0x00, 0x00, 0xFF, 0xFF}; + EXPECT_FALSE(is_server_pdu_standard(wild_echo, sizeof(wild_echo))); + const uint8_t ok_echo[] = {0x10, 0x00, 0x00, 0x00, 0x02}; + EXPECT_TRUE(is_server_pdu_standard(ok_echo, sizeof(ok_echo))); +} + +TEST(ModbusPduStandard, SingleCoilValueMustBeCanonical) { + // FC 0x05's value field allows exactly 0xFF00 (ON) and 0x0000 (OFF); anything else is non-standard. + const uint8_t on[] = {0x05, 0x00, 0x10, 0xFF, 0x00}; + const uint8_t off[] = {0x05, 0x00, 0x10, 0x00, 0x00}; + const uint8_t junk[] = {0x05, 0x00, 0x10, 0x12, 0x34}; + EXPECT_TRUE(is_client_pdu_standard(on, sizeof(on))); + EXPECT_TRUE(is_client_pdu_standard(off, sizeof(off))); + EXPECT_FALSE(is_client_pdu_standard(junk, sizeof(junk))); + EXPECT_TRUE(is_server_pdu_standard(on, sizeof(on))); // the response echoes the request + EXPECT_FALSE(is_server_pdu_standard(junk, sizeof(junk))); +} + +TEST(ModbusPduStandard, NonStandardFunctionCodesAcceptedOnLengthAlone) { + // Custom, unimplemented, and exception function codes have no standard shape to check: they are + // accepted whenever the parsed length matches, so a dispatcher can still route them by function + // code instead of having them rejected outright. This is the documented contract - see the header. + const uint8_t custom[] = {0x42}; // user-defined space; 1 byte matches the MIN_PDU_SIZE fallback + EXPECT_TRUE(is_client_pdu_standard(custom, sizeof(custom))); + EXPECT_TRUE(is_server_pdu_standard(custom, sizeof(custom))); + const uint8_t unimplemented[] = {0x07}; // READ_EXCEPTION_STATUS + EXPECT_TRUE(is_server_pdu_standard(unimplemented, sizeof(unimplemented))); + const uint8_t exception[] = {0x83, 0x02}; // exception response; length pinned to 2 bytes + EXPECT_TRUE(is_server_pdu_standard(exception, sizeof(exception))); + // The length identity still gates: extra bytes beyond the parsed fallback are non-conformant. + const uint8_t custom_long[] = {0x42, 0x01}; + EXPECT_FALSE(is_client_pdu_standard(custom_long, sizeof(custom_long))); +} + // --- create_client_pdu ----------------------------------------------------- // PDU = function code + data (no address, no CRC). @@ -179,19 +306,44 @@ TEST(ModbusCreateClientPdu, WriteMultipleOverEntityLimitReturnsEmpty) { EXPECT_TRUE(pdu.empty()); } +// The generic write path requires the data length to agree exactly with the entity count +// (registers: 2 bytes each; coils: 8 packed per byte) - the same rule the response dispatch +// enforces via is_client_pdu_standard(), so a frame built here always passes that gate. +TEST(ModbusCreateClientPdu, WriteMultipleRejectsMismatchedDataLength) { + const uint8_t values[] = {0x00, 0x0B, 0x00, 0x16}; + // 2 registers need exactly 4 data bytes. + EXPECT_TRUE(create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 2, values, 3).empty()); + EXPECT_FALSE(create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 2, values, 4).empty()); + // 10 coils pack into exactly 2 data bytes - the coil formula, not the register one. + EXPECT_FALSE(create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 10, values, 2).empty()); + EXPECT_TRUE(create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 10, values, 4).empty()); +} + +TEST(ModbusCreateClientPdu, WriteCoilsUseTheCoilLimitNotTheRegisterLimit) { + // 200 coils: above the 123-register write limit but well within the 1968-coil limit; 25 data bytes. + std::vector values(25, 0xAA); + auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 200, values.data(), values.size()); + ASSERT_FALSE(pdu.empty()); + EXPECT_EQ(pdu[5], 25); // byte count uses the coil formula + EXPECT_TRUE(is_client_pdu_standard(pdu.data(), pdu.size())); // builder output passes the validator + // Builder and validator agree at the top of the range too: 1969 coils rejected. + std::vector big((1969 + 7) / 8, 0x00); + EXPECT_TRUE(create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 1969, big.data(), big.size()).empty()); +} + TEST(ModbusHelpersTest, PayloadToNumberRejectsOffsetAtEndOfBuffer) { const std::vector data{0x12, 0x34}; - EXPECT_EQ(payload_to_number(data, SensorValueType::U_WORD, 2, 0xFFFFFFFF), 0); + EXPECT_FALSE(payload_to_number(std::span(data), SensorValueType::U_WORD, 2, 0xFFFFFFFF).has_value()); } TEST(ModbusHelpersTest, PayloadToNumberRejectsTruncatedMultiRegisterValue) { const std::vector data{0x12, 0x34, 0x56}; - EXPECT_EQ(payload_to_number(data, SensorValueType::U_DWORD, 0, 0xFFFFFFFF), 0); + EXPECT_FALSE(payload_to_number(std::span(data), SensorValueType::U_DWORD, 0, 0xFFFFFFFF).has_value()); } TEST(ModbusHelpersTest, PayloadToNumberDecodesValidWord) { const std::vector data{0x12, 0x34}; - EXPECT_EQ(payload_to_number(data, SensorValueType::U_WORD, 0, 0xFFFFFFFF), 0x1234); + EXPECT_EQ(payload_to_number(std::span(data), SensorValueType::U_WORD, 0, 0xFFFFFFFF), 0x1234); } // --- registers_to_number --------------------------------------------------- @@ -218,16 +370,194 @@ TEST(ModbusHelpersTest, RegistersToNumberMatchesPayloadToNumber) { const uint16_t registers[] = {0x8001, 0x0002}; const std::vector bytes{0x80, 0x01, 0x00, 0x02}; for (auto value_type : {SensorValueType::S_DWORD, SensorValueType::U_DWORD, SensorValueType::S_DWORD_R}) { - EXPECT_EQ(registers_to_number(registers, 2, value_type), payload_to_number(bytes, value_type, 0, 0xFFFFFFFF)) + EXPECT_EQ(registers_to_number(registers, 2, value_type), + payload_to_number(std::span(bytes), value_type, 0, 0xFFFFFFFF)) << "value_type=" << static_cast(value_type); } } TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) { const uint16_t registers[] = {0x1234}; - bool error = false; - EXPECT_EQ(registers_to_number(registers, 1, SensorValueType::U_DWORD, &error), 0); - EXPECT_TRUE(error); + EXPECT_FALSE(registers_to_number(registers, 1, SensorValueType::U_DWORD).has_value()); +} + +// --- typed builders ---------------------------------------------------------- + +TEST(ModbusTypedBuilders, ReadPduWireBytes) { + auto pdu = create_read_pdu(FC::READ_HOLDING_REGISTERS, 0x0102, 3); + const std::vector expected{0x03, 0x01, 0x02, 0x00, 0x03}; + EXPECT_EQ(std::vector(pdu.begin(), pdu.end()), expected); + EXPECT_TRUE(is_client_pdu_standard(pdu.data(), pdu.size())); + // Reads that run past the 16-bit address space are refused. + EXPECT_TRUE(create_read_pdu(FC::READ_HOLDING_REGISTERS, 0xFFFF, 2).empty()); +} + +TEST(ModbusTypedBuilders, WriteSinglePduWireBytes) { + auto reg = create_write_single_register_pdu(0x0010, 0xABCD); + const std::vector expected_reg{0x06, 0x00, 0x10, 0xAB, 0xCD}; + EXPECT_EQ(std::vector(reg.begin(), reg.end()), expected_reg); + EXPECT_TRUE(is_client_pdu_standard(reg.data(), reg.size())); + auto coil_on = create_write_single_coil_pdu(0x0011, true); + auto coil_off = create_write_single_coil_pdu(0x0011, false); + const std::vector expected_on{0x05, 0x00, 0x11, 0xFF, 0x00}; + const std::vector expected_off{0x05, 0x00, 0x11, 0x00, 0x00}; + EXPECT_EQ(std::vector(coil_on.begin(), coil_on.end()), expected_on); + EXPECT_EQ(std::vector(coil_off.begin(), coil_off.end()), expected_off); + EXPECT_TRUE(is_client_pdu_standard(coil_on.data(), coil_on.size())); + EXPECT_TRUE(is_client_pdu_standard(coil_off.data(), coil_off.size())); +} + +TEST(ModbusTypedBuilders, WriteRegistersPduWireBytes) { + const uint16_t values[] = {0x000B, 0x0016}; + auto pdu = create_write_registers_pdu(0x0000, values); + const std::vector expected{0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x0B, 0x00, 0x16}; + EXPECT_EQ(std::vector(pdu.begin(), pdu.end()), expected); + EXPECT_TRUE(is_client_pdu_standard(pdu.data(), pdu.size())); + // Writes that run past the 16-bit address space are refused. + EXPECT_TRUE(create_write_registers_pdu(0xFFFF, values).empty()); +} + +TEST(ModbusTypedBuilders, WriteRegistersPduRejectsOverLimit) { + std::vector values(MAX_NUM_OF_REGISTERS_TO_WRITE + 1, 0xAAAA); + EXPECT_TRUE(create_write_registers_pdu(0x0000, values).empty()); + values.pop_back(); + EXPECT_FALSE(create_write_registers_pdu(0x0000, values).empty()); +} + +TEST(ModbusTypedBuilders, FloatToPayloadAppendsToExistingContent) { + // The container overload appends - the semantic every migrated caller relies on when a lambda + // has already put words into the buffer. + std::vector data{0x1234}; + float_to_payload(data, 1.0f, SensorValueType::U_WORD); + ASSERT_EQ(data.size(), 2u); + EXPECT_EQ(data[0], 0x1234); + EXPECT_EQ(data[1], 0x0001); +} + +TEST(ModbusCreateClientPdu, ExceptionFlaggedWriteCodesRejected) { + // is_function_code_write() masks the exception bit; the builder must not. + const uint8_t values[] = {0x00, 0x0B, 0x00, 0x16}; + EXPECT_TRUE(create_client_pdu(FunctionCode(0x90), 0x0000, 2, values, 4).empty()); + EXPECT_TRUE(create_client_pdu(FunctionCode(0x85), 0x0000, 1, values, 2).empty()); +} + +TEST(ModbusTypedBuilders, BoolSpanCoilBuilderRejectsOverLimit) { + // This early guard is what keeps the 246-byte packing buffer from overflowing - the shared core's + // identical check runs after packing, so it cannot protect it. + auto big = std::make_unique(MAX_NUM_OF_COILS_TO_WRITE + 1); + EXPECT_TRUE(create_write_coils_pdu(0, std::span(big.get(), MAX_NUM_OF_COILS_TO_WRITE + 1)).empty()); +} + +TEST(ModbusCreateClientPdu, GenericCoilWriteMasksTrailingPadBits) { + // 10 coils with junk in the pad bits of the last data byte: the generic path masks them like the + // typed builder, so both produce identical wire bytes. + const uint8_t values[] = {0xFF, 0xFF}; + auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_COILS, 0x0000, 10, values, 2); + ASSERT_FALSE(pdu.empty()); + EXPECT_EQ(pdu[pdu.size() - 1], 0x03); // bits 8-9 kept, pad bits 10-15 zeroed +} + +TEST(ModbusCreateClientPdu, SingleCoilValueValidated) { + const uint8_t on[] = {0xFF, 0x00}; + const uint8_t junk[] = {0x01, 0x00}; + EXPECT_FALSE(create_client_pdu(FC::WRITE_SINGLE_COIL, 0x0003, 1, on, 2).empty()); + EXPECT_TRUE(create_client_pdu(FC::WRITE_SINGLE_COIL, 0x0003, 1, junk, 2).empty()); +} + +// --- create_write_coils_pdu (packed) --------------------------------------- + +TEST(ModbusWriteCoilsPacked, MatchesBoolBuilder) { + const bool coils[] = {true, false, true, true, false, false, true, false, true, true}; + uint8_t packed[] = {0b01001101, 0b00000011}; + auto from_bools = create_write_coils_pdu(0x13, coils); + auto from_packed = create_write_coils_pdu(0x13, PackedBits(packed, 10)); + ASSERT_EQ(from_packed.size(), from_bools.size()); + EXPECT_EQ(0, memcmp(from_packed.data(), from_bools.data(), from_bools.size())); +} + +TEST(ModbusWriteCoilsPacked, MasksUnusedTrailingBits) { + uint8_t packed[] = {0xFF}; + auto pdu = create_write_coils_pdu(0, PackedBits(packed, 3)); + ASSERT_EQ(pdu.size(), 7u); + EXPECT_EQ(pdu[6], 0x07); +} + +TEST(ModbusWriteCoilsPacked, RejectsShortBufferAndZeroCount) { + uint8_t packed[] = {0xFF}; + EXPECT_TRUE(create_write_coils_pdu(0, PackedBits(packed, 9)).empty()); // needs 2 bytes + EXPECT_TRUE(create_write_coils_pdu(0, PackedBits(packed, 0)).empty()); +} + +TEST(ModbusHelpersTest, PackedBitsReadsLsbFirst) { + const uint8_t packed[] = {0x0D, 0x03}; // bits 0,2,3 and 8,9 + PackedBits bits(packed, 11); + EXPECT_EQ(bits.size(), 11u); + EXPECT_TRUE(bits[0]); + EXPECT_FALSE(bits[1]); + EXPECT_TRUE(bits[2]); + EXPECT_TRUE(bits[3]); + EXPECT_FALSE(bits[7]); + EXPECT_TRUE(bits[8]); + EXPECT_TRUE(bits[9]); + EXPECT_FALSE(bits[10]); + EXPECT_EQ(bits.bytes().size(), 2u); +} + +TEST(ModbusHelpersTest, MutablePackedBitsSetsAndClears) { + uint8_t packed[2] = {0x00, 0xFF}; + MutablePackedBits bits(packed, 16); + bits.set(0, true); + bits.set(3, true); + bits.set(9, false); + EXPECT_EQ(packed[0], 0x09); // bits 0 and 3 + EXPECT_EQ(packed[1], 0xFD); // bit 9 (bit 1 of byte 1) cleared +} + +TEST(ModbusHelpersTest, MutablePackedBitsRoundTripAndConversion) { + const bool original[] = {true, true, false, true, false, false, false, false, true, false, true}; + constexpr uint16_t count = sizeof(original); + uint8_t packed[(count + 7) / 8] = {}; + MutablePackedBits out(packed, count); + for (uint16_t i = 0; i != count; i++) + out.set(i, original[i]); + PackedBits view = out; // implicit conversion to the read-only view + ASSERT_EQ(view.size(), count); + for (uint16_t i = 0; i != count; i++) + EXPECT_EQ(view[i], original[i]) << "bit " << i; +} + +TEST(ModbusHelpersTest, PackedBitsViewContractsEnforced) { + uint8_t buf[8] = {}; + PackedBits view(buf, 10); // 10 bits -> 2 bytes, over an 8-byte buffer + EXPECT_EQ(view.bytes().size(), 2u); + + MutablePackedBits bits(std::span(buf, 2), 10); + bits.set(9, true); // in range: lands in byte 1 + bits.set(10, true); // out of range: dropped + bits.set(300, true); // far out of range: dropped, no write past the span + + MutablePackedBits short_bits(std::span(buf, 1), 10); // contract-violating: 10 bits over 1 byte + short_bits.set(9, false); // within count_ but past the span: dropped (would clear bit 9 set above) + EXPECT_EQ(buf[1], 0x02); + for (size_t i = 2; i < sizeof(buf); i++) + EXPECT_EQ(buf[i], 0) << "byte " << i; +} + +// server_pdu_payload() must never classify an exception PDU as a read: [fc|0x80, code] is 2 bytes, and a +// read-offset of 2 would return an empty span, losing the exception code. The payload of an exception PDU +// is the exception code byte, for reads and writes alike. +TEST(ModbusServerPduPayload, ExceptionOfReadYieldsExceptionCode) { + const uint8_t pdu[] = {0x83, 0x02}; // exception response to READ_HOLDING_REGISTERS + auto payload = server_pdu_payload(pdu); + ASSERT_EQ(payload.size(), 1u); + EXPECT_EQ(payload[0], 0x02); +} + +TEST(ModbusServerPduPayload, ExceptionOfWriteYieldsExceptionCode) { + const uint8_t pdu[] = {0x86, 0x03}; // exception response to WRITE_SINGLE_REGISTER + auto payload = server_pdu_payload(pdu); + ASSERT_EQ(payload.size(), 1u); + EXPECT_EQ(payload[0], 0x03); } } // namespace esphome::modbus::helpers diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index 51951a4528..aa2855c2b0 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -18,7 +18,7 @@ binary_sensor: modbus_controller_id: modbus_controller1 id: modbus_binary_sensor2 name: Test Binary Sensor with Lambda - register_type: read + register_type: input address: 0x3201 lambda: |- return x; diff --git a/tests/components/modbus_server/modbus_server_test.cpp b/tests/components/modbus_server/modbus_server_test.cpp index 419bb9cf25..8c2e1d16d9 100644 --- a/tests/components/modbus_server/modbus_server_test.cpp +++ b/tests/components/modbus_server/modbus_server_test.cpp @@ -4,7 +4,7 @@ namespace esphome::modbus_server { -using modbus::ModbusExceptionCode; +using modbus::ExceptionCode; using modbus::RegisterValues; namespace { @@ -29,7 +29,7 @@ TEST(ModbusServerWrite, SingleWordSucceeds) { }; server.add_server_register(®); - auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234})); + auto status = server.on_write_registers(0x0000, make_registers({0x1234})); EXPECT_FALSE(status.has_value()); // nullopt == success EXPECT_EQ(written, 0x1234); } @@ -45,7 +45,7 @@ TEST(ModbusServerWrite, DwordSucceeds) { }; server.add_server_register(®); - auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234, 0x5678})); + auto status = server.on_write_registers(0x0000, make_registers({0x1234, 0x5678})); EXPECT_FALSE(status.has_value()); EXPECT_EQ(written, 0x12345678); } @@ -70,10 +70,10 @@ TEST(ModbusServerWrite, UnderSuppliedValueAppliesNothing) { server.add_server_register(&dword_reg); // Two words supplied: one for the WORD at 0x0000, but only one of the two the DWORD at 0x0001 needs. - auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1111, 0x2222})); + auto status = server.on_write_registers(0x0000, make_registers({0x1111, 0x2222})); ASSERT_TRUE(status.has_value()); if (status.has_value()) - EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_VALUE); + EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_VALUE); EXPECT_FALSE(word_written); // the writable WORD must NOT have been applied EXPECT_FALSE(dword_written); } @@ -84,19 +84,19 @@ TEST(ModbusServerWrite, UnwritableRegisterRejected) { ServerRegister read_only(0x0000, SensorValueType::U_WORD, 1); // no write_lambda set server.add_server_register(&read_only); - auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234})); + auto status = server.on_write_registers(0x0000, make_registers({0x1234})); ASSERT_TRUE(status.has_value()); if (status.has_value()) - EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS); } // An address with no registered register yields ILLEGAL_DATA_ADDRESS. TEST(ModbusServerWrite, UnmatchedAddressRejected) { ModbusServer server; - auto status = server.on_modbus_write_registers(0x0005, make_registers({0x1234})); + auto status = server.on_write_registers(0x0005, make_registers({0x1234})); ASSERT_TRUE(status.has_value()); if (status.has_value()) - EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS); } // A write_lambda failing at runtime is the one non-atomic case: the earlier register is already @@ -114,14 +114,14 @@ TEST(ModbusServerWrite, CallbackFailureIsServiceDeviceFailure) { server.add_server_register(&first); server.add_server_register(&second); - auto status = server.on_modbus_write_registers(0x0000, make_registers({0xAAAA, 0xBBBB})); + auto status = server.on_write_registers(0x0000, make_registers({0xAAAA, 0xBBBB})); ASSERT_TRUE(status.has_value()); if (status.has_value()) - EXPECT_EQ(status.value(), ModbusExceptionCode::SERVICE_DEVICE_FAILURE); + EXPECT_EQ(status.value(), ExceptionCode::SERVICE_DEVICE_FAILURE); EXPECT_TRUE(first_written); // pre-validation passed, so the first write applied before the failure } -// --- on_modbus_read_registers -------------------------------------------------- +// --- on_read_registers -------------------------------------------------- TEST(ModbusServerRead, SingleWordSucceeds) { ModbusServer server; @@ -130,7 +130,7 @@ TEST(ModbusServerRead, SingleWordSucceeds) { server.add_server_register(®); RegisterValues out; - auto status = server.on_modbus_read_registers(0x0000, 1, out); + auto status = server.on_read_registers(0x0000, 1, out); EXPECT_FALSE(status.has_value()); ASSERT_EQ(out.size(), 1u); EXPECT_EQ(out[0], 0x1234); @@ -143,7 +143,7 @@ TEST(ModbusServerRead, DwordReturnsTwoWordsHighFirst) { server.add_server_register(®); RegisterValues out; - auto status = server.on_modbus_read_registers(0x0000, 2, out); + auto status = server.on_read_registers(0x0000, 2, out); EXPECT_FALSE(status.has_value()); ASSERT_EQ(out.size(), 2u); EXPECT_EQ(out[0], 0x1234); @@ -165,10 +165,10 @@ TEST(ModbusServerRead, StartInsideValueRejected) { server.add_server_register(®); RegisterValues out; - auto status = server.on_modbus_read_registers(0x0011, 1, out); // the second cell of the DWORD + auto status = server.on_read_registers(0x0011, 1, out); // the second cell of the DWORD ASSERT_TRUE(status.has_value()); if (status.has_value()) - EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS); EXPECT_FALSE(read_called); } @@ -184,10 +184,10 @@ TEST(ModbusServerRead, ClippedTailRejected) { server.add_server_register(®); RegisterValues out; - auto status = server.on_modbus_read_registers(0x0000, 1, out); // only 1 of the DWORD's 2 registers + auto status = server.on_read_registers(0x0000, 1, out); // only 1 of the DWORD's 2 registers ASSERT_TRUE(status.has_value()); if (status.has_value()) - EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS); EXPECT_FALSE(read_called); } @@ -200,10 +200,10 @@ TEST(ModbusServerRead, WriteOnlyRegisterRejected) { server.add_server_register(®); RegisterValues out; - auto status = server.on_modbus_read_registers(0x0000, 1, out); + auto status = server.on_read_registers(0x0000, 1, out); ASSERT_TRUE(status.has_value()); if (status.has_value()) - EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS); } // An unregistered address with courtesy enabled returns the default value for each cell. @@ -213,7 +213,7 @@ TEST(ModbusServerRead, CourtesyDefaultForUnregistered) { ServerCourtesyResponse{.enabled = true, .register_last_address = 0xFFFF, .register_value = 0xABCD}); RegisterValues out; - auto status = server.on_modbus_read_registers(0x0005, 2, out); + auto status = server.on_read_registers(0x0005, 2, out); EXPECT_FALSE(status.has_value()); ASSERT_EQ(out.size(), 2u); EXPECT_EQ(out[0], 0xABCD); @@ -224,10 +224,10 @@ TEST(ModbusServerRead, CourtesyDefaultForUnregistered) { TEST(ModbusServerRead, UnregisteredRejectedWithoutCourtesy) { ModbusServer server; RegisterValues out; - auto status = server.on_modbus_read_registers(0x0005, 1, out); + auto status = server.on_read_registers(0x0005, 1, out); ASSERT_TRUE(status.has_value()); if (status.has_value()) - EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_EQ(status.value(), ExceptionCode::ILLEGAL_DATA_ADDRESS); } // --- partial reads (opt-in) ---------------------------------------------------- @@ -241,7 +241,7 @@ TEST(ModbusServerRead, PartialReadHighWord) { server.add_server_register(®); RegisterValues out; - auto status = server.on_modbus_read_registers(0x0010, 1, out); + auto status = server.on_read_registers(0x0010, 1, out); EXPECT_FALSE(status.has_value()); ASSERT_EQ(out.size(), 1u); EXPECT_EQ(out[0], 0x1234); @@ -256,7 +256,7 @@ TEST(ModbusServerRead, PartialReadLowWordFromInterior) { server.add_server_register(®); RegisterValues out; - auto status = server.on_modbus_read_registers(0x0011, 1, out); + auto status = server.on_read_registers(0x0011, 1, out); EXPECT_FALSE(status.has_value()); ASSERT_EQ(out.size(), 1u); EXPECT_EQ(out[0], 0x5678); @@ -272,12 +272,12 @@ TEST(ModbusServerRead, PartialReadReversedType) { server.add_server_register(®); RegisterValues first; - ASSERT_FALSE(server.on_modbus_read_registers(0x0010, 1, first).has_value()); + ASSERT_FALSE(server.on_read_registers(0x0010, 1, first).has_value()); ASSERT_EQ(first.size(), 1u); EXPECT_EQ(first[0], 0x5678); RegisterValues second; - ASSERT_FALSE(server.on_modbus_read_registers(0x0011, 1, second).has_value()); + ASSERT_FALSE(server.on_read_registers(0x0011, 1, second).has_value()); ASSERT_EQ(second.size(), 1u); EXPECT_EQ(second[0], 0x1234); } diff --git a/tests/components/network/__init__.py b/tests/components/network/__init__.py new file mode 100644 index 0000000000..101f63ac82 --- /dev/null +++ b/tests/components/network/__init__.py @@ -0,0 +1,13 @@ +import esphome.codegen as cg +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() + real_to_code = manifest.to_code + + async def to_code_testing(config): + await real_to_code(config) + cg.add_define("USE_NETWORK_IPV6", True) + + manifest.to_code = to_code_testing diff --git a/tests/components/network/test-priority.esp32-idf.yaml b/tests/components/network/test-priority.esp32-idf.yaml new file mode 100644 index 0000000000..baa821a234 --- /dev/null +++ b/tests/components/network/test-priority.esp32-idf.yaml @@ -0,0 +1,23 @@ +# Compiled dual-stack test: wifi + ethernet coexisting via network: priority:. +# This is the first build path that keeps both radios' stacks compiled in, so +# it must actually compile (not just validate) to guard the reconciler wiring. +# WiFi is listed first so the build also exercises the wifi-primary branch in +# network/util.cpp (the ethernet-primary branch matches the legacy order). +wifi: + ssid: MySSID + password: password1 + +ethernet: + type: W5500 + clk_pin: GPIO19 + mosi_pin: GPIO21 + miso_pin: GPIO23 + cs_pin: GPIO18 + interrupt_pin: GPIO36 + reset_pin: GPIO22 + clock_speed: 10Mhz + +network: + priority: + - wifi + - ethernet diff --git a/tests/components/network/test_ip_address_host.cpp b/tests/components/network/test_ip_address_host.cpp new file mode 100644 index 0000000000..4070f422b1 --- /dev/null +++ b/tests/components/network/test_ip_address_host.cpp @@ -0,0 +1,208 @@ +#include + +#include "esphome/components/network/ip_address.h" + +#ifdef USE_HOST +#if USE_NETWORK_IPV6 + +namespace esphome::network::testing { + +// ========================================================================= +// IPv4 +// ========================================================================= + +TEST(IPAddressHost, IPv4DefaultNotSet) { + IPAddress addr; + EXPECT_FALSE(addr.is_set()); +} + +TEST(IPAddressHost, IPv4DefaultIsIPv4) { + IPAddress addr; + EXPECT_TRUE(addr.is_ip4()); + EXPECT_FALSE(addr.is_ip6()); +} + +TEST(IPAddressHost, IPv4ParseAndSerialize) { + IPAddress addr("192.168.1.1"); + char buf[IP_ADDRESS_BUFFER_SIZE]; + EXPECT_STREQ(addr.str_to(buf), "192.168.1.1"); +} + +TEST(IPAddressHost, IPv4FromOctets) { + IPAddress addr(192, 168, 1, 1); + char buf[IP_ADDRESS_BUFFER_SIZE]; + EXPECT_STREQ(addr.str_to(buf), "192.168.1.1"); +} + +TEST(IPAddressHost, IPv4IsSet) { + IPAddress addr("192.168.1.1"); + EXPECT_TRUE(addr.is_set()); +} + +TEST(IPAddressHost, IPv4IsIp4) { + IPAddress addr("192.168.1.1"); + EXPECT_TRUE(addr.is_ip4()); + EXPECT_FALSE(addr.is_ip6()); +} + +TEST(IPAddressHost, IPv4MulticastDetected) { + IPAddress addr("239.0.60.53"); + EXPECT_TRUE(addr.is_multicast()); +} + +TEST(IPAddressHost, IPv4MulticastBoundaryLow) { + IPAddress addr("224.0.0.0"); + EXPECT_TRUE(addr.is_multicast()); +} + +TEST(IPAddressHost, IPv4MulticastBoundaryHigh) { + IPAddress addr("239.255.255.255"); + EXPECT_TRUE(addr.is_multicast()); +} + +TEST(IPAddressHost, IPv4UnicastNotMulticast) { + IPAddress addr("192.168.1.1"); + EXPECT_FALSE(addr.is_multicast()); +} + +TEST(IPAddressHost, IPv4EqualityMatch) { + IPAddress a("192.168.1.1"); + IPAddress b("192.168.1.1"); + EXPECT_EQ(a, b); +} + +TEST(IPAddressHost, IPv4EqualityMismatch) { + IPAddress a("192.168.1.1"); + IPAddress b("192.168.1.2"); + EXPECT_NE(a, b); +} + +TEST(IPAddressHost, IPv4FromOctetsMatchesParse) { + IPAddress from_octets(192, 168, 1, 1); + IPAddress from_string("192.168.1.1"); + EXPECT_EQ(from_octets, from_string); +} + +TEST(IPAddressHost, IPv4FromIPAddrT) { + ip_addr_t raw; + memset(&raw, 0, sizeof(raw)); + raw.u_addr.ip4.s_addr = htonl((192u << 24) | (168u << 16) | (1u << 8) | 1u); + raw.type = IPADDR_TYPE_V4; + IPAddress addr(&raw); + char buf[IP_ADDRESS_BUFFER_SIZE]; + EXPECT_STREQ(addr.str_to(buf), "192.168.1.1"); + EXPECT_TRUE(addr.is_ip4()); + EXPECT_FALSE(addr.is_ip6()); +} + +// ========================================================================= +// IPv6 +// ========================================================================= + +TEST(IPAddressHost, IPv6ParseAndSerialize) { + IPAddress addr("ff12::cafe"); + char buf[IP_ADDRESS_BUFFER_SIZE]; + EXPECT_STREQ(addr.str_to(buf), "ff12::cafe"); +} + +TEST(IPAddressHost, IPv6Loopback) { + IPAddress addr("::1"); + char buf[IP_ADDRESS_BUFFER_SIZE]; + EXPECT_STREQ(addr.str_to(buf), "::1"); +} + +TEST(IPAddressHost, IPv6IsIp6) { + IPAddress addr("ff12::cafe"); + EXPECT_TRUE(addr.is_ip6()); + EXPECT_FALSE(addr.is_ip4()); +} + +TEST(IPAddressHost, IPv6AllZerosNotSet) { + IPAddress addr("::"); + EXPECT_FALSE(addr.is_set()); +} + +TEST(IPAddressHost, IPv6LoopbackIsSet) { + IPAddress addr("::1"); + EXPECT_TRUE(addr.is_set()); +} + +TEST(IPAddressHost, IPv6MulticastDetected) { + IPAddress addr("ff12::cafe"); + EXPECT_TRUE(addr.is_multicast()); +} + +TEST(IPAddressHost, IPv6MulticastLinkLocal) { + IPAddress addr("ff02::1"); + EXPECT_TRUE(addr.is_multicast()); +} + +TEST(IPAddressHost, IPv6UnicastNotMulticast) { + IPAddress addr("::1"); + EXPECT_FALSE(addr.is_multicast()); +} + +TEST(IPAddressHost, IPv6EqualityMatch) { + IPAddress a("ff12::cafe"); + IPAddress b("ff12::cafe"); + EXPECT_EQ(a, b); +} + +TEST(IPAddressHost, IPv6EqualityMismatch) { + IPAddress a("ff12::cafe"); + IPAddress b("ff02::1"); + EXPECT_NE(a, b); +} + +TEST(IPAddressHost, IPv6OutputIsLowercase) { + // inet_pton is case-insensitive; str_to must lowercase the output + IPAddress addr("FF12::CAFE"); + char buf[IP_ADDRESS_BUFFER_SIZE]; + addr.str_to(buf); + for (const char *p = buf; *p; ++p) { + EXPECT_FALSE(*p >= 'A' && *p <= 'F') << "uppercase letter in: " << buf; + } +} + +TEST(IPAddressHost, IPv6FullAddressRoundTrip) { + // A full 128-bit address with no compression opportunity + const char *input = "fde0:983a:d0d3:a65e:725a:0fff:fe36:9916"; + IPAddress addr(input); + char buf[IP_ADDRESS_BUFFER_SIZE]; + addr.str_to(buf); + EXPECT_NE(buf[0], '\0'); + EXPECT_NE(std::string(buf).find("fde0"), std::string::npos); +} + +// ========================================================================= +// Malformed input +// ========================================================================= + +TEST(IPAddressHost, MalformedIPv4YieldsEmptyAddress) { + IPAddress addr("not-an-ip"); + EXPECT_FALSE(addr.is_set()); + EXPECT_TRUE(addr.is_ip4()); +} + +TEST(IPAddressHost, MalformedIPv6YieldsEmptyAddress) { + // "gg::1" looks like IPv6 (contains ':') but fails inet_pton; addr stays + // zeroed (type=V4 from memset) so is_set() is false and is_ip4() is true. + IPAddress addr("gg::1"); + EXPECT_FALSE(addr.is_set()); + EXPECT_TRUE(addr.is_ip4()); +} + +// ========================================================================= +// Cross-family +// ========================================================================= + +TEST(IPAddressHost, IPv4AndIPv6NotEqual) { + IPAddress v4("192.168.1.1"); + IPAddress v6("::1"); + EXPECT_NE(v4, v6); +} + +} // namespace esphome::network::testing + +#endif // USE_NETWORK_IPV6 +#endif // USE_HOST diff --git a/tests/components/nrf52/test.nrf52-microbit.yaml b/tests/components/nrf52/test.nrf52-microbit.yaml new file mode 100644 index 0000000000..d27f9ff699 --- /dev/null +++ b/tests/components/nrf52/test.nrf52-microbit.yaml @@ -0,0 +1 @@ +nrf52: diff --git a/tests/components/nrf52/test.nrf52-xiao-ble.yaml b/tests/components/nrf52/test.nrf52-xiao-ble.yaml index de4c0c6e00..e1b5f088bb 100644 --- a/tests/components/nrf52/test.nrf52-xiao-ble.yaml +++ b/tests/components/nrf52/test.nrf52-xiao-ble.yaml @@ -2,3 +2,5 @@ nrf52: dfu: true reg0: voltage: 1.8V + framework: + libc_nano: false diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index fc3cc94217..f71cf63de9 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -2,11 +2,9 @@ wifi: ssid: MySSID password: password1 -# Purposely test that `online_image:` does auto-load `image:` -# Keep the `image:` undefined. -# image: -online_image: - - id: online_binary_image +image: + - platform: online_image + id: online_binary_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png format: PNG type: BINARY @@ -21,34 +19,41 @@ online_image: } else { ESP_LOGD("online_image", "Cache miss: fresh download"); } - - id: online_binary_transparent_image + - platform: online_image + id: online_binary_transparent_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png type: BINARY transparency: chroma_key format: png - - id: online_rgba_image + - platform: online_image + id: online_rgba_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png format: PNG type: RGB transparency: alpha_channel - - id: online_rgb24_image + - platform: online_image + id: online_rgb24_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png format: PNG type: RGB transparency: chroma_key - - id: online_binary_bmp + - platform: online_image + id: online_binary_bmp url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp format: BMP type: BINARY - - id: online_rgb_bmp_8bit + - platform: online_image + id: online_rgb_bmp_8bit url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp format: BMP type: RGB - - id: online_jpeg_image + - platform: online_image + id: online_jpeg_image url: http://www.faqs.org/images/library.jpg format: JPEG type: RGB - - id: online_jpg_image + - platform: online_image + id: online_jpg_image url: http://www.faqs.org/images/library.jpg format: JPG type: RGB565 diff --git a/tests/components/online_image/validate.host.yaml b/tests/components/online_image/validate.host.yaml new file mode 100644 index 0000000000..f0ba98c65d --- /dev/null +++ b/tests/components/online_image/validate.host.yaml @@ -0,0 +1,22 @@ +# Legacy top-level `online_image:` form (deprecated; migrates to +# `platform: online_image`). Config-only test exercising the deprecation path. +wifi: + ssid: MySSID + password: password1 + +http_request: + +display: + - platform: sdl + id: online_image_display + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +online_image: + - id: legacy_online_image + url: http://www.example.org/example.png + format: PNG + type: RGB565 + resize: 50x50 diff --git a/tests/components/openthread_info/test.esp32-c6-idf.yaml b/tests/components/openthread_info/test.esp32-c6-idf.yaml index ded0f17611..8c55546ce6 100644 --- a/tests/components/openthread_info/test.esp32-c6-idf.yaml +++ b/tests/components/openthread_info/test.esp32-c6-idf.yaml @@ -28,3 +28,32 @@ text_sensor: name: "PAN ID" ext_pan_id: name: "Extended PAN ID" + +sensor: + - platform: openthread_info + parent_average_rssi: + name: "Parent Average RSSI" + parent_last_rssi: + name: "Parent Last RSSI" + parent_link_quality_in: + name: "Parent Link Quality In" + parent_link_quality_out: + name: "Parent Link Quality Out" + tx_total: + name: "TX Total" + tx_retries: + name: "TX Retries" + tx_err_cca: + name: "TX CCA Errors" + tx_err_abort: + name: "TX Abort Errors" + rx_total: + name: "RX Total" + rx_err_fcs: + name: "RX FCS Errors" + attach_attempts: + name: "Attach Attempts" + parent_changes: + name: "Parent Changes" + partition_id_changes: + name: "Partition ID Changes" diff --git a/tests/components/ota/test.nrf52-adafruit.yaml b/tests/components/ota/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..e8ac96f051 --- /dev/null +++ b/tests/components/ota/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +zephyr_ble_server: + +ota: + - platform: zephyr_mcumgr diff --git a/tests/components/ota/test_version_compare.cpp b/tests/components/ota/test_version_compare.cpp new file mode 100644 index 0000000000..4072a45792 --- /dev/null +++ b/tests/components/ota/test_version_compare.cpp @@ -0,0 +1,52 @@ +#include + +#include "esphome/components/ota/ota_backend.h" + +namespace esphome::ota::testing { + +// version_is_older(candidate, reference) == true means candidate is a downgrade +// and should be rejected. + +TEST(VersionIsOlder, PatchOlder) { + EXPECT_TRUE(version_is_older("1.2.3", "1.2.4")); + EXPECT_FALSE(version_is_older("1.2.4", "1.2.3")); +} + +TEST(VersionIsOlder, NumericNotLexical) { + // "1.10.0" is newer than "1.9.0" even though '1' < '9' lexically. + EXPECT_TRUE(version_is_older("1.9.0", "1.10.0")); + EXPECT_FALSE(version_is_older("1.10.0", "1.9.0")); +} + +TEST(VersionIsOlder, MajorMinor) { + EXPECT_TRUE(version_is_older("1.9.9", "2.0.0")); + EXPECT_TRUE(version_is_older("1.2.9", "1.3.0")); + EXPECT_FALSE(version_is_older("2.0.0", "1.9.9")); +} + +TEST(VersionIsOlder, EqualVersionsAllowed) { + // Re-flashing the same version must be permitted. + EXPECT_FALSE(version_is_older("1.2.3", "1.2.3")); + EXPECT_FALSE(version_is_older("2024.1.0", "2024.1.0")); +} + +TEST(VersionIsOlder, DifferingComponentCounts) { + // Missing trailing components count as 0. + EXPECT_FALSE(version_is_older("1.2", "1.2.0")); + EXPECT_FALSE(version_is_older("1.2.0", "1.2")); + EXPECT_TRUE(version_is_older("1.2", "1.2.1")); + EXPECT_FALSE(version_is_older("1.2.1", "1.2")); +} + +TEST(VersionIsOlder, CalendarVersions) { + EXPECT_TRUE(version_is_older("2024.12.0", "2025.1.0")); + EXPECT_FALSE(version_is_older("2025.1.0", "2024.12.0")); +} + +TEST(VersionIsOlder, NullInputsAreSafe) { + EXPECT_FALSE(version_is_older(nullptr, "1.2.3")); + EXPECT_FALSE(version_is_older("1.2.3", nullptr)); + EXPECT_FALSE(version_is_older(nullptr, nullptr)); +} + +} // namespace esphome::ota::testing diff --git a/tests/components/packet_transport/common.yaml b/tests/components/packet_transport/common.yaml index 9151cf27dc..5c6c8dd636 100644 --- a/tests/components/packet_transport/common.yaml +++ b/tests/components/packet_transport/common.yaml @@ -7,36 +7,40 @@ udp: addresses: ["239.0.60.53"] packet_transport: - platform: udp - update_interval: 5s - encryption: "our key goes here" - rolling_code_enable: true - ping_pong_enable: true - binary_sensors: - - binary_sensor_id1 - - id: binary_sensor_id1 - broadcast_id: other_id - sensors: - - sensor_id1 - - id: sensor_id1 - broadcast_id: other_id - providers: - - name: some-device-name - encryption: "their key goes here" + - platform: udp + id: transport_udp + update_interval: 5s + encryption: "our key goes here" + rolling_code_enable: true + ping_pong_enable: true + binary_sensors: + - binary_sensor_id1 + - id: binary_sensor_id1 + broadcast_id: other_id + sensors: + - sensor_id1 + - id: sensor_id1 + broadcast_id: other_id + providers: + - name: some-device-name + encryption: "their key goes here" sensor: - platform: template id: sensor_id1 - platform: packet_transport + transport_id: transport_udp provider: some-device-name id: our_id remote_id: some_sensor_id binary_sensor: - platform: packet_transport + transport_id: transport_udp provider: unencrypted-device id: other_binary_sensor_id - platform: packet_transport + transport_id: transport_udp provider: some-device-name type: status name: Some-Device Status diff --git a/tests/components/packet_transport/test.host.yaml b/tests/components/packet_transport/test.host.yaml index 49fdbbc9b2..f67b561226 100644 --- a/tests/components/packet_transport/test.host.yaml +++ b/tests/components/packet_transport/test.host.yaml @@ -3,36 +3,40 @@ udp: addresses: ["239.0.60.53"] packet_transport: - platform: udp - update_interval: 5s - encryption: "our key goes here" - rolling_code_enable: true - ping_pong_enable: true - binary_sensors: - - binary_sensor_id1 - - id: binary_sensor_id1 - broadcast_id: other_id - sensors: - - sensor_id1 - - id: sensor_id1 - broadcast_id: other_id - providers: - - name: some-device-name - encryption: "their key goes here" + - platform: udp + id: transport_udp + update_interval: 5s + encryption: "our key goes here" + rolling_code_enable: true + ping_pong_enable: true + binary_sensors: + - binary_sensor_id1 + - id: binary_sensor_id1 + broadcast_id: other_id + sensors: + - sensor_id1 + - id: sensor_id1 + broadcast_id: other_id + providers: + - name: some-device-name + encryption: "their key goes here" sensor: - platform: template id: sensor_id1 - platform: packet_transport + transport_id: transport_udp provider: some-device-name id: our_id remote_id: some_sensor_id binary_sensor: - platform: packet_transport + transport_id: transport_udp provider: unencrypted-device id: other_binary_sensor_id - platform: packet_transport + transport_id: transport_udp provider: some-device-name type: status name: Some-Device Status diff --git a/tests/components/pcm5122/common.yaml b/tests/components/pcm5122/common.yaml index cf96f57464..a8ae1e6975 100644 --- a/tests/components/pcm5122/common.yaml +++ b/tests/components/pcm5122/common.yaml @@ -4,6 +4,11 @@ audio_dac: i2c_id: i2c_bus address: 0x4D bits_per_sample: 32bit + analog_gain: -6db + channel_mix: swapped + volume_min_db: -60dB + volume_max_db: -3dB + enable_pin: GPIO12 output: - platform: gpio @@ -22,3 +27,9 @@ binary_sensor: number: 4 mode: input: true + +switch: + - platform: pcm5122 + pcm5122: pcm5122_dac + name: PCM5122 Power Down + power_mode: powerdown diff --git a/tests/components/preferences/validate-rtc-storage.esp32-idf.yaml b/tests/components/preferences/validate-rtc-storage.esp32-idf.yaml new file mode 100644 index 0000000000..1808e09f5d --- /dev/null +++ b/tests/components/preferences/validate-rtc-storage.esp32-idf.yaml @@ -0,0 +1,4 @@ +# Exercises the opt-in that compiles the RTC-backed preference storage into +# the ESP32 backend without any other option selecting it. +preferences: + rtc_storage: true diff --git a/tests/components/provisioning/test.esp32-idf.yaml b/tests/components/provisioning/test.esp32-idf.yaml new file mode 100644 index 0000000000..24168881fc --- /dev/null +++ b/tests/components/provisioning/test.esp32-idf.yaml @@ -0,0 +1,25 @@ +# Exercises the provisioning window: api registers as a provisioning source +# (encryption enabled, no key), the on_timeout automation, and the wifi + +# esp32_improv cross-component guards. improv_serial is intentionally NOT gated. +provisioning: + timeout: 1min + on_timeout: + then: + - logger.log: "Provisioning window expired" + +api: + encryption: + +wifi: + ssid: MySSID + password: password1 + +improv_serial: + +binary_sensor: + - platform: gpio + pin: 0 + id: io0_button + +esp32_improv: + authorizer: io0_button diff --git a/tests/components/provisioning/test.esp8266-ard.yaml b/tests/components/provisioning/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4188c00bef --- /dev/null +++ b/tests/components/provisioning/test.esp8266-ard.yaml @@ -0,0 +1,16 @@ +# Provisioning window on ESP8266 (no BLE Improv): api as a provisioning source +# and the wifi reboot guard. improv_serial is present and intentionally NOT gated. +provisioning: + timeout: 1min + on_timeout: + then: + - logger.log: "Provisioning window expired" + +api: + encryption: + +wifi: + ssid: MySSID + password: password1 + +improv_serial: diff --git a/tests/components/provisioning/validate.esp32-idf.yaml b/tests/components/provisioning/validate.esp32-idf.yaml new file mode 100644 index 0000000000..1fd3d67882 --- /dev/null +++ b/tests/components/provisioning/validate.esp32-idf.yaml @@ -0,0 +1,15 @@ +# A device provisioned over the network (wifi / Improv) with no api: network +# connectivity alone satisfies provisioning, so `provisioning:` is valid without an +# api encryption source. Config-only -- exercises the network provisioning-source +# validation path (the Improv-only case from the review). +provisioning: + timeout: 1min + on_timeout: + then: + - logger.log: "Provisioning window expired" + +wifi: + ssid: MySSID + password: password1 + +improv_serial: diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index 5631c48f95..981946a9a4 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -198,7 +198,7 @@ button: 0xFF, ] - platform: template - name: Haier + name: Haier Long on_press: remote_transmitter.transmit_haier: code: @@ -217,6 +217,21 @@ button: 0x00, 0x05, ] + - platform: template + name: Haier Short + on_press: + remote_transmitter.transmit_haier: + code: + [ + 0xA6, + 0xDA, + 0x00, + 0x00, + 0x40, + 0x40, + 0x00, + 0x80, + ] - platform: template name: Mirage on_press: diff --git a/tests/components/rp2040/test.rp2040-ard.yaml b/tests/components/rp2/test.rp2040-ard.yaml similarity index 97% rename from tests/components/rp2040/test.rp2040-ard.yaml rename to tests/components/rp2/test.rp2040-ard.yaml index 09531f914e..eaa494a01a 100644 --- a/tests/components/rp2040/test.rp2040-ard.yaml +++ b/tests/components/rp2/test.rp2040-ard.yaml @@ -1,4 +1,4 @@ -rp2040: +rp2: variant: rp2040 enable_full_printf: false diff --git a/tests/components/rp2040/test.rp2040-pico2-ard.yaml b/tests/components/rp2/test.rp2350-ard.yaml similarity index 90% rename from tests/components/rp2040/test.rp2040-pico2-ard.yaml rename to tests/components/rp2/test.rp2350-ard.yaml index c9d795840d..84ee39a81e 100644 --- a/tests/components/rp2040/test.rp2040-pico2-ard.yaml +++ b/tests/components/rp2/test.rp2350-ard.yaml @@ -1,4 +1,4 @@ -rp2040: +rp2: variant: rp2350 enable_full_printf: false diff --git a/tests/components/safe_mode/test-no-shutdown-confirm.esp32-idf.yaml b/tests/components/safe_mode/test-no-shutdown-confirm.esp32-idf.yaml new file mode 100644 index 0000000000..5ee1f308a3 --- /dev/null +++ b/tests/components/safe_mode/test-no-shutdown-confirm.esp32-idf.yaml @@ -0,0 +1,11 @@ +# Compile with OTA rollback support active (ota + safe_mode on ESP-IDF, the +# default) but boot_is_good_on_shutdown disabled, so an orderly shutdown does +# not confirm the app image; only boot_is_good_after / mark_successful do. +packages: + safe_mode: !include common-enabled.yaml + +safe_mode: + boot_is_good_on_shutdown: false + +ota: + - platform: esphome diff --git a/tests/components/safe_mode/test-rtc.esp32-idf.yaml b/tests/components/safe_mode/test-rtc.esp32-idf.yaml new file mode 100644 index 0000000000..113a2b6ab5 --- /dev/null +++ b/tests/components/safe_mode/test-rtc.esp32-idf.yaml @@ -0,0 +1,4 @@ +# Exercises the ESP32 RTC-backed preferences path (storage: rtc) for safe_mode. +safe_mode: + num_attempts: 3 + storage: rtc diff --git a/tests/components/sen5x/common.yaml b/tests/components/sen5x/common.yaml index a4462a16ea..5cfff15243 100644 --- a/tests/components/sen5x/common.yaml +++ b/tests/components/sen5x/common.yaml @@ -24,10 +24,10 @@ sensor: name: PM <10µm Weight concentration id: pm_10_0 accuracy_decimals: 1 - nox: - name: NOx - voc: - name: VOC + nox_index: + name: NOx Index + voc_index: + name: VOC Index algorithm_tuning: index_offset: 100 learning_time_offset_hours: 12 @@ -42,4 +42,5 @@ sensor: auto_cleaning_interval: 604800s acceleration_mode: low store_baseline: true + model: sen55 address: 0x69 diff --git a/tests/components/sen6x/common.yaml b/tests/components/sen6x/common.yaml index 61ff9f1e0c..859e012c4a 100644 --- a/tests/components/sen6x/common.yaml +++ b/tests/components/sen6x/common.yaml @@ -26,10 +26,10 @@ sensor: name: PM <10µm Weight concentration id: sen6x_pm_10_0 accuracy_decimals: 1 - nox: - name: NOx - voc: - name: VOC + nox_index: + name: NOx Index + voc_index: + name: VOC Index co2: name: Carbon Dioxide formaldehyde: diff --git a/tests/components/sgp4x/common.yaml b/tests/components/sgp4x/common.yaml index 4edda8fd1b..88b8166876 100644 --- a/tests/components/sgp4x/common.yaml +++ b/tests/components/sgp4x/common.yaml @@ -1,7 +1,7 @@ sensor: - platform: sgp4x i2c_id: i2c_bus - voc: + voc_index: name: VOC Index id: sgp40_voc_index algorithm_tuning: @@ -11,8 +11,8 @@ sensor: gating_max_duration_minutes: 180 std_initial: 50 gain_factor: 230 - nox: - name: NOx + nox_index: + name: NOx Index algorithm_tuning: index_offset: 100 learning_time_offset_hours: 12 diff --git a/tests/components/spi/test.rp2040-pico2-ard.yaml b/tests/components/spi/test.rp2350-ard.yaml similarity index 100% rename from tests/components/spi/test.rp2040-pico2-ard.yaml rename to tests/components/spi/test.rp2350-ard.yaml diff --git a/tests/components/syslog/test.host.yaml b/tests/components/syslog/test.host.yaml index 31122437d5..d9aa8e529b 100644 --- a/tests/components/syslog/test.host.yaml +++ b/tests/components/syslog/test.host.yaml @@ -2,7 +2,7 @@ udp: addresses: ["239.0.60.53"] time: - platform: host + - platform: host syslog: port: 514 diff --git a/tests/components/test_display/common.yaml b/tests/components/test_display/common.yaml new file mode 100644 index 0000000000..c36cf4b997 --- /dev/null +++ b/tests/components/test_display/common.yaml @@ -0,0 +1,13 @@ +# The test_display platform (and its external_components entry) is provided by +# the shared package included from the test.*.yaml files. These extra instances +# exercise the remaining `dimensions` code paths: the width/height map form and +# the default when omitted. The package's own `test_display_screen` covers the +# "WIDTHxHEIGHT" string form. +display: + - platform: test_display + id: test_display_wh_dimensions + dimensions: + width: 320 + height: 240 + - platform: test_display + id: test_display_default_dimensions diff --git a/tests/components/test_display/components/test_display/__init__.py b/tests/components/test_display/components/test_display/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/components/test_display/components/test_display/display.py b/tests/components/test_display/components/test_display/display.py new file mode 100644 index 0000000000..8503053b46 --- /dev/null +++ b/tests/components/test_display/components/test_display/display.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +from esphome.components import display +import esphome.config_validation as cv +from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_ID, CONF_WIDTH +from esphome.core import CoroPriority, coroutine_with_priority + +test_display_ns = cg.esphome_ns.namespace("test_display") +TestDisplay = test_display_ns.class_("TestDisplay", display.Display) + +CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TestDisplay), + cv.Optional(CONF_DIMENSIONS, default="100x100"): cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + } + ), + ), + } +) + + +@coroutine_with_priority(CoroPriority.CORE) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await display.register_display(var, config) + + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + width, height = dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT] + else: + width, height = dimensions + cg.add(var.set_dimensions(width, height)) diff --git a/tests/components/test_display/components/test_display/test_display.h b/tests/components/test_display/components/test_display/test_display.h new file mode 100644 index 0000000000..3f2b03a773 --- /dev/null +++ b/tests/components/test_display/components/test_display/test_display.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/components/display/display.h" +#include "esphome/core/color.h" + +namespace esphome::test_display { + +/** A no-op display that draws nothing and uses no pins. + * + * It exists purely to satisfy components that require a display (for example + * touchscreens, which read the display dimensions) in configurations - most + * notably YAML build tests - where a real display driver would only get in the + * way by occupying GPIO pins and pulling in bus dependencies. + */ +class TestDisplay : public display::Display { + public: + void update() override { this->do_update_(); } + + void set_dimensions(int width, int height) { + this->width_ = width; + this->height_ = height; + } + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + + void draw_pixel_at(int x, int y, Color color) override {} + + protected: + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + + int width_{0}; + int height_{0}; +}; + +} // namespace esphome::test_display diff --git a/tests/components/test_display/test.esp32-idf.yaml b/tests/components/test_display/test.esp32-idf.yaml new file mode 100644 index 0000000000..dfc8006c38 --- /dev/null +++ b/tests/components/test_display/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + test_display_extra: !include common.yaml diff --git a/tests/components/test_display/test.esp8266-ard.yaml b/tests/components/test_display/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dfc8006c38 --- /dev/null +++ b/tests/components/test_display/test.esp8266-ard.yaml @@ -0,0 +1,3 @@ +packages: + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + test_display_extra: !include common.yaml diff --git a/tests/components/test_display/test.rp2040-ard.yaml b/tests/components/test_display/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dfc8006c38 --- /dev/null +++ b/tests/components/test_display/test.rp2040-ard.yaml @@ -0,0 +1,3 @@ +packages: + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + test_display_extra: !include common.yaml diff --git a/tests/components/tt21100/common.yaml b/tests/components/tt21100/common.yaml index 1f9249f1ba..5cb6b99a8e 100644 --- a/tests/components/tt21100/common.yaml +++ b/tests/components/tt21100/common.yaml @@ -1,19 +1,8 @@ -display: - - platform: ssd1306_i2c - i2c_id: i2c_bus - id: tt21100_ssd1306_i2c_display - model: SSD1306_128X64 - reset_pin: ${disp_reset_pin} - pages: - - id: tt21100_page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - touchscreen: - platform: tt21100 i2c_id: i2c_bus id: tt21100_touchscreen - display: tt21100_ssd1306_i2c_display + display: test_display_screen interrupt_pin: ${interrupt_pin} reset_pin: ${reset_pin} diff --git a/tests/components/tt21100/test.esp32-idf.yaml b/tests/components/tt21100/test.esp32-idf.yaml index 033aafb73c..a79695d611 100644 --- a/tests/components/tt21100/test.esp32-idf.yaml +++ b/tests/components/tt21100/test.esp32-idf.yaml @@ -1,9 +1,8 @@ substitutions: - disp_reset_pin: GPIO12 interrupt_pin: GPIO15 reset_pin: GPIO4 packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml - -<<: !include common.yaml + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + tt21100: !include common.yaml diff --git a/tests/components/tt21100/test.esp8266-ard.yaml b/tests/components/tt21100/test.esp8266-ard.yaml index 25d1ff82e3..ae6977c6ec 100644 --- a/tests/components/tt21100/test.esp8266-ard.yaml +++ b/tests/components/tt21100/test.esp8266-ard.yaml @@ -1,9 +1,8 @@ substitutions: - disp_reset_pin: GPIO0 interrupt_pin: GPIO15 reset_pin: GPIO16 packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml - -<<: !include common.yaml + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + tt21100: !include common.yaml diff --git a/tests/components/tt21100/test.rp2040-ard.yaml b/tests/components/tt21100/test.rp2040-ard.yaml index 0d13628294..98b2ad600c 100644 --- a/tests/components/tt21100/test.rp2040-ard.yaml +++ b/tests/components/tt21100/test.rp2040-ard.yaml @@ -1,9 +1,8 @@ substitutions: - disp_reset_pin: GPIO10 interrupt_pin: GPIO2 reset_pin: GPIO3 packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml - -<<: !include common.yaml + test_display: !include ../../test_build_components/common/test_display/test_display.yaml + tt21100: !include common.yaml diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index de3ea3029e..5c4ba1130e 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -32,6 +32,9 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(size_t, available, (), (override)); MOCK_METHOD(UARTFlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); +#if defined(USE_ESP8266) || defined(USE_ESP32) + MOCK_METHOD(void, load_settings, (bool dump_config), (override)); +#endif }; } // namespace esphome::uart::testing diff --git a/tests/components/ultrasonic/test.nrf52-adafruit.yaml b/tests/components/ultrasonic/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/ultrasonic/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/web_server/common.yaml b/tests/components/web_server/common.yaml index 5a05a58c2d..d7b880efe6 100644 --- a/tests/components/web_server/common.yaml +++ b/tests/components/web_server/common.yaml @@ -4,6 +4,17 @@ wifi: binary_sensor: cover: + - platform: template + name: "Template Cover Assumed" + # assumed_state must be reflected in the web_server JSON (detail=all) + assumed_state: true + lambda: 'return COVER_OPEN;' + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action fan: light: sensor: diff --git a/tests/components/web_server/common_v2.yaml b/tests/components/web_server/common_v2.yaml index f2b15e484d..b9bc0bbf61 100644 --- a/tests/components/web_server/common_v2.yaml +++ b/tests/components/web_server/common_v2.yaml @@ -5,3 +5,6 @@ web_server: port: 8080 version: 2 compression: br + enable_private_network_access: true + allowed_origins: + - https://app.esphome.io diff --git a/tests/components/web_server/common_v3.yaml b/tests/components/web_server/common_v3.yaml index bdacaaddbe..354d7bb6ac 100644 --- a/tests/components/web_server/common_v3.yaml +++ b/tests/components/web_server/common_v3.yaml @@ -4,6 +4,9 @@ packages: web_server: port: 8080 version: 3 + # allowed_origins can be set independently of Private Network Access + allowed_origins: + - https://app.esphome.io sorting_groups: - id: sorting_group_1 name: "Group 1 Diplayed Last" diff --git a/tests/components/web_server/test.esp32-ard.yaml b/tests/components/web_server/test.esp32-ard.yaml index 7e6658e20e..2e091b905a 100644 --- a/tests/components/web_server/test.esp32-ard.yaml +++ b/tests/components/web_server/test.esp32-ard.yaml @@ -1 +1,8 @@ -<<: !include common_v2.yaml +packages: + web_server: !include common_v2.yaml + +web_server: + auth: + username: admin + password: password + type: digest diff --git a/tests/components/web_server/test.esp32-idf.yaml b/tests/components/web_server/test.esp32-idf.yaml index 24b292d0d6..2e091b905a 100644 --- a/tests/components/web_server/test.esp32-idf.yaml +++ b/tests/components/web_server/test.esp32-idf.yaml @@ -1,6 +1,8 @@ -<<: !include common_v2.yaml +packages: + web_server: !include common_v2.yaml web_server: auth: username: admin password: password + type: digest diff --git a/tests/components/web_server/test.esp8266-ard.yaml b/tests/components/web_server/test.esp8266-ard.yaml index 7e6658e20e..2e091b905a 100644 --- a/tests/components/web_server/test.esp8266-ard.yaml +++ b/tests/components/web_server/test.esp8266-ard.yaml @@ -1 +1,8 @@ -<<: !include common_v2.yaml +packages: + web_server: !include common_v2.yaml + +web_server: + auth: + username: admin + password: password + type: digest diff --git a/tests/components/web_server/test.rp2040-ard.yaml b/tests/components/web_server/test.rp2040-ard.yaml index 7e6658e20e..e4d50d7776 100644 --- a/tests/components/web_server/test.rp2040-ard.yaml +++ b/tests/components/web_server/test.rp2040-ard.yaml @@ -1 +1,8 @@ -<<: !include common_v2.yaml +packages: + web_server: !include common_v2.yaml + +web_server: + auth: + username: admin + password: password + type: basic diff --git a/tests/components/web_server/test_v1.esp32-ard.yaml b/tests/components/web_server/test_v1.esp32-ard.yaml index 389a930284..1d563de834 100644 --- a/tests/components/web_server/test_v1.esp32-ard.yaml +++ b/tests/components/web_server/test_v1.esp32-ard.yaml @@ -1 +1,2 @@ -<<: !include common_v1.yaml +packages: + web_server: !include common_v1.yaml diff --git a/tests/components/web_server/test_v1.esp32-idf.yaml b/tests/components/web_server/test_v1.esp32-idf.yaml index 389a930284..1d563de834 100644 --- a/tests/components/web_server/test_v1.esp32-idf.yaml +++ b/tests/components/web_server/test_v1.esp32-idf.yaml @@ -1 +1,2 @@ -<<: !include common_v1.yaml +packages: + web_server: !include common_v1.yaml diff --git a/tests/components/web_server/test_v3.esp32-ard.yaml b/tests/components/web_server/test_v3.esp32-ard.yaml index 00d05521e4..956a88bc68 100644 --- a/tests/components/web_server/test_v3.esp32-ard.yaml +++ b/tests/components/web_server/test_v3.esp32-ard.yaml @@ -1 +1,2 @@ -<<: !include common_v3.yaml +packages: + web_server: !include common_v3.yaml diff --git a/tests/components/web_server/validate.esp32-idf.yaml b/tests/components/web_server/validate.esp32-idf.yaml new file mode 100644 index 0000000000..e4d50d7776 --- /dev/null +++ b/tests/components/web_server/validate.esp32-idf.yaml @@ -0,0 +1,8 @@ +packages: + web_server: !include common_v2.yaml + +web_server: + auth: + username: admin + password: password + type: basic diff --git a/tests/components/wifi/validate-fast-connect-storage.esp32-idf.yaml b/tests/components/wifi/validate-fast-connect-storage.esp32-idf.yaml new file mode 100644 index 0000000000..93d223b908 --- /dev/null +++ b/tests/components/wifi/validate-fast-connect-storage.esp32-idf.yaml @@ -0,0 +1,7 @@ +# Exercises the dict form of fast_connect with RTC-backed preference storage. +wifi: + ssid: MySSID + password: password1 + fast_connect: + enabled: true + storage: rtc diff --git a/tests/components/wifi/validate-fast-connect-storage.esp8266-ard.yaml b/tests/components/wifi/validate-fast-connect-storage.esp8266-ard.yaml new file mode 100644 index 0000000000..070e22fd5b --- /dev/null +++ b/tests/components/wifi/validate-fast-connect-storage.esp8266-ard.yaml @@ -0,0 +1,7 @@ +# Exercises the dict form of fast_connect overriding the ESP8266 default (rtc) +# back to flash storage. +wifi: + ssid: MySSID + password: password1 + fast_connect: + storage: flash diff --git a/tests/components/wifi/validate-fast-connect-substitution.esp8266-ard.yaml b/tests/components/wifi/validate-fast-connect-substitution.esp8266-ard.yaml new file mode 100644 index 0000000000..f9fab8261a --- /dev/null +++ b/tests/components/wifi/validate-fast-connect-substitution.esp8266-ard.yaml @@ -0,0 +1,9 @@ +# fast_connect passed through a substitution arrives as a string ("false"), +# which must be accepted like the historic plain boolean form. +substitutions: + fast_connect_value: "false" + +wifi: + ssid: MySSID + password: password1 + fast_connect: ${fast_connect_value} diff --git a/tests/components/zigbee/common_esp32.yaml b/tests/components/zigbee/common_esp32.yaml index 787afc4476..6cac9c9e2a 100644 --- a/tests/components/zigbee/common_esp32.yaml +++ b/tests/components/zigbee/common_esp32.yaml @@ -5,13 +5,24 @@ binary_sensor: - platform: template name: "Garage Door Open 10" report: "default" + use_device_type: false - platform: template name: "Garage Door Open 12" report: "force" + endpoint: 1 + +sensor: + - platform: template + name: "Temperature Sensor" + lambda: return 10.0; + device_class: temperature + unit_of_measurement: "°C" + endpoint: 1 + use_device_type: true zigbee: model: zigbee_test - router: true + router: false power_source: MAINS_SINGLE_PHASE on_join: then: diff --git a/tests/components/zigbee/test-router.esp32-c6-idf.yaml b/tests/components/zigbee/test-router.esp32-c6-idf.yaml new file mode 100644 index 0000000000..228fe331e5 --- /dev/null +++ b/tests/components/zigbee/test-router.esp32-c6-idf.yaml @@ -0,0 +1,7 @@ +zigbee: + model: zigbee_test + router: true + power_source: MAINS_SINGLE_PHASE + on_join: + then: + - logger.log: "Joined network" diff --git a/tests/integration/README.md b/tests/integration/README.md index 4de08777b0..44d9e0d644 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -187,6 +187,7 @@ loop = asyncio.get_running_loop() states: dict[int, EntityState] = {} state_future: asyncio.Future[EntityState] = loop.create_future() + def on_state(state: EntityState) -> None: """This callback only receives NEW state changes, not initial states.""" states[state.key] = state @@ -195,6 +196,7 @@ def on_state(state: EntityState) -> None: if not state_future.done(): state_future.set_result(state) + # Get entities and set up state synchronization entities, services = await client.list_entities_services() initial_state_helper = InitialStateHelper(entities) @@ -228,6 +230,7 @@ loop = asyncio.get_running_loop() states: dict[int, EntityState] = {} state_future: asyncio.Future[EntityState] = loop.create_future() + def on_state(state: EntityState) -> None: states[state.key] = state # Check for specific condition using isinstance @@ -235,6 +238,7 @@ def on_state(state: EntityState) -> None: if not state_future.done(): state_future.set_result(state) + client.subscribe_states(on_state) # Wait for state with timeout @@ -263,11 +267,13 @@ entity_count = 50 received_states: set[int] = set() all_states_future: asyncio.Future[bool] = loop.create_future() + def on_state(state: EntityState) -> None: received_states.add(state.key) if len(received_states) >= entity_count and not all_states_future.done(): all_states_future.set_result(True) + client.subscribe_states(on_state) await asyncio.wait_for(all_states_future, timeout=10.0) ``` @@ -367,6 +373,7 @@ service_future = loop.create_future() connected_pattern = re.compile(r"Client .* connected from") service_pattern = re.compile(r"Service called") + def check_output(line: str) -> None: """Check log output for expected messages.""" if not connected_future.done() and connected_pattern.search(line): @@ -374,6 +381,7 @@ def check_output(line: str) -> None: elif not service_future.done() and service_pattern.search(line): service_future.set_result(True) + async with run_compiled(yaml_config, line_callback=check_output): async with api_client_connected() as client: # Wait for specific log message diff --git a/tests/integration/fixtures/api_homeassistant_action_no_subscriber.yaml b/tests/integration/fixtures/api_homeassistant_action_no_subscriber.yaml new file mode 100644 index 0000000000..26791e2cd1 --- /dev/null +++ b/tests/integration/fixtures/api_homeassistant_action_no_subscriber.yaml @@ -0,0 +1,30 @@ +esphome: + name: test-ha-action-no-subscriber + friendly_name: Home Assistant Action No Subscriber Test + on_boot: + # Fires before any client is connected - dropped with a warning. + - homeassistant.action: + action: test.boot_action + +host: + +api: + on_client_connected: + # Fires at authentication time, before the client has subscribed to + # Home Assistant actions - dropped with a warning. + - homeassistant.action: + action: test.connected_action + +logger: + level: DEBUG + +button: + - platform: template + name: Send Action Button + id: send_action_button + on_press: + # Pressed only after the client has subscribed - must be delivered. + - homeassistant.action: + action: test.button_action + data: + value: subscribed diff --git a/tests/integration/fixtures/api_zero_psk_provisioning.yaml b/tests/integration/fixtures/api_zero_psk_provisioning.yaml new file mode 100644 index 0000000000..1bb2a43e71 --- /dev/null +++ b/tests/integration/fixtures/api_zero_psk_provisioning.yaml @@ -0,0 +1,6 @@ +esphome: + name: zero-psk-provision-test +host: +api: + encryption: +logger: diff --git a/tests/integration/fixtures/api_zero_psk_provisioning_plaintext.yaml b/tests/integration/fixtures/api_zero_psk_provisioning_plaintext.yaml new file mode 100644 index 0000000000..a798c038d7 --- /dev/null +++ b/tests/integration/fixtures/api_zero_psk_provisioning_plaintext.yaml @@ -0,0 +1,6 @@ +esphome: + name: zero-psk-plaintext-test +host: +api: + encryption: +logger: diff --git a/tests/integration/fixtures/blocking_warning_log_time_not_charged_to_next_operation.yaml b/tests/integration/fixtures/blocking_warning_log_time_not_charged_to_next_operation.yaml new file mode 100644 index 0000000000..2f30a07d0d --- /dev/null +++ b/tests/integration/fixtures/blocking_warning_log_time_not_charged_to_next_operation.yaml @@ -0,0 +1,66 @@ +esphome: + name: blocking-warning-cascade + on_boot: + then: + - script.execute: blocking_60 + - script.execute: blocking_90 + - script.execute: blocking_120 + +host: + +api: + +logger: + level: DEBUG + on_message: + level: WARN + then: + - lambda: |- + uint32_t injected_delay = 0; + if (strstr(message, "blocking_60 took a long time") != nullptr) { + injected_delay = 60; + } else if (strstr(message, "blocking_90 took a long time") != nullptr) { + injected_delay = 90; + } else if (strstr(message, "blocking_120 took a long time") != nullptr) { + injected_delay = 120; + } + if (injected_delay != 0) { + id(injected_delay_total) += injected_delay; + const uint32_t start = millis(); + while (millis() - start < injected_delay) { + } + } + +globals: + - id: injected_delay_total + type: uint32_t + initial_value: "0" + +script: + - id: blocking_60 + then: + - delay: 20ms + - lambda: |- + const uint32_t start = millis(); + while (millis() - start < 80) { + } + + - id: blocking_90 + then: + - delay: 300ms + - lambda: |- + const uint32_t start = millis(); + while (millis() - start < 80) { + } + + - id: blocking_120 + then: + - delay: 600ms + - lambda: |- + const uint32_t start = millis(); + while (millis() - start < 80) { + } + - delay: 200ms + - logger.log: + format: "BLOCKING_WARNING_CASCADE_TEST_COMPLETE total=%u" + args: [id(injected_delay_total)] diff --git a/tests/integration/fixtures/light_binary_effect_off_phase.yaml b/tests/integration/fixtures/light_binary_effect_off_phase.yaml new file mode 100644 index 0000000000..c5c74001c3 --- /dev/null +++ b/tests/integration/fixtures/light_binary_effect_off_phase.yaml @@ -0,0 +1,29 @@ +esphome: + name: light-binary-effect-off +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: binary_output + type: binary + write_action: + - logger.log: + format: "BINARY_OUTPUT:%s" + args: [YESNO(state)] + +light: + - platform: binary + name: "Test Binary Light" + id: test_binary_light + output: binary_output + effects: + - strobe: + name: "Fast Strobe" + colors: + - state: true + duration: 50ms + - state: false + duration: 50ms diff --git a/tests/integration/fixtures/light_binary_zero_brightness_is_recoverable.yaml b/tests/integration/fixtures/light_binary_zero_brightness_is_recoverable.yaml new file mode 100644 index 0000000000..1db8b44fd1 --- /dev/null +++ b/tests/integration/fixtures/light_binary_zero_brightness_is_recoverable.yaml @@ -0,0 +1,21 @@ +esphome: + name: light-binary-zero-bright +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: binary_output + type: binary + write_action: + - logger.log: + format: "BINARY_OUTPUT:%s" + args: [YESNO(state)] + +light: + - platform: binary + name: "Test Binary Light" + id: test_binary_light + output: binary_output diff --git a/tests/integration/fixtures/light_effect_zero_brightness.yaml b/tests/integration/fixtures/light_effect_zero_brightness.yaml new file mode 100644 index 0000000000..b98bed84db --- /dev/null +++ b/tests/integration/fixtures/light_effect_zero_brightness.yaml @@ -0,0 +1,35 @@ +esphome: + name: light-effect-zero-bright +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: pulse_output + type: float + write_action: + - logger.log: + format: "PULSE_OUTPUT:%.4f" + args: [state] + +light: + - platform: monochromatic + name: "Test Pulse Light" + id: test_pulse_light + output: pulse_output + effects: + - pulse: + name: "Fast Pulse" + transition_length: 20ms + update_interval: 50ms + min_brightness: 0% + max_brightness: 100% + - strobe: + name: "Fast Strobe" + colors: + - state: true + duration: 50ms + - state: false + duration: 50ms diff --git a/tests/integration/fixtures/light_initial_state.yaml b/tests/integration/fixtures/light_initial_state.yaml index 2654c76aa0..052de0a4e5 100644 --- a/tests/integration/fixtures/light_initial_state.yaml +++ b/tests/integration/fixtures/light_initial_state.yaml @@ -21,6 +21,11 @@ output: type: float write_action: - lambda: "" + - platform: template + id: test_restore_and_on_output + type: float + write_action: + - lambda: "" light: - platform: rgb @@ -37,3 +42,16 @@ light: red: 1.0 green: 0.5 blue: 0.0 + + - platform: monochromatic + name: "Test Restore And On Light" + id: test_restore_and_on_light + output: test_restore_and_on_output + restore_mode: RESTORE_AND_ON + # Simulates a stale/persisted zero brightness: RESTORE_AND_ON always forces the light + # on at boot regardless of the recovered state, so a leftover brightness of 0 must not + # leave the light on-but-invisible. + initial_state: + color_mode: BRIGHTNESS + state: false + brightness: 0% diff --git a/tests/integration/fixtures/scheduler_numeric_id_test.yaml b/tests/integration/fixtures/scheduler_numeric_id_test.yaml index 25decf20f5..ae95e095f6 100644 --- a/tests/integration/fixtures/scheduler_numeric_id_test.yaml +++ b/tests/integration/fixtures/scheduler_numeric_id_test.yaml @@ -18,9 +18,6 @@ globals: - id: interval_counter type: int initial_value: '0' - - id: retry_counter - type: int - initial_value: '0' - id: defer_counter type: int initial_value: '0' @@ -118,29 +115,7 @@ script: id(timeout_counter) += 1; }); - // Test 10: set_retry with numeric ID - App.scheduler.set_retry(component1, 6001U, 50, 3, - [](uint8_t retry_countdown) { - id(retry_counter)++; - ESP_LOGI("test", "Numeric retry 6001 attempt %d (countdown=%d)", - id(retry_counter), retry_countdown); - if (id(retry_counter) >= 2) { - ESP_LOGI("test", "Numeric retry 6001 done"); - return RetryResult::DONE; - } - return RetryResult::RETRY; - }); - - // Test 11: cancel_retry with numeric ID - App.scheduler.set_retry(component1, 6002U, 100, 5, - [](uint8_t retry_countdown) { - ESP_LOGE("test", "ERROR: Numeric retry 6002 should have been cancelled"); - return RetryResult::RETRY; - }); - App.scheduler.cancel_retry(component1, 6002U); - ESP_LOGI("test", "Cancelled numeric retry 6002"); - - // Test 12: defer with numeric ID (Component method) + // Test 10: defer with numeric ID (Component method) class TestDeferComponent : public Component { public: void test_defer_methods() { @@ -161,7 +136,7 @@ script: static TestDeferComponent test_defer_component; test_defer_component.test_defer_methods(); - // Test 13: cancel_defer with numeric ID (Component method) + // Test 11: cancel_defer with numeric ID (Component method) class TestCancelDeferComponent : public Component { public: void test_cancel_defer() { @@ -181,8 +156,8 @@ script: - id: report_results then: - lambda: |- - ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d, Defers: %d", - id(timeout_counter), id(interval_counter), id(retry_counter), id(defer_counter)); + ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Defers: %d", + id(timeout_counter), id(interval_counter), id(defer_counter)); sensor: - platform: template diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml deleted file mode 100644 index cdf71152bd..0000000000 --- a/tests/integration/fixtures/scheduler_retry_test.yaml +++ /dev/null @@ -1,287 +0,0 @@ -esphome: - debug_scheduler: true # Enable scheduler leak detection - name: scheduler-retry-test - on_boot: - priority: -100 - then: - - logger.log: "Starting scheduler retry tests" - # Run all tests sequentially with delays - - script.execute: run_all_tests - -host: -api: -logger: - level: VERY_VERBOSE - -globals: - - id: simple_retry_counter - type: int - initial_value: '0' - - id: backoff_retry_counter - type: int - initial_value: '0' - - id: backoff_last_attempt_time - type: uint32_t - initial_value: '0' - - id: immediate_done_counter - type: int - initial_value: '0' - - id: cancel_retry_counter - type: int - initial_value: '0' - - id: empty_name_retry_counter - type: int - initial_value: '0' - - id: script_retry_counter - type: int - initial_value: '0' - - id: multiple_same_name_counter - type: int - initial_value: '0' - - id: const_char_retry_counter - type: int - initial_value: '0' - - id: static_char_retry_counter - type: int - initial_value: '0' - -# Using different component types for each test to ensure isolation -sensor: - - platform: template - name: Simple Retry Test Sensor - id: simple_retry_sensor - lambda: return 1.0; - update_interval: never - - - platform: template - name: Backoff Retry Test Sensor - id: backoff_retry_sensor - lambda: return 2.0; - update_interval: never - - - platform: template - name: Immediate Done Test Sensor - id: immediate_done_sensor - lambda: return 3.0; - update_interval: never - -binary_sensor: - - platform: template - name: Cancel Retry Test Binary Sensor - id: cancel_retry_binary_sensor - lambda: return false; - - - platform: template - name: Empty Name Test Binary Sensor - id: empty_name_binary_sensor - lambda: return true; - -switch: - - platform: template - name: Script Retry Test Switch - id: script_retry_switch - optimistic: true - - - platform: template - name: Multiple Same Name Test Switch - id: multiple_same_name_switch - optimistic: true - -script: - - id: run_all_tests - then: - # Test 1: Simple retry - - logger.log: "=== Test 1: Simple retry ===" - - lambda: |- - auto *component = id(simple_retry_sensor); - App.scheduler.set_retry(component, "simple_retry", 50, 3, - [](uint8_t retry_countdown) { - id(simple_retry_counter)++; - ESP_LOGI("test", "Simple retry attempt %d (countdown=%d)", - id(simple_retry_counter), retry_countdown); - - if (id(simple_retry_counter) >= 2) { - ESP_LOGI("test", "Simple retry succeeded on attempt %d", id(simple_retry_counter)); - return RetryResult::DONE; - } - return RetryResult::RETRY; - }); - - # Test 2: Backoff retry - - logger.log: "=== Test 2: Retry with backoff ===" - - lambda: |- - auto *component = id(backoff_retry_sensor); - - App.scheduler.set_retry(component, "backoff_retry", 50, 4, - [](uint8_t retry_countdown) { - id(backoff_retry_counter)++; - uint32_t now = millis(); - uint32_t interval = 0; - - // Only calculate interval after first attempt - if (id(backoff_retry_counter) > 1) { - interval = now - id(backoff_last_attempt_time); - } - id(backoff_last_attempt_time) = now; - - ESP_LOGI("test", "Backoff retry attempt %d (countdown=%d, interval=%dms)", - id(backoff_retry_counter), retry_countdown, interval); - - if (id(backoff_retry_counter) == 1) { - ESP_LOGI("test", "First call was immediate"); - } else if (id(backoff_retry_counter) == 2) { - ESP_LOGI("test", "Second call interval: %dms (expected ~50ms)", interval); - } else if (id(backoff_retry_counter) == 3) { - ESP_LOGI("test", "Third call interval: %dms (expected ~100ms)", interval); - } else if (id(backoff_retry_counter) == 4) { - ESP_LOGI("test", "Fourth call interval: %dms (expected ~200ms)", interval); - ESP_LOGI("test", "Backoff retry completed"); - return RetryResult::DONE; - } - - return RetryResult::RETRY; - }, 2.0f); - - # Test 3: Immediate done - - logger.log: "=== Test 3: Immediate done ===" - - lambda: |- - auto *component = id(immediate_done_sensor); - App.scheduler.set_retry(component, "immediate_done", 50, 5, - [](uint8_t retry_countdown) { - id(immediate_done_counter)++; - ESP_LOGI("test", "Immediate done retry called (countdown=%d)", retry_countdown); - return RetryResult::DONE; - }); - - # Test 4: Cancel retry - - logger.log: "=== Test 4: Cancel retry ===" - - lambda: |- - auto *component = id(cancel_retry_binary_sensor); - App.scheduler.set_retry(component, "cancel_test", 30, 10, - [](uint8_t retry_countdown) { - id(cancel_retry_counter)++; - ESP_LOGI("test", "Cancel test retry attempt %d", id(cancel_retry_counter)); - return RetryResult::RETRY; - }); - - // Cancel it after 100ms - App.scheduler.set_timeout(component, "cancel_timer", 100, []() { - bool cancelled = App.scheduler.cancel_retry(id(cancel_retry_binary_sensor), "cancel_test"); - ESP_LOGI("test", "Retry cancellation result: %s", cancelled ? "true" : "false"); - ESP_LOGI("test", "Cancel retry ran %d times before cancellation", id(cancel_retry_counter)); - }); - - # Test 5: Empty name retry - - logger.log: "=== Test 5: Empty name retry ===" - - lambda: |- - auto *component = id(empty_name_binary_sensor); - App.scheduler.set_retry(component, "", 100, 5, - [](uint8_t retry_countdown) { - id(empty_name_retry_counter)++; - ESP_LOGI("test", "Empty name retry attempt %d", id(empty_name_retry_counter)); - return RetryResult::RETRY; - }); - - // Try to cancel after 150ms - App.scheduler.set_timeout(component, "empty_cancel_timer", 150, []() { - bool cancelled = App.scheduler.cancel_retry(id(empty_name_binary_sensor), ""); - ESP_LOGI("test", "Empty name retry cancel result: %s", - cancelled ? "true" : "false"); - ESP_LOGI("test", "Empty name retry ran %d times", id(empty_name_retry_counter)); - }); - - # Test 6: Component method - - logger.log: "=== Test 6: Component::set_retry method ===" - - lambda: |- - class TestRetryComponent : public Component { - public: - void test_retry() { - this->set_retry(50, 3, - [](uint8_t retry_countdown) { - id(script_retry_counter)++; - ESP_LOGI("test", "Component retry attempt %d", id(script_retry_counter)); - if (id(script_retry_counter) >= 2) { - return RetryResult::DONE; - } - return RetryResult::RETRY; - }, 1.5f); - } - }; - - static TestRetryComponent test_component; - test_component.test_retry(); - - # Test 7: Multiple same name - - logger.log: "=== Test 7: Multiple retries with same name ===" - - lambda: |- - auto *component = id(multiple_same_name_switch); - - // Set first retry - App.scheduler.set_retry(component, "duplicate_retry", 100, 5, - [](uint8_t retry_countdown) { - id(multiple_same_name_counter) += 1; - ESP_LOGI("test", "First duplicate retry - should not run"); - return RetryResult::RETRY; - }); - - // Set second retry with same name (should cancel first) - App.scheduler.set_retry(component, "duplicate_retry", 50, 3, - [](uint8_t retry_countdown) { - id(multiple_same_name_counter) += 10; - ESP_LOGI("test", "Second duplicate retry attempt (counter=%d)", - id(multiple_same_name_counter)); - if (id(multiple_same_name_counter) >= 20) { - return RetryResult::DONE; - } - return RetryResult::RETRY; - }); - - # Test 8: Const char* overloads - - logger.log: "=== Test 8: Const char* overloads ===" - - lambda: |- - auto *component = id(simple_retry_sensor); - - // Test 8a: Direct string literal - App.scheduler.set_retry(component, "const_char_test", 30, 2, - [](uint8_t retry_countdown) { - id(const_char_retry_counter)++; - ESP_LOGI("test", "Const char retry %d", id(const_char_retry_counter)); - return RetryResult::DONE; - }); - - # Test 9: Static const char* variable - - logger.log: "=== Test 9: Static const char* ===" - - lambda: |- - auto *component = id(backoff_retry_sensor); - - static const char* STATIC_NAME = "static_retry_test"; - App.scheduler.set_retry(component, STATIC_NAME, 20, 1, - [](uint8_t retry_countdown) { - id(static_char_retry_counter)++; - ESP_LOGI("test", "Static const char retry %d", id(static_char_retry_counter)); - return RetryResult::DONE; - }); - - // Cancel with same static const char* - App.scheduler.set_timeout(component, "static_cancel", 10, []() { - static const char* STATIC_NAME = "static_retry_test"; - bool result = App.scheduler.cancel_retry(id(backoff_retry_sensor), STATIC_NAME); - ESP_LOGI("test", "Static cancel result: %s", result ? "true" : "false"); - }); - - # Wait for all tests to complete before reporting - - delay: 500ms - - # Final report - - logger.log: "=== Retry Test Results ===" - - lambda: |- - ESP_LOGI("test", "Simple retry counter: %d (expected 2)", id(simple_retry_counter)); - ESP_LOGI("test", "Backoff retry counter: %d (expected 4)", id(backoff_retry_counter)); - ESP_LOGI("test", "Immediate done counter: %d (expected 1)", id(immediate_done_counter)); - ESP_LOGI("test", "Cancel retry counter: %d (expected 2-4)", id(cancel_retry_counter)); - ESP_LOGI("test", "Empty name retry counter: %d (expected 1-2)", id(empty_name_retry_counter)); - ESP_LOGI("test", "Component retry counter: %d (expected 2)", id(script_retry_counter)); - ESP_LOGI("test", "Multiple same name counter: %d (expected 20+)", id(multiple_same_name_counter)); - ESP_LOGI("test", "Const char retry counter: %d (expected 1)", id(const_char_retry_counter)); - ESP_LOGI("test", "Static char retry counter: %d (expected 1)", id(static_char_retry_counter)); - ESP_LOGI("test", "All retry tests completed"); diff --git a/tests/integration/fixtures/use_address_runtime.yaml b/tests/integration/fixtures/use_address_runtime.yaml new file mode 100644 index 0000000000..29f3369285 --- /dev/null +++ b/tests/integration/fixtures/use_address_runtime.yaml @@ -0,0 +1,8 @@ +esphome: + name: use-address-runtime + +host: + +api: + +logger: diff --git a/tests/integration/fixtures/use_address_runtime_mac_suffix.yaml b/tests/integration/fixtures/use_address_runtime_mac_suffix.yaml new file mode 100644 index 0000000000..9785724cd5 --- /dev/null +++ b/tests/integration/fixtures/use_address_runtime_mac_suffix.yaml @@ -0,0 +1,9 @@ +esphome: + name: use-address-mac + name_add_mac_suffix: true + +host: + +api: + +logger: diff --git a/tests/integration/fixtures/wait_until_reentrant_restart.yaml b/tests/integration/fixtures/wait_until_reentrant_restart.yaml new file mode 100644 index 0000000000..337d0de837 --- /dev/null +++ b/tests/integration/fixtures/wait_until_reentrant_restart.yaml @@ -0,0 +1,86 @@ +esphome: + name: wait-until-reentrant-restart + +host: + +api: + actions: + - action: start_self_restart + then: + - script.execute: retry_script + - action: start_stop_during_wait + then: + - globals.set: + id: gate_open + value: 'false' + # num 0 is a blocker: its condition never becomes true, so it is still + # waiting (already checked and set aside) when num 1 stops the script - + # it must be cancelled, not restored, so its timeout must never fire + - script.execute: + id: waiter + num: 0 + - script.execute: + id: waiter + num: 1 + - script.execute: + id: waiter + num: 2 + - script.execute: + id: waiter + num: 3 + # Give all three instances time to queue in the same wait_until + - delay: 100ms + - globals.set: + id: gate_open + value: 'true' + - delay: 200ms + - logger.log: "stop test complete" + +logger: + level: DEBUG + +globals: + - id: attempt + type: int + initial_value: '0' + - id: gate_open + type: bool + initial_value: 'false' + +script: + # Self-restart retry pattern: when the wait_until times out, the rest of the + # script runs synchronously from inside the wait queue processing and restarts + # this same script - re-entering the same WaitUntilAction while it is still + # processing its queue. This used to corrupt the queue and crash. + - id: retry_script + mode: restart + then: + - wait_until: + condition: + lambda: 'return false;' + timeout: 20ms + - lambda: |- + id(attempt) += 1; + ESP_LOGD("test", "attempt %d done", id(attempt)); + - if: + condition: + lambda: 'return id(attempt) < 5;' + then: + - script.execute: retry_script + else: + - logger.log: "retry test complete" + + # Parallel waiters all queued in the same wait_until; the first one to pass the + # gate stops the script from its continuation, cancelling the other waiters + # while the queue is still being processed. + - id: waiter + mode: parallel + parameters: + num: int + then: + - wait_until: + condition: + lambda: 'return num != 0 && id(gate_open);' + timeout: 1s + - lambda: 'ESP_LOGD("test", "gate passed %d", num);' + - script.stop: waiter diff --git a/tests/integration/test_api_homeassistant_action_no_subscriber.py b/tests/integration/test_api_homeassistant_action_no_subscriber.py new file mode 100644 index 0000000000..9e7594e0ce --- /dev/null +++ b/tests/integration/test_api_homeassistant_action_no_subscriber.py @@ -0,0 +1,79 @@ +"""Integration test for Home Assistant actions fired without a subscriber. + +Home Assistant subscribes to device actions shortly after authenticating, while +on_client_connected (and similar triggers) fire right at authentication. Actions +fired before any client has subscribed cannot be delivered - they must produce a +warning in the log instead of vanishing silently. +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ButtonInfo, HomeassistantServiceCall +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_homeassistant_action_no_subscriber( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Undeliverable actions warn in the log; actions after subscribing arrive.""" + loop = asyncio.get_running_loop() + + boot_warning_future = loop.create_future() + connected_warning_future = loop.create_future() + button_action_future = loop.create_future() + + def check_output(line: str) -> None: + if ( + not boot_warning_future.done() + and "Home Assistant action 'test.boot_action' dropped; no client connected" + in line + ): + boot_warning_future.set_result(True) + if ( + not connected_warning_future.done() + and "Home Assistant action 'test.connected_action' dropped; " + "client has not subscribed to actions (yet)" + in line + ): + connected_warning_future.set_result(True) + + service_calls: list[HomeassistantServiceCall] = [] + + def on_service_call(service_call: HomeassistantServiceCall) -> None: + service_calls.append(service_call) + if ( + service_call.service == "test.button_action" + and not button_action_future.done() + ): + button_action_future.set_result(service_call) + + async with run_compiled(yaml_config, line_callback=check_output): + # The on_boot action fires with no client connected at all. + await asyncio.wait_for(boot_warning_future, timeout=10.0) + + async with api_client_connected() as client: + device_info = await client.device_info() + assert device_info.name == "test-ha-action-no-subscriber" + + # on_client_connected fired at authentication, before this client + # subscribed to Home Assistant actions. + await asyncio.wait_for(connected_warning_future, timeout=5.0) + + # After subscribing, actions must be delivered normally (and the + # dropped ones must not suddenly show up). + client.subscribe_service_calls(on_service_call) + + entities, _ = await client.list_entities_services() + button = next(e for e in entities if isinstance(e, ButtonInfo)) + client.button_command(button.key) + + button_call = await asyncio.wait_for(button_action_future, timeout=5.0) + assert button_call.data == {"value": "subscribed"} + assert [call.service for call in service_calls] == ["test.button_action"] diff --git a/tests/integration/test_api_zero_psk_provisioning.py b/tests/integration/test_api_zero_psk_provisioning.py new file mode 100644 index 0000000000..bcea2a2471 --- /dev/null +++ b/tests/integration/test_api_zero_psk_provisioning.py @@ -0,0 +1,127 @@ +"""Integration tests for provisioning the encryption key over a zero-PSK connection. + +A device with `api: encryption:` but no key accepts Noise handshakes using the +well-known all-zeros PSK. The ephemeral X25519 exchange protects the key from +passive sniffing while it is provisioned; plaintext provisioning still works +but is deprecated. +""" + +from __future__ import annotations + +import asyncio +import base64 + +from aioesphomeapi import InvalidEncryptionKeyAPIError, RequiresEncryptionAPIError +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + +# The well-known provisioning PSK: base64 of 32 zero bytes +ZERO_PSK = base64.b64encode(bytes(32)).decode() +# A real key to provision +NEW_KEY = base64.b64encode(b"n" * 32) +# Time for the device to activate a newly saved key (100ms timer plus margin) +KEY_ACTIVATION_DELAY = 0.5 + + +@pytest.fixture(autouse=True) +def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + """Keep host preferences per-test so every run starts unprovisioned.""" + monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs")) + + +@pytest.mark.asyncio +async def test_api_zero_psk_provisioning( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Exercise the reject paths, then provision a key over the zero-PSK channel.""" + async with run_compiled(yaml_config): + # --- Pre-provisioning reject paths (device state is unchanged) --- + + # A wrong (non-zero) PSK fails against the zero provisioning PSK + with pytest.raises(InvalidEncryptionKeyAPIError): + async with api_client_connected( + noise_psk=base64.b64encode(b"w" * 32).decode(), timeout=5 + ) as client: + await client.device_info() + + # A plaintext client and a zero-PSK client can be connected at the + # same time while the device is unprovisioned + async with ( + api_client_connected() as plaintext_client, + api_client_connected(noise_psk=ZERO_PSK) as noise_client, + ): + plaintext_info = await plaintext_client.device_info() + noise_info = await noise_client.device_info() + # Both transports advertise provisioning support so old and new + # clients can decide how to provision + assert plaintext_info.api_encryption_provisionable is True + assert noise_info.api_encryption_provisionable is True + + # The all-zeros key is reserved as the provisioning PSK and is + # rejected on both transports + zero_key = base64.b64encode(bytes(32)) + assert await noise_client.noise_encryption_set_key(zero_key) is False + assert await plaintext_client.noise_encryption_set_key(zero_key) is False + + # --- Provision over the zero-PSK channel --- + + # The unprovisioned device accepts the all-zeros PSK; the handshake's + # ephemeral-ephemeral DH encrypts everything that follows + async with api_client_connected(noise_psk=ZERO_PSK) as client: + device_info = await client.device_info() + assert device_info.name == "zero-psk-provision-test" + assert device_info.api_encryption_supported is True + assert device_info.api_encryption_provisionable is True + + assert await client.noise_encryption_set_key(NEW_KEY) is True + + # The device activates the new key shortly after responding + await asyncio.sleep(KEY_ACTIVATION_DELAY) + + # The new key now works, and the device is no longer provisionable + async with api_client_connected(noise_psk=NEW_KEY.decode()) as client: + device_info = await client.device_info() + assert device_info.name == "zero-psk-provision-test" + assert device_info.api_encryption_provisionable is False + + # The zero PSK no longer works + with pytest.raises(InvalidEncryptionKeyAPIError): + async with api_client_connected(noise_psk=ZERO_PSK, timeout=5) as client: + await client.device_info() + + # Plaintext no longer works + with pytest.raises(RequiresEncryptionAPIError): + async with api_client_connected(timeout=5) as client: + await client.device_info() + + +@pytest.mark.asyncio +async def test_api_zero_psk_provisioning_plaintext( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """The legacy plaintext provisioning path still works and warns.""" + log_lines: list[str] = [] + async with run_compiled(yaml_config, line_callback=log_lines.append): + async with api_client_connected() as client: + device_info = await client.device_info() + assert device_info.name == "zero-psk-plaintext-test" + + assert await client.noise_encryption_set_key(NEW_KEY) is True + + await asyncio.sleep(KEY_ACTIVATION_DELAY) + + # The deprecation warning was logged + assert any("deprecated" in line for line in log_lines) + + # The new key works; the zero PSK does not + async with api_client_connected(noise_psk=NEW_KEY.decode()) as client: + assert (await client.device_info()).name == "zero-psk-plaintext-test" + + with pytest.raises(InvalidEncryptionKeyAPIError): + async with api_client_connected(noise_psk=ZERO_PSK, timeout=5) as client: + await client.device_info() diff --git a/tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py b/tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py new file mode 100644 index 0000000000..e8b08f2265 --- /dev/null +++ b/tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py @@ -0,0 +1,63 @@ +"""Regression test for blocking-warning log time attribution.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + +WARN_PATTERN = re.compile( + r"(\S+) took a long time for an operation \((\d+) ms\), max is (\d+) ms" +) +COMPLETE_PATTERN = re.compile(r"BLOCKING_WARNING_CASCADE_TEST_COMPLETE total=(\d+)") +PRIMARY_SOURCES = {"blocking_60", "blocking_90", "blocking_120"} + + +@pytest.mark.asyncio +async def test_blocking_warning_log_time_not_charged_to_next_operation( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Synchronous warning-log delays must not be charged to the next operation.""" + loop = asyncio.get_running_loop() + complete = asyncio.Event() + warnings: list[tuple[str, int, int]] = [] + injected_delay_total = 0 + + def check_output(line: str) -> None: + nonlocal injected_delay_total + if match := WARN_PATTERN.search(line): + warnings.append((match.group(1), int(match.group(2)), int(match.group(3)))) + if match := COMPLETE_PATTERN.search(line): + injected_delay_total = int(match.group(1)) + loop.call_soon_threadsafe(complete.set) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + assert await client.device_info() is not None + await asyncio.wait_for(complete.wait(), timeout=10.0) + + assert injected_delay_total == 270, ( + f"Expected 270 ms of injected warning-log delay, got {injected_delay_total} ms" + ) + + primary_warnings = [ + warning for warning in warnings if warning[0] in PRIMARY_SOURCES + ] + assert {warning[0] for warning in primary_warnings} == PRIMARY_SOURCES, ( + f"Expected one real blocking warning from each test script, got: {warnings}" + ) + + secondary_warnings = [ + warning for warning in warnings if warning[0] not in PRIMARY_SOURCES + ] + assert not secondary_warnings, ( + "Warning-handler time was incorrectly charged to the next operation: " + f"{secondary_warnings}" + ) diff --git a/tests/integration/test_light_binary_effect_off_phase.py b/tests/integration/test_light_binary_effect_off_phase.py new file mode 100644 index 0000000000..571fca19f6 --- /dev/null +++ b/tests/integration/test_light_binary_effect_off_phase.py @@ -0,0 +1,207 @@ +"""Integration test verifying the off phase of an effect reaches an ON/OFF-only light. + +Regression test for https://github.com/esphome/esphome/issues/17873. A strobe effect +encodes its dark phase as `brightness = 0` while keeping `state = true`, so that the +effect keeps running instead of being stopped by an explicit turn-off. On a dimmable +light that works, because the output is driven by `state * brightness`. On a binary +light the dark phase used to be dropped, so the output stayed on forever. + +Effect ticks are published with `publish: false` (so Home Assistant isn't spammed with +every frame), so the effect's actual output can't be observed via API state broadcasts. +Instead, this test reads the output component's log lines, which are written on every +update regardless of the publish flag. + +The output log line is emitted strictly after the API state response: `perform()` +publishes inline, but the write is deferred to the next `LightState::loop()` iteration +and then has to cross the subprocess stdout pipe. So a future is armed *before* each +command and awaited afterwards, rather than reading the last observed value. +""" + +from __future__ import annotations + +import asyncio +import re +from typing import Any + +from aioesphomeapi import EntityState, LightState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + +OUTPUT_PATTERN = re.compile(r"BINARY_OUTPUT:(YES|NO)") + + +@pytest.mark.asyncio +async def test_light_binary_effect_off_phase( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """A strobe effect must drive a binary light's output both on and off.""" + loop = asyncio.get_running_loop() + observed: list[bool] = [] + pending: list[asyncio.Future[bool]] = [] + + def on_log_line(line: str) -> None: + if match := OUTPUT_PATTERN.search(line): + value = match.group(1) == "YES" + observed.append(value) + while pending: + future = pending.pop(0) + if not future.done(): + future.set_result(value) + break + + def arm_output() -> asyncio.Future[bool]: + """Arm a future for the next output write, before sending the command.""" + future: asyncio.Future[bool] = loop.create_future() + pending.append(future) + return future + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + light = next(e for e in entities if e.object_id == "test_binary_light") + + state_futures: dict[int, asyncio.Future[LightState]] = {} + + def on_state(state: EntityState) -> None: + if isinstance(state, LightState) and state.key in state_futures: + future = state_futures[state.key] + if not future.done(): + future.set_result(state) + + # ESPHome sends the current state of every entity right after connecting; drain + # that initial burst so it can't be mistaken for the response to a command below. + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + async def send_and_wait(timeout: float = 5.0, **kwargs: Any) -> LightState: + """Send a light command and wait for the matching state response.""" + state_futures[light.key] = loop.create_future() + client.light_command(key=light.key, **kwargs) + return await asyncio.wait_for(state_futures[light.key], timeout=timeout) + + # A plain turn-on must drive the output on -- brightness defaults to 100% and + # must not be mistaken for a dark phase. + output = arm_output() + state = await send_and_wait(state=True) + assert state.state is True + assert await asyncio.wait_for(output, timeout=5.0) is True, ( + "Plain turn-on did not switch the output on" + ) + + # Run the strobe effect; both phases must reach the output. + observed.clear() + state = await send_and_wait(effect="Fast Strobe") + assert state.effect == "Fast Strobe" + # Let several effect cycles run (each phase is 50ms in the fixture). + await asyncio.sleep(1.0) + + assert True in observed, ( + f"Strobe effect never switched the output on -- got {observed}" + ) + assert False in observed, ( + f"Strobe effect never switched the output off; its dark phase was lost -- " + f"got {observed}" + ) + + # Stopping the effect must leave the light usable. + state = await send_and_wait(effect="None") + assert state.effect == "None" + output = arm_output() + state = await send_and_wait(state=True) + assert state.state is True + assert await asyncio.wait_for(output, timeout=5.0) is True, ( + "Light stayed off after the effect stopped" + ) + + # An explicit turn-off still switches the output off. + output = arm_output() + state = await send_and_wait(state=False) + assert state.state is False + assert await asyncio.wait_for(output, timeout=5.0) is False, ( + "Turn-off did not switch the output off" + ) + + +@pytest.mark.asyncio +async def test_light_binary_zero_brightness_is_recoverable( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Zero brightness on an ON/OFF light must not leave it permanently stuck off. + + An ON/OFF light has no brightness capability, so `turn_on` with 0% brightness has + no representable "on but dark" state. It must switch the output off and report the + light as off, and a later plain turn-on must bring it back. + """ + loop = asyncio.get_running_loop() + pending: list[asyncio.Future[bool]] = [] + + def on_log_line(line: str) -> None: + if match := OUTPUT_PATTERN.search(line): + value = match.group(1) == "YES" + while pending: + future = pending.pop(0) + if not future.done(): + future.set_result(value) + break + + def arm_output() -> asyncio.Future[bool]: + future: asyncio.Future[bool] = loop.create_future() + pending.append(future) + return future + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + light = next(e for e in entities if e.object_id == "test_binary_light") + + state_futures: dict[int, asyncio.Future[LightState]] = {} + + def on_state(state: EntityState) -> None: + if isinstance(state, LightState) and state.key in state_futures: + future = state_futures[state.key] + if not future.done(): + future.set_result(state) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + async def send_and_wait(timeout: float = 5.0, **kwargs: Any) -> LightState: + state_futures[light.key] = loop.create_future() + client.light_command(key=light.key, **kwargs) + return await asyncio.wait_for(state_futures[light.key], timeout=timeout) + + output = arm_output() + state = await send_and_wait(state=True) + assert state.state is True + assert await asyncio.wait_for(output, timeout=5.0) is True + + # Turning on at 0% brightness has no representable "on but dark" state here, + # so the light must switch off and report itself as off. + output = arm_output() + state = await send_and_wait(state=True, brightness=0.0) + assert await asyncio.wait_for(output, timeout=5.0) is False, ( + "Zero brightness did not switch the output off" + ) + assert state.state is False, ( + "Light reported itself as on while its output was off" + ) + + # A plain turn-on must recover -- the stored zero brightness must not persist. + output = arm_output() + state = await send_and_wait(state=True) + assert state.state is True + assert await asyncio.wait_for(output, timeout=5.0) is True, ( + "Light was left permanently off by a zero-brightness turn-on" + ) diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py index 0eaf5af91b..b75e2fac62 100644 --- a/tests/integration/test_light_calls.py +++ b/tests/integration/test_light_calls.py @@ -322,6 +322,49 @@ async def test_light_calls( assert state.state is True assert state.brightness == pytest.approx(0.75) + # Test 31: Setting brightness to 0 without an explicit state implicitly turns + # the light off; turning it back on (without an explicit brightness) then + # restores full brightness so the light is visible again. + client.light_command(key=rgbcw_light.key, state=True, brightness=0.5) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.5) + + # Brightness 0 with no explicit state -> implicit turn-off + client.light_command(key=rgbcw_light.key, brightness=0.0) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is False + assert state.brightness == pytest.approx(0.0) + # Turning on without an explicit brightness restores it to full brightness + client.light_command(key=rgbcw_light.key, state=True) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(1.0) + + # Test 31b: An explicit turn-on with brightness 0 respects the explicit value and + # stays dark. Only a turn-on with no brightness specified (Test 31) restores + # visibility -- an explicit brightness request (e.g. from a light effect's dark + # phase) is never overridden. + client.light_command(key=rgbcw_light.key, state=True, brightness=0.0) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.0) + + # Test 32: Turning a light on when it already has nonzero brightness leaves + # the brightness unchanged (the reset only happens when brightness is 0). + client.light_command(key=rgbcw_light.key, state=True, brightness=0.4) + state = await wait_for_state_change(rgbcw_light.key) + assert state.brightness == pytest.approx(0.4) + + client.light_command(key=rgbcw_light.key, state=False) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is False + + client.light_command(key=rgbcw_light.key, state=True) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.4) + # Final cleanup - turn all lights off for light in lights: client.light_command( diff --git a/tests/integration/test_light_effect_zero_brightness.py b/tests/integration/test_light_effect_zero_brightness.py new file mode 100644 index 0000000000..6c386d4229 --- /dev/null +++ b/tests/integration/test_light_effect_zero_brightness.py @@ -0,0 +1,91 @@ +"""Integration test verifying light effects can dim to 0% brightness while staying on. + +Regression test for https://github.com/esphome/esphome/issues/17639, where PR #17103's +"make turn-on visible" logic in LightCall::validate_() also clobbered brightness set by a +running effect (e.g. pulse, strobe), forcing it back to 100% and breaking the dark phase +of those effects. + +Effect ticks are published with `publish: false` (so Home Assistant isn't spammed with +every frame), so the effect's actual output can't be observed via API state broadcasts. +Instead, this test reads the output component's log lines, which are written on every +update regardless of the publish flag. +""" + +from __future__ import annotations + +import asyncio +import re +from typing import Any + +from aioesphomeapi import EntityState, LightState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_effect_zero_brightness( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Pulse and strobe effects must be able to reach 0% brightness while the light stays on.""" + output_pattern = re.compile(r"PULSE_OUTPUT:([\d.]+)") + observed: list[float] = [] + + def on_log_line(line: str) -> None: + match = output_pattern.search(line) + if match: + observed.append(float(match.group(1))) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + light = next(e for e in entities if e.object_id == "test_pulse_light") + + state_futures: dict[int, asyncio.Future[LightState]] = {} + + def on_state(state: EntityState) -> None: + if isinstance(state, LightState) and state.key in state_futures: + future = state_futures[state.key] + if not future.done(): + future.set_result(state) + + # ESPHome sends the current state of every entity right after connecting; drain + # that initial burst so it can't be mistaken for the response to a command below. + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + async def send_and_wait(timeout: float = 5.0, **kwargs: Any) -> LightState: + """Send a light command and wait for the matching state response.""" + state_futures[light.key] = asyncio.get_running_loop().create_future() + client.light_command(key=light.key, **kwargs) + return await asyncio.wait_for(state_futures[light.key], timeout=timeout) + + # Turn the light on first so the effect starts from a known, visible state. + state = await send_and_wait(state=True, brightness=1.0) + assert state.state is True + assert state.brightness == pytest.approx(1.0) + + for effect_name in ("Fast Pulse", "Fast Strobe"): + observed.clear() + state = await send_and_wait(effect=effect_name) + assert state.effect == effect_name + # Let several effect cycles run (update_interval/duration is 50ms in the fixture). + await asyncio.sleep(1.0) + + assert observed, f"No output observed while running effect {effect_name!r}" + assert min(observed) == pytest.approx(0.0, abs=0.01), ( + f"Effect {effect_name!r} never dimmed to 0% brightness while the light " + f"stayed on -- got min={min(observed):.4f} (values: {observed})" + ) + assert max(observed) > 0.5, ( + f"Effect {effect_name!r} never reached full brightness -- " + f"got max={max(observed):.4f}" + ) + + client.light_command(key=light.key, effect="None") diff --git a/tests/integration/test_light_initial_state.py b/tests/integration/test_light_initial_state.py index f1cd96dbf0..657e273fe7 100644 --- a/tests/integration/test_light_initial_state.py +++ b/tests/integration/test_light_initial_state.py @@ -11,6 +11,14 @@ from .state_utils import InitialStateHelper, require_entity from .types import APIClientConnectedFactory, RunCompiledFunction +@pytest.fixture(autouse=True) +def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + """Keep host preferences per-test so RESTORE_AND_ON never loads a stale value left + behind by a previous run (host preferences otherwise persist to ~/.esphome/prefs, + keyed only by device name).""" + monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs")) + + @pytest.mark.asyncio async def test_light_initial_state( yaml_config: str, @@ -36,3 +44,10 @@ async def test_light_initial_state( assert state.red == pytest.approx(1.0, abs=0.01) assert state.green == pytest.approx(0.5, abs=0.01) assert state.blue == pytest.approx(0.0, abs=0.01) + + # Regression test: RESTORE_AND_ON always forces the light on at boot, even when + # the recovered/initial brightness was 0 -- it must never come up on-but-invisible. + restore_and_on_light = require_entity(entities, "test_restore_and_on_light") + restore_and_on_state = helper.initial_states[restore_and_on_light.key] + assert restore_and_on_state.state is True + assert restore_and_on_state.brightness == pytest.approx(1.0) diff --git a/tests/integration/test_scheduler_numeric_id_test.py b/tests/integration/test_scheduler_numeric_id_test.py index c1958db685..3591e3014e 100644 --- a/tests/integration/test_scheduler_numeric_id_test.py +++ b/tests/integration/test_scheduler_numeric_id_test.py @@ -18,7 +18,6 @@ async def test_scheduler_numeric_id_test( # Track counts timeout_count = 0 interval_count = 0 - retry_count = 0 defer_count = 0 # Events for each test completion @@ -32,8 +31,6 @@ async def test_scheduler_numeric_id_test( component_interval_fired = asyncio.Event() zero_id_timeout_fired = asyncio.Event() max_id_timeout_fired = asyncio.Event() - numeric_retry_done = asyncio.Event() - numeric_retry_cancelled = asyncio.Event() numeric_defer_7001_fired = asyncio.Event() numeric_defer_7002_fired = asyncio.Event() numeric_defer_cancelled = asyncio.Event() @@ -41,11 +38,10 @@ async def test_scheduler_numeric_id_test( # Track interval counts numeric_interval_count = 0 - numeric_retry_count = 0 def on_log_line(line: str) -> None: - nonlocal timeout_count, interval_count, retry_count, defer_count - nonlocal numeric_interval_count, numeric_retry_count + nonlocal timeout_count, interval_count, defer_count + nonlocal numeric_interval_count # Strip ANSI color codes clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) @@ -97,18 +93,6 @@ async def test_scheduler_numeric_id_test( max_id_timeout_fired.set() timeout_count += 1 - # Check for numeric retry tests - elif "Numeric retry 6001 attempt" in clean_line: - match = re.search(r"attempt (\d+)", clean_line) - if match: - numeric_retry_count = int(match.group(1)) - - elif "Numeric retry 6001 done" in clean_line: - numeric_retry_done.set() - - elif "Cancelled numeric retry 6002" in clean_line: - numeric_retry_cancelled.set() - # Check for numeric defer tests elif "Component numeric defer 7001 fired" in clean_line: numeric_defer_7001_fired.set() @@ -122,14 +106,13 @@ async def test_scheduler_numeric_id_test( # Check for final results elif "Final results" in clean_line: match = re.search( - r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+), Defers: (\d+)", + r"Timeouts: (\d+), Intervals: (\d+), Defers: (\d+)", clean_line, ) if match: timeout_count = int(match.group(1)) interval_count = int(match.group(2)) - retry_count = int(match.group(3)) - defer_count = int(match.group(4)) + defer_count = int(match.group(3)) final_results_logged.set() async with ( @@ -200,23 +183,6 @@ async def test_scheduler_numeric_id_test( except TimeoutError: pytest.fail("Max ID timeout did not fire within 0.5 seconds") - # Wait for numeric retry tests - try: - await asyncio.wait_for(numeric_retry_done.wait(), timeout=1.0) - except TimeoutError: - pytest.fail( - f"Numeric retry 6001 did not complete. Count: {numeric_retry_count}" - ) - - assert numeric_retry_count >= 2, ( - f"Expected at least 2 numeric retry attempts, got {numeric_retry_count}" - ) - - # Verify numeric retry was cancelled - assert numeric_retry_cancelled.is_set(), ( - "Numeric retry 6002 should have been cancelled" - ) - # Wait for numeric defer tests try: await asyncio.wait_for(numeric_defer_7001_fired.wait(), timeout=0.5) @@ -245,7 +211,4 @@ async def test_scheduler_numeric_id_test( assert interval_count >= 3, ( f"Expected at least 3 interval fires, got {interval_count}" ) - assert retry_count >= 2, ( - f"Expected at least 2 retry attempts, got {retry_count}" - ) assert defer_count >= 2, f"Expected at least 2 defer fires, got {defer_count}" diff --git a/tests/integration/test_scheduler_retry_test.py b/tests/integration/test_scheduler_retry_test.py deleted file mode 100644 index 910034e5bb..0000000000 --- a/tests/integration/test_scheduler_retry_test.py +++ /dev/null @@ -1,279 +0,0 @@ -"""Test scheduler retry functionality.""" - -import asyncio -import re - -import pytest - -from .types import APIClientConnectedFactory, RunCompiledFunction - - -@pytest.mark.asyncio -async def test_scheduler_retry_test( - yaml_config: str, - run_compiled: RunCompiledFunction, - api_client_connected: APIClientConnectedFactory, -) -> None: - """Test that scheduler retry functionality works correctly.""" - # Track test progress - simple_retry_done = asyncio.Event() - backoff_retry_done = asyncio.Event() - immediate_done_done = asyncio.Event() - cancel_retry_done = asyncio.Event() - empty_name_retry_done = asyncio.Event() - component_retry_done = asyncio.Event() - multiple_name_done = asyncio.Event() - const_char_done = asyncio.Event() - static_char_done = asyncio.Event() - test_complete = asyncio.Event() - - # Track retry counts - simple_retry_count = 0 - backoff_retry_count = 0 - immediate_done_count = 0 - cancel_retry_count = 0 - empty_name_retry_count = 0 - component_retry_count = 0 - multiple_name_count = 0 - const_char_retry_count = 0 - static_char_retry_count = 0 - - # Track specific test results - cancel_result = None - empty_cancel_result = None - backoff_intervals = [] - - def on_log_line(line: str) -> None: - nonlocal simple_retry_count, backoff_retry_count, immediate_done_count - nonlocal cancel_retry_count, empty_name_retry_count, component_retry_count - nonlocal multiple_name_count, const_char_retry_count, static_char_retry_count - nonlocal cancel_result, empty_cancel_result - - # Strip ANSI color codes - clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) - - # Simple retry test - if "Simple retry attempt" in clean_line: - if match := re.search(r"Simple retry attempt (\d+)", clean_line): - simple_retry_count = int(match.group(1)) - - elif "Simple retry succeeded on attempt" in clean_line: - simple_retry_done.set() - - # Backoff retry test - elif "Backoff retry attempt" in clean_line: - if match := re.search( - r"Backoff retry attempt (\d+).*interval=(\d+)ms", clean_line - ): - backoff_retry_count = int(match.group(1)) - interval = int(match.group(2)) - if backoff_retry_count > 1: # Skip first (immediate) call - backoff_intervals.append(interval) - - elif "Backoff retry completed" in clean_line: - backoff_retry_done.set() - - # Immediate done test - elif "Immediate done retry called" in clean_line: - immediate_done_count += 1 - immediate_done_done.set() - - # Cancel retry test - elif "Cancel test retry attempt" in clean_line: - cancel_retry_count += 1 - - elif "Retry cancellation result:" in clean_line: - cancel_result = "true" in clean_line - cancel_retry_done.set() - - # Empty name retry test - elif "Empty name retry attempt" in clean_line: - if match := re.search(r"Empty name retry attempt (\d+)", clean_line): - empty_name_retry_count = int(match.group(1)) - - elif "Empty name retry cancel result:" in clean_line: - empty_cancel_result = "true" in clean_line - - elif "Empty name retry ran" in clean_line: - empty_name_retry_done.set() - - # Component retry test - elif "Component retry attempt" in clean_line: - if match := re.search(r"Component retry attempt (\d+)", clean_line): - component_retry_count = int(match.group(1)) - if component_retry_count >= 2: - component_retry_done.set() - - # Multiple same name test - elif "Second duplicate retry attempt" in clean_line: - if match := re.search(r"counter=(\d+)", clean_line): - multiple_name_count = int(match.group(1)) - if multiple_name_count >= 20: - multiple_name_done.set() - - # Const char retry test - elif "Const char retry" in clean_line: - if match := re.search(r"Const char retry (\d+)", clean_line): - const_char_retry_count = int(match.group(1)) - const_char_done.set() - - # Static const char retry test - elif "Static const char retry" in clean_line: - if match := re.search(r"Static const char retry (\d+)", clean_line): - static_char_retry_count = int(match.group(1)) - static_char_done.set() - - elif "Static cancel result:" in clean_line: - # This is part of test 9, but we don't track it separately - pass - - # Test completion - elif "All retry tests completed" in clean_line: - test_complete.set() - - async with ( - run_compiled(yaml_config, line_callback=on_log_line), - api_client_connected() as client, - ): - # Verify we can connect - device_info = await client.device_info() - assert device_info is not None - assert device_info.name == "scheduler-retry-test" - - # Wait for simple retry test - try: - await asyncio.wait_for(simple_retry_done.wait(), timeout=1.0) - except TimeoutError: - pytest.fail( - f"Simple retry test did not complete. Count: {simple_retry_count}" - ) - - assert simple_retry_count == 2, ( - f"Expected 2 simple retry attempts, got {simple_retry_count}" - ) - - # Wait for backoff retry test - try: - await asyncio.wait_for(backoff_retry_done.wait(), timeout=3.0) - except TimeoutError: - pytest.fail( - f"Backoff retry test did not complete. Count: {backoff_retry_count}" - ) - - assert backoff_retry_count == 4, ( - f"Expected 4 backoff retry attempts, got {backoff_retry_count}" - ) - - # Verify backoff intervals (allowing for timing variations) - assert len(backoff_intervals) >= 2, ( - f"Expected at least 2 intervals, got {len(backoff_intervals)}" - ) - if len(backoff_intervals) >= 3: - # First interval should be ~50ms (very wide tolerance for heavy system load) - assert 20 <= backoff_intervals[0] <= 150, ( - f"First interval {backoff_intervals[0]}ms not ~50ms" - ) - # Second interval should be ~100ms (50ms * 2.0) - assert 50 <= backoff_intervals[1] <= 250, ( - f"Second interval {backoff_intervals[1]}ms not ~100ms" - ) - # Third interval should be ~200ms (100ms * 2.0) - assert 100 <= backoff_intervals[2] <= 500, ( - f"Third interval {backoff_intervals[2]}ms not ~200ms" - ) - - # Wait for immediate done test - try: - await asyncio.wait_for(immediate_done_done.wait(), timeout=3.0) - except TimeoutError: - pytest.fail( - f"Immediate done test did not complete. Count: {immediate_done_count}" - ) - - assert immediate_done_count == 1, ( - f"Expected 1 immediate done call, got {immediate_done_count}" - ) - - # Wait for cancel retry test - try: - await asyncio.wait_for(cancel_retry_done.wait(), timeout=3.0) - except TimeoutError: - pytest.fail( - f"Cancel retry test did not complete. Count: {cancel_retry_count}" - ) - - assert cancel_result is True, "Retry cancellation should have succeeded" - assert 2 <= cancel_retry_count <= 5, ( - f"Expected 2-5 cancel retry attempts before cancellation, got {cancel_retry_count}" - ) - - # Wait for empty name retry test - try: - await asyncio.wait_for(empty_name_retry_done.wait(), timeout=1.0) - except TimeoutError: - pytest.fail( - f"Empty name retry test did not complete. Count: {empty_name_retry_count}" - ) - - # Empty name retry should run at least once before being cancelled - assert 1 <= empty_name_retry_count <= 3, ( - f"Expected 1-3 empty name retry attempts, got {empty_name_retry_count}" - ) - assert empty_cancel_result is True, ( - "Empty name retry cancel should have succeeded" - ) - - # Wait for component retry test - try: - await asyncio.wait_for(component_retry_done.wait(), timeout=1.0) - except TimeoutError: - pytest.fail( - f"Component retry test did not complete. Count: {component_retry_count}" - ) - - assert component_retry_count >= 2, ( - f"Expected at least 2 component retry attempts, got {component_retry_count}" - ) - - # Wait for multiple same name test - try: - await asyncio.wait_for(multiple_name_done.wait(), timeout=1.0) - except TimeoutError: - pytest.fail( - f"Multiple same name test did not complete. Count: {multiple_name_count}" - ) - - # Should be 20+ (only second retry should run) - assert multiple_name_count >= 20, ( - f"Expected multiple name count >= 20 (second retry only), got {multiple_name_count}" - ) - - # Wait for const char retry test - try: - await asyncio.wait_for(const_char_done.wait(), timeout=1.0) - except TimeoutError: - pytest.fail( - f"Const char retry test did not complete. Count: {const_char_retry_count}" - ) - - assert const_char_retry_count == 1, ( - f"Expected 1 const char retry call, got {const_char_retry_count}" - ) - - # Wait for static char retry test - try: - await asyncio.wait_for(static_char_done.wait(), timeout=1.0) - except TimeoutError: - pytest.fail( - f"Static char retry test did not complete. Count: {static_char_retry_count}" - ) - - assert static_char_retry_count == 1, ( - f"Expected 1 static char retry call, got {static_char_retry_count}" - ) - - # Wait for test completion - try: - await asyncio.wait_for(test_complete.wait(), timeout=1.0) - except TimeoutError: - pytest.fail("Test did not complete within timeout") diff --git a/tests/integration/test_use_address_runtime.py b/tests/integration/test_use_address_runtime.py new file mode 100644 index 0000000000..a4cbbb9c5f --- /dev/null +++ b/tests/integration/test_use_address_runtime.py @@ -0,0 +1,73 @@ +"""Integration tests for the runtime-built use_address. + +The default ".local" address is no longer stored as a compile-time string; +it is built at runtime from the device name. This also fixes the logged address +when name_add_mac_suffix is enabled: the baked string used to miss the MAC +suffix, so it never matched the actual mDNS hostname. +""" + +from __future__ import annotations + +import asyncio + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679" +MAC_SUFFIX = "abf679" + + +@pytest.mark.asyncio +async def test_use_address_runtime( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """The API dump_config logs ".local" built from the device name.""" + address_seen = asyncio.Event() + + def check_output(line: str) -> None: + if "Address: use-address-runtime.local:" in line: + address_seen.set() + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "use-address-runtime" + + try: + await asyncio.wait_for(address_seen.wait(), timeout=10.0) + except TimeoutError: + pytest.fail("Did not log 'Address: use-address-runtime.local:'") + + +@pytest.mark.asyncio +async def test_use_address_runtime_mac_suffix( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """With name_add_mac_suffix the logged address includes the MAC suffix.""" + address_seen = asyncio.Event() + expected = f"Address: use-address-mac-{MAC_SUFFIX}.local:" + + def check_output(line: str) -> None: + if expected in line: + address_seen.set() + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == f"use-address-mac-{MAC_SUFFIX}" + + try: + await asyncio.wait_for(address_seen.wait(), timeout=10.0) + except TimeoutError: + pytest.fail(f"Did not log '{expected}'") diff --git a/tests/integration/test_wait_until_reentrant_restart.py b/tests/integration/test_wait_until_reentrant_restart.py new file mode 100644 index 0000000000..9c73339515 --- /dev/null +++ b/tests/integration/test_wait_until_reentrant_restart.py @@ -0,0 +1,89 @@ +"""Integration test for wait_until queue reentrancy. + +When a wait_until completes, the rest of the action chain runs synchronously +from inside the wait queue processing. That chain can re-enter the very same +WaitUntilAction - for example a script with mode: restart that executes itself +as a retry pattern, or a waiter that stops its own script. Both used to mutate +the std::list while it was being iterated, corrupting it and crashing the +device (Guru Meditation StoreProhibited in _M_transfer). +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_reentrant_restart( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that re-entering a wait_until from its own continuation is safe.""" + retry_complete = asyncio.Event() + stop_complete = asyncio.Event() + + attempt_pattern = re.compile(r"attempt (\d+) done") + gate_pattern = re.compile(r"gate passed (\d+)") + + attempts: list[int] = [] + gate_passed: list[int] = [] + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if mo := attempt_pattern.search(line): + attempts.append(int(mo.group(1))) + elif mo := gate_pattern.search(line): + gate_passed.append(int(mo.group(1))) + elif "retry test complete" in line: + retry_complete.set() + elif "stop test complete" in line: + stop_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "wait-until-reentrant-restart" + + _, services = await client.list_entities_services() + self_restart_service = next( + (s for s in services if s.name == "start_self_restart"), None + ) + assert self_restart_service is not None, "start_self_restart not found" + stop_service = next( + (s for s in services if s.name == "start_stop_during_wait"), None + ) + assert stop_service is not None, "start_stop_during_wait not found" + + # Scenario 1: the wait_until timeout continuation restarts its own + # script five times, re-entering the same wait_until each time. + await client.execute_service(self_restart_service, {}) + try: + await asyncio.wait_for(retry_complete.wait(), timeout=10.0) + except TimeoutError: + pytest.fail(f"Self-restart retry did not finish. Attempts: {attempts}") + assert attempts == [1, 2, 3, 4, 5], attempts + + # Scenario 2: the first waiter through the gate stops the script while + # the other waiters are still queued in the same wait_until; both the + # not-yet-checked waiters (2, 3) and the already-checked still-waiting + # blocker (0) must be cancelled, not fired. + await client.execute_service(stop_service, {}) + try: + await asyncio.wait_for(stop_complete.wait(), timeout=10.0) + except TimeoutError: + pytest.fail(f"Stop-during-wait did not finish. Gate passed: {gate_passed}") + assert gate_passed == [1], gate_passed + + # If the cancelled blocker had been kept, its 1s wait_until timeout + # would still fire - give it the chance and check it stays silent. + await asyncio.sleep(1.5) + assert gate_passed == [1], gate_passed diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 2f038155c0..a05b683a5f 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -2225,15 +2225,33 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> "esphome/components/libretiny/wifi_ln882x.cpp", determine_jobs.Platform.LN882X_ARD, ), - # RP2040 / Raspberry Pi Pico detection + # RP2 family detection — explicit chip names only. + # RP2040 chip: _rp2040.*, _pico.* (Pico / Pico W) ("esphome/components/gpio/gpio_rp2040.cpp", determine_jobs.Platform.RP2040_ARD), ("esphome/components/wifi/wifi_rp2040.cpp", determine_jobs.Platform.RP2040_ARD), ("esphome/components/i2c/i2c_pico.cpp", determine_jobs.Platform.RP2040_ARD), ("esphome/components/spi/spi_pico.cpp", determine_jobs.Platform.RP2040_ARD), ( - "tests/components/rp2040/test.rp2040-ard.yaml", + "tests/components/rp2/test.rp2040-ard.yaml", determine_jobs.Platform.RP2040_ARD, ), + # RP2350 chip: _rp2350.*, _pico2.* (Pico 2 / Pico 2 W) + ( + "esphome/components/foo/foo_rp2350.cpp", + determine_jobs.Platform.RP2350_ARD, + ), + ( + "esphome/components/wifi/wifi_pico2.cpp", + determine_jobs.Platform.RP2350_ARD, + ), + ( + "tests/components/rp2/test.rp2350-ard.yaml", + determine_jobs.Platform.RP2350_ARD, + ), + # Family-wide files (_rp2.*) intentionally do NOT get a hint — + # they apply to both RP2040 and RP2350 chips. + ("esphome/components/debug/debug_rp2.cpp", None), + ("esphome/components/logger/logger_rp2.h", None), # nRF52 / Zephyr detection ( "tests/components/logger/test.nrf52-adafruit.yaml", @@ -2280,6 +2298,11 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> "pico_i2c", "pico_spi", "rp2040_test_yaml", + "rp2350_cpp", + "pico2_cpp", + "rp2350_test_yaml", + "rp2_family_debug_no_hint", + "rp2_family_logger_h_no_hint", "nrf52_test_yaml", "nrf52_gpio", "zephyr_core", @@ -2970,3 +2993,53 @@ def test_main_force_all_off_uses_detection( assert output["component_test_count"] == 0 mock_determine_integration_tests.assert_called_once() mock_should_run_clang_tidy.assert_called_once() + + +# Every platform the memory impact analysis can select must produce an ELF that +# find_elf_path knows how to locate. The analysis fails the job when it cannot +# find one, so a platform with an unknown layout would turn a clean build red. +_MEMORY_IMPACT_ELF_LAYOUTS = { + # Native ESP-IDF toolchain (the esp32 default): /build/firmware.elf + "esp32-c6-idf": "build/firmware.elf", + "esp32-idf": "build/firmware.elf", + "esp32-c3-idf": "build/firmware.elf", + "esp32-s2-idf": "build/firmware.elf", + "esp32-s3-idf": "build/firmware.elf", + # PlatformIO: /.pioenvs//firmware.elf + "esp8266-ard": ".pioenvs/{name}/firmware.elf", + "rp2040-ard": ".pioenvs/{name}/firmware.elf", + "rp2350-ard": ".pioenvs/{name}/firmware.elf", + # LibreTiny: /.pioenvs//raw_firmware.elf + "bk72xx-ard": ".pioenvs/{name}/raw_firmware.elf", + "rtl87xx-ard": ".pioenvs/{name}/raw_firmware.elf", + "ln882x-ard": ".pioenvs/{name}/raw_firmware.elf", + # Zephyr: /.pioenvs//zephyr/[zephyr/]zephyr.elf + "nrf52-adafruit": ".pioenvs/{name}/zephyr/zephyr/zephyr.elf", +} + + +def test_memory_impact_platforms_have_known_elf_layout() -> None: + """Every selectable memory impact platform has a documented ELF layout. + + Adding a platform to the preference list without teaching find_elf_path + where its ELF lands would fail the memory impact job on a clean build. + """ + selectable = { + platform.value for platform in determine_jobs.MEMORY_IMPACT_PLATFORM_PREFERENCE + } + selectable.add(determine_jobs.MEMORY_IMPACT_FALLBACK_PLATFORM.value) + + assert selectable == set(_MEMORY_IMPACT_ELF_LAYOUTS) + + +def test_memory_impact_elf_layouts_are_found(tmp_path: Path) -> None: + """find_elf_path locates the ELF each memory impact platform produces.""" + from esphome.analyze_memory.toolchain import find_elf_path + + for platform, layout in _MEMORY_IMPACT_ELF_LAYOUTS.items(): + build_path = tmp_path / platform / ".esphome" / "build" / "mydevice" + elf = build_path / layout.format(name=build_path.name) + elf.parent.mkdir(parents=True) + elf.write_text("") + + assert find_elf_path(build_path) == elf, f"{platform} ELF not found" diff --git a/tests/script/test_docker_build.py b/tests/script/test_docker_build.py index 34bcc4e714..06dc21a92e 100644 --- a/tests/script/test_docker_build.py +++ b/tests/script/test_docker_build.py @@ -71,10 +71,12 @@ def test_branch_manifest_targets_ghcr_only( ) assert commands == [ - "docker buildx imagetools create " - "--tag ghcr.io/esphome/esphome-hassio:my-branch " - "ghcr.io/esphome/esphome-hassio-amd64:my-branch " - "ghcr.io/esphome/esphome-hassio-aarch64:my-branch" + ( + "docker buildx imagetools create " + "--tag ghcr.io/esphome/esphome-hassio:my-branch " + "ghcr.io/esphome/esphome-hassio-amd64:my-branch " + "ghcr.io/esphome/esphome-hassio-aarch64:my-branch" + ) ] diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 886d413ccf..43c4445dcf 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -79,6 +79,22 @@ def test_get_pr_number_from_github_env_event_file( assert result == "5678" +def test_get_github_event_data_decodes_utf8_regardless_of_locale( + monkeypatch: MonkeyPatch, tmp_path: Path +) -> None: + """The event payload is UTF-8; parsing must not depend on the platform + default encoding. On Windows the default is cp1252, which raised + UnicodeDecodeError as soon as a commit title carried non ASCII text.""" + event_file = tmp_path / "event.json" + event_data = {"head_commit": {"message": "Answer UNPAIR with Response… é"}} + event_file.write_bytes(json.dumps(event_data, ensure_ascii=False).encode("utf-8")) + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_file)) + + result = helpers._get_github_event_data() + + assert result == event_data + + def test_get_pr_number_from_github_env_no_pr( monkeypatch: MonkeyPatch, tmp_path: Path ) -> None: diff --git a/tests/script/test_merge_component_configs.py b/tests/script/test_merge_component_configs.py index 6ed1bd2c1e..27be3b5628 100644 --- a/tests/script/test_merge_component_configs.py +++ b/tests/script/test_merge_component_configs.py @@ -10,7 +10,10 @@ sys.path.insert(0, str((Path(__file__).parent / ".." / ".." / "script").resolve( import merge_component_configs # noqa: E402 +from esphome import yaml_util # noqa: E402 + deduplicate_by_id = merge_component_configs.deduplicate_by_id +prepare_component_body = merge_component_configs.prepare_component_body def test_identical_duplicate_ids_collapse() -> None: @@ -99,3 +102,56 @@ def test_nested_lists_are_checked() -> None: } with pytest.raises(ValueError, match="dup"): deduplicate_by_id(data) + + +def test_nested_package_includes_are_fully_expanded(tmp_path: Path) -> None: + """A package include that itself pulls in another package expands fully. + + Mirrors web_server's tests, where test.yaml includes common_v2, which + includes common (holding the wifi/network config). Without recursive + expansion the nested include is dropped and network config is lost. + """ + (tmp_path / "common.yaml").write_text("wifi:\n ssid: MySSID\n") + (tmp_path / "common_v2.yaml").write_text( + "packages:\n device_base: !include common.yaml\nweb_server:\n port: 8080\n" + ) + (tmp_path / "test.yaml").write_text( + "packages:\n web_server: !include common_v2.yaml\n" + "web_server:\n auth:\n username: admin\n" + ) + + comp_data = yaml_util.load_yaml(tmp_path / "test.yaml") + result = prepare_component_body(comp_data, "web_server", tmp_path) + + assert "packages" not in result + assert result["wifi"] == {"ssid": "MySSID"} + assert result["web_server"] == {"port": 8080, "auth": {"username": "admin"}} + + +def test_common_bus_package_is_left_for_caller(tmp_path: Path) -> None: + """Common bus packages are not expanded inline; the caller re-adds them.""" + comp_data = { + "packages": { + "i2c": {"sda": 21, "scl": 22}, + "device_base": {"wifi": {"ssid": "MySSID"}}, + }, + } + result = prepare_component_body(comp_data, "mycomp", tmp_path) + + # The bus package's body must not be merged in, and the packages key is + # dropped entirely for the caller to re-add the common bus package. + assert "packages" not in result + assert "sda" not in result + assert result["wifi"] == {"ssid": "MySSID"} + + +def test_list_style_packages_are_expanded(tmp_path: Path) -> None: + """List-style package includes are expanded and the key removed.""" + (tmp_path / "common.yaml").write_text("wifi:\n ssid: MySSID\n") + (tmp_path / "test.yaml").write_text("packages:\n - !include common.yaml\n") + + comp_data = yaml_util.load_yaml(tmp_path / "test.yaml") + result = prepare_component_body(comp_data, "mycomp", tmp_path) + + assert "packages" not in result + assert result["wifi"] == {"ssid": "MySSID"} diff --git a/tests/script/test_test_build_components.py b/tests/script/test_test_build_components.py new file mode 100644 index 0000000000..74e150380c --- /dev/null +++ b/tests/script/test_test_build_components.py @@ -0,0 +1,238 @@ +"""Unit tests for script/test_build_components.py logging helpers.""" + +from pathlib import Path +import sys + +import pytest + +# Add the script directory to the path so we can import the module under test. +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "script")) + +import test_build_components as tbc # noqa: E402 + + +class _FakeCompleted: + """Minimal stand-in for subprocess.CompletedProcess.""" + + def __init__(self, returncode: int) -> None: + self.returncode = returncode + + +@pytest.fixture +def _no_ci(monkeypatch: pytest.MonkeyPatch) -> None: + """Ensure GITHUB_ACTIONS is unset so group markers are suppressed.""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + +@pytest.fixture +def _ci(monkeypatch: pytest.MonkeyPatch) -> None: + """Pretend we are running inside GitHub Actions.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + + +def test_start_log_group_outside_ci_is_silent( + _no_ci: None, capsys: pytest.CaptureFixture[str] +) -> None: + tbc.start_log_group("hello") + assert capsys.readouterr().out == "" + + +def test_end_log_group_outside_ci_is_silent( + _no_ci: None, capsys: pytest.CaptureFixture[str] +) -> None: + tbc.end_log_group() + assert capsys.readouterr().out == "" + + +def test_start_log_group_in_ci_emits_marker( + _ci: None, capsys: pytest.CaptureFixture[str] +) -> None: + tbc.start_log_group("hello") + assert capsys.readouterr().out == "::group::hello\n" + + +def test_end_log_group_in_ci_emits_marker( + _ci: None, capsys: pytest.CaptureFixture[str] +) -> None: + tbc.end_log_group() + assert capsys.readouterr().out == "::endgroup::\n" + + +def _make_base_file(tmp_path: Path) -> Path: + base_file = tmp_path / "base.yaml" + base_file.write_text("esphome:\n name: $component_test_file\n") + return base_file + + +def test_run_esphome_test_wraps_output_in_group( + _ci: None, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """A passing single-component test is bracketed by group markers.""" + monkeypatch.setattr(tbc.subprocess, "run", lambda *a, **k: _FakeCompleted(0)) + repo_root = Path(tbc.__file__).parent.parent + test_file = repo_root / "tests" / "components" / "foo" / "test.esp32-idf.yaml" + + result = tbc.run_esphome_test( + component="foo", + test_file=test_file, + platform="esp32-idf", + platform_with_version="esp32-idf", + base_file=_make_base_file(tmp_path), + build_dir=tmp_path, + esphome_command="config", + continue_on_fail=True, + ) + + out = capsys.readouterr().out + assert result.success is True + assert "::group::[foo] [test] [esp32-idf]" in out + assert "::endgroup::" in out + # The header line is printed inside the group. + assert out.index("::group::") < out.index("> [foo]") < out.index("::endgroup::") + + +def test_run_esphome_test_closes_group_before_failure_report( + _ci: None, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """On a fail-fast failure the group closes before the reproduce report.""" + monkeypatch.setattr(tbc.subprocess, "run", lambda *a, **k: _FakeCompleted(1)) + repo_root = Path(tbc.__file__).parent.parent + test_file = repo_root / "tests" / "components" / "foo" / "test.esp32-idf.yaml" + + # continue_on_fail=False makes the failure raise after printing the + # reproduce block, which is the path that must stay outside the group. + with pytest.raises(tbc.subprocess.CalledProcessError): + tbc.run_esphome_test( + component="foo", + test_file=test_file, + platform="esp32-idf", + platform_with_version="esp32-idf", + base_file=_make_base_file(tmp_path), + build_dir=tmp_path, + esphome_command="config", + continue_on_fail=False, + ) + + out = capsys.readouterr().out + assert "::endgroup::" in out + assert "FAILED - Command to reproduce:" in out + # The group must be closed before the failure report is printed. + assert out.index("::endgroup::") < out.index("FAILED - Command to reproduce:") + + +def test_run_esphome_test_closes_group_when_subprocess_raises( + _ci: None, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """If the subprocess raises, the group is still closed (via finally).""" + + def _boom(*a: object, **k: object) -> None: + raise OSError("boom") + + monkeypatch.setattr(tbc.subprocess, "run", _boom) + repo_root = Path(tbc.__file__).parent.parent + test_file = repo_root / "tests" / "components" / "foo" / "test.esp32-idf.yaml" + + with pytest.raises(OSError, match="boom"): + tbc.run_esphome_test( + component="foo", + test_file=test_file, + platform="esp32-idf", + platform_with_version="esp32-idf", + base_file=_make_base_file(tmp_path), + build_dir=tmp_path, + esphome_command="config", + continue_on_fail=True, + ) + + assert "::endgroup::" in capsys.readouterr().out + + +def test_run_grouped_test_wraps_output_in_group( + _ci: None, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """A grouped test is bracketed by group markers listing its components.""" + monkeypatch.setattr(tbc.subprocess, "run", lambda *a, **k: _FakeCompleted(0)) + monkeypatch.setattr(tbc, "merge_component_configs", lambda **k: None) + + result = tbc.run_grouped_test( + components=["foo", "bar"], + platform="esp32-idf", + platform_with_version="esp32-idf", + base_file=_make_base_file(tmp_path), + build_dir=tmp_path, + tests_dir=tmp_path, + esphome_command="config", + continue_on_fail=True, + ) + + out = capsys.readouterr().out + assert result.success is True + assert "::group::[GROUPED: foo, bar] [esp32-idf]" in out + assert out.index("::group::") < out.index("> [GROUPED") < out.index("::endgroup::") + + +def test_run_grouped_test_closes_group_before_failure_report( + _ci: None, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """A fail-fast grouped failure closes the group before the report.""" + monkeypatch.setattr(tbc.subprocess, "run", lambda *a, **k: _FakeCompleted(1)) + monkeypatch.setattr(tbc, "merge_component_configs", lambda **k: None) + + with pytest.raises(tbc.subprocess.CalledProcessError): + tbc.run_grouped_test( + components=["foo", "bar"], + platform="esp32-idf", + platform_with_version="esp32-idf", + base_file=_make_base_file(tmp_path), + build_dir=tmp_path, + tests_dir=tmp_path, + esphome_command="config", + continue_on_fail=False, + ) + + out = capsys.readouterr().out + assert out.index("::endgroup::") < out.index("FAILED - Command to reproduce:") + + +def test_run_grouped_test_closes_group_when_subprocess_raises( + _ci: None, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """If the grouped subprocess raises, the group is still closed (finally).""" + + def _boom(*a: object, **k: object) -> None: + raise OSError("boom") + + monkeypatch.setattr(tbc.subprocess, "run", _boom) + monkeypatch.setattr(tbc, "merge_component_configs", lambda **k: None) + + with pytest.raises(OSError, match="boom"): + tbc.run_grouped_test( + components=["foo", "bar"], + platform="esp32-idf", + platform_with_version="esp32-idf", + base_file=_make_base_file(tmp_path), + build_dir=tmp_path, + tests_dir=tmp_path, + esphome_command="config", + continue_on_fail=True, + ) + + assert "::endgroup::" in capsys.readouterr().out diff --git a/tests/test_build_components/build_components_base.nrf52-microbit.yaml b/tests/test_build_components/build_components_base.nrf52-microbit.yaml new file mode 100644 index 0000000000..37728b4b64 --- /dev/null +++ b/tests/test_build_components/build_components_base.nrf52-microbit.yaml @@ -0,0 +1,16 @@ +esphome: + name: componenttestnrf52 + friendly_name: $component_name + +nrf52: + board: bbc_microbit + +logger: + level: VERY_VERBOSE + hardware_uart: UART0 + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.rp2040-ard.yaml b/tests/test_build_components/build_components_base.rp2040-ard.yaml index 4fb8d51333..4d26a38b69 100644 --- a/tests/test_build_components/build_components_base.rp2040-ard.yaml +++ b/tests/test_build_components/build_components_base.rp2040-ard.yaml @@ -2,7 +2,7 @@ esphome: name: componenttestrp2040ard friendly_name: $component_name -rp2040: +rp2: board: rpipicow logger: diff --git a/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml b/tests/test_build_components/build_components_base.rp2350-ard.yaml similarity index 97% rename from tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml rename to tests/test_build_components/build_components_base.rp2350-ard.yaml index 0922a5238e..5df1670862 100644 --- a/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml +++ b/tests/test_build_components/build_components_base.rp2350-ard.yaml @@ -2,7 +2,7 @@ esphome: name: componenttestrp2040pico2ard friendly_name: $component_name -rp2040: +rp2: board: rpipico2 logger: diff --git a/tests/test_build_components/common/README.md b/tests/test_build_components/common/README.md index 5e925d0067..a3c6f476e0 100644 --- a/tests/test_build_components/common/README.md +++ b/tests/test_build_components/common/README.md @@ -45,14 +45,13 @@ common/ ## How It Works ### Component Test Structure -Each component test includes the common bus config: +Each component test includes the common bus config and its own `common.yaml` through dict-style `packages:`. Always use packages for every include — the grouping scripts only understand dict-style packages, so list-style packages or top-level `<<:` merge keys prevent correct batch grouping. Key the bus package by the bus name and the component's `common.yaml` by the component name: ```yaml # tests/components/bh1750/test.esp32-idf.yaml packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml - -<<: !include common.yaml + bh1750: !include common.yaml ``` The common config provides: diff --git a/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml b/tests/test_build_components/common/spi/rp2350-ard.yaml similarity index 100% rename from tests/test_build_components/common/spi/rp2040-pico2-ard.yaml rename to tests/test_build_components/common/spi/rp2350-ard.yaml diff --git a/tests/test_build_components/common/test_display/test_display.yaml b/tests/test_build_components/common/test_display/test_display.yaml new file mode 100644 index 0000000000..986ab45223 --- /dev/null +++ b/tests/test_build_components/common/test_display/test_display.yaml @@ -0,0 +1,26 @@ +# Shared "test display" package for component tests. +# +# Provides a no-op display (id: test_display_screen) that uses no pins and no +# bus, so tests that only need a display to exist -- touchscreens especially -- +# don't have to instantiate a real driver and fight it over GPIOs. Include it +# like a common bus package; the consuming test does NOT need to declare +# external_components itself: +# +# packages: +# test_display: !include ../../test_build_components/common/test_display/test_display.yaml +# +# then point the touchscreen (or other display consumer) at `test_display_screen`. +# +# The test_display platform lives at tests/components/test_display/components/ and +# is loaded via external_components. The source path is written relative to the +# build directory (tests/test_build_components/build/), which every test -- +# standalone or grouped -- is generated into, so this always resolves to the +# component under tests/components/test_display/. +external_components: + - source: ../../components/test_display/components + components: [test_display] + +display: + - platform: test_display + id: test_display_screen + dimensions: 240x320 diff --git a/tests/unit_tests/analyze_memory/test_build_artifacts.py b/tests/unit_tests/analyze_memory/test_build_artifacts.py new file mode 100644 index 0000000000..ad2f8c2210 --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_build_artifacts.py @@ -0,0 +1,157 @@ +"""Tests for locating build artifacts across the supported toolchain layouts.""" + +from pathlib import Path + +import pytest + +from esphome.analyze_memory.toolchain import ( + find_elf_path, + find_idedata_path, + idedata_candidates, +) +from esphome.espidf.idedata import _cc_path_from_cxx +from esphome.platformio.toolchain import IDEData + + +def _make_build_dir(tmp_path: Path, name: str = "mydevice") -> Path: + """Create /.esphome/build/, mirroring a real data dir.""" + build_path = tmp_path / ".esphome" / "build" / name + build_path.mkdir(parents=True) + return build_path + + +def _touch(path: Path) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("", encoding="utf-8") + return path + + +def test_find_elf_path_native_esp_idf(tmp_path: Path) -> None: + """The native ESP-IDF toolchain writes the ELF under build/.""" + build_path = _make_build_dir(tmp_path) + elf = _touch(build_path / "build" / "firmware.elf") + + assert find_elf_path(build_path) == elf + + +def test_find_elf_path_platformio(tmp_path: Path) -> None: + """The PlatformIO toolchain writes the ELF under .pioenvs//.""" + build_path = _make_build_dir(tmp_path) + elf = _touch(build_path / ".pioenvs" / build_path.name / "firmware.elf") + + assert find_elf_path(build_path) == elf + + +def test_find_elf_path_libretiny(tmp_path: Path) -> None: + """The LibreTiny toolchain names the unwrapped ELF raw_firmware.elf.""" + build_path = _make_build_dir(tmp_path) + elf = _touch(build_path / ".pioenvs" / build_path.name / "raw_firmware.elf") + + assert find_elf_path(build_path) == elf + + +@pytest.mark.parametrize( + "relative_elf", + [ + # SDK < 2.9.2 + "zephyr/zephyr.elf", + # SDK >= 2.9.2 nests the artifacts one level deeper + "zephyr/zephyr/zephyr.elf", + ], +) +def test_find_elf_path_zephyr(tmp_path: Path, relative_elf: str) -> None: + """Zephyr (nRF52) keeps the ELF under .pioenvs//zephyr/.""" + build_path = _make_build_dir(tmp_path) + elf = _touch(build_path / ".pioenvs" / build_path.name / relative_elf) + + assert find_elf_path(build_path) == elf + + +def test_find_elf_path_missing(tmp_path: Path) -> None: + """An unknown layout resolves to None rather than a bogus path.""" + assert find_elf_path(_make_build_dir(tmp_path)) is None + + +def test_find_idedata_path_in_data_dir(tmp_path: Path) -> None: + """The idedata cache sits in the data dir that holds the build dir.""" + build_path = _make_build_dir(tmp_path) + idedata = _touch(tmp_path / ".esphome" / "idedata" / f"{build_path.name}.json") + + assert find_idedata_path(build_path) == idedata + + +def test_find_idedata_path_in_pioenvs(tmp_path: Path) -> None: + """Test builds may keep idedata alongside the PlatformIO env.""" + build_path = _make_build_dir(tmp_path) + idedata = _touch(build_path / ".pioenvs" / build_path.name / "idedata.json") + + assert find_idedata_path(build_path) == idedata + + +def test_find_idedata_path_missing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A missing idedata resolves to None.""" + # Keep the cwd/home fallbacks from finding an unrelated file on this machine + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + + assert find_idedata_path(_make_build_dir(tmp_path)) is None + + +def test_idedata_candidates_are_what_find_probes( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Every advertised candidate is one find_idedata_path actually accepts. + + The candidates are reported to the user when idedata is missing, so a list + that drifts from the lookup would send someone hunting in the wrong place. + """ + # Two candidates are relative to the cwd and to home; keep the test from + # writing into the real ones. + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + + build_path = _make_build_dir(tmp_path) + candidates = idedata_candidates(build_path) + + assert candidates, "no candidates advertised" + for candidate in candidates: + _touch(candidate) + assert find_idedata_path(build_path) == candidate + candidate.unlink() + + +@pytest.mark.parametrize( + ("cxx_path", "expected"), + [ + ("/tools/bin/xtensa-esp32-elf-g++", "/tools/bin/xtensa-esp32-elf-gcc"), + ("/tools/bin/riscv32-esp-elf-g++", "/tools/bin/riscv32-esp-elf-gcc"), + ( + r"C:\tools\bin\xtensa-esp32-elf-g++.exe", + r"C:\tools\bin\xtensa-esp32-elf-gcc.exe", + ), + # Nothing to rewrite; leave the path alone + ("/tools/bin/clang++", "/tools/bin/clang++"), + ], +) +def test_cc_path_from_cxx(cxx_path: str, expected: str) -> None: + """cc_path is derived from the C++ compiler that compile_commands.json names.""" + assert _cc_path_from_cxx(cxx_path) == expected + + +def test_native_idedata_resolves_toolchain_tools() -> None: + """The binutils paths are derived from the native ESP-IDF cc_path. + + Without cc_path, IDEData.objdump_path raises KeyError and the memory + analysis silently degrades to no component or symbol detail. + """ + idedata = IDEData( + { + "cc_path": _cc_path_from_cxx("/tools/bin/xtensa-esp32-elf-g++"), + "cxx_path": "/tools/bin/xtensa-esp32-elf-g++", + } + ) + + assert idedata.objdump_path == "/tools/bin/xtensa-esp32-elf-objdump" + assert idedata.readelf_path == "/tools/bin/xtensa-esp32-elf-readelf" diff --git a/tests/unit_tests/analyze_memory/test_ci_memory_impact_extract.py b/tests/unit_tests/analyze_memory/test_ci_memory_impact_extract.py new file mode 100644 index 0000000000..73a1c63e1a --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_ci_memory_impact_extract.py @@ -0,0 +1,57 @@ +"""Tests for script/ci_memory_impact_extract.py.""" + +import io +from pathlib import Path +import sys + +import pytest + +# Add script directory to path so we can import the module +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "script")) + +from ci_memory_impact_extract import main # noqa: E402 + +_COMPILE_OUTPUT = ( + "RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes)\n" + "Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes)\n" +) + + +@pytest.fixture(autouse=True) +def _no_github_output(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("GITHUB_OUTPUT", raising=False) + + +def _run(monkeypatch: pytest.MonkeyPatch, compile_output: str, argv: list[str]) -> int: + monkeypatch.setattr(sys, "stdin", io.StringIO(compile_output)) + monkeypatch.setattr(sys, "argv", ["ci_memory_impact_extract.py", *argv]) + return main() + + +def test_missing_detailed_analysis_fails( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A build with no usable ELF fails instead of posting a comment without detail.""" + build_dir = tmp_path / ".esphome" / "build" / "mydevice" + build_dir.mkdir(parents=True) + out_json = tmp_path / "analysis.json" + + rc = _run( + monkeypatch, + _COMPILE_OUTPUT, + ["--build-dir", str(build_dir), "--output-json", str(out_json)], + ) + + assert rc == 1 + # The totals are still written so the failure can be diagnosed from the artifact + assert out_json.is_file() + + +def test_undetected_build_dir_fails(monkeypatch: pytest.MonkeyPatch) -> None: + """Compile output without a build path cannot be analyzed, so it fails.""" + assert _run(monkeypatch, _COMPILE_OUTPUT, []) == 1 + + +def test_unparseable_output_fails(monkeypatch: pytest.MonkeyPatch) -> None: + """Output with no memory totals at all is still a failure.""" + assert _run(monkeypatch, "nothing useful here\n", []) == 1 diff --git a/tests/unit_tests/analyze_memory/test_ram_strings.py b/tests/unit_tests/analyze_memory/test_ram_strings.py new file mode 100644 index 0000000000..dda793a7f1 --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_ram_strings.py @@ -0,0 +1,123 @@ +"""Tests for RAM symbol analysis in the RAM strings analyzer.""" + +from pathlib import Path +from unittest.mock import patch + +from esphome.analyze_memory.ram_strings import RamStringsAnalyzer, SectionInfo + +# nm -S --size-sort output with the newlib lock mutexes: nine global +# symbols that are all aliases of two local StaticSemaphore_t objects. +NM_OUTPUT_WITH_ALIASES = """\ +3ffb4400 00000010 B small_symbol +3ffb43c8 00000054 B __lock___atexit_recursive_mutex +3ffb43c8 00000054 B __lock___env_recursive_mutex +3ffb43c8 00000054 B __lock___malloc_recursive_mutex +3ffb43c8 00000054 B __lock___sfp_recursive_mutex +3ffb43c8 00000054 B __lock___sinit_recursive_mutex +3ffb43c8 00000054 b s_common_recursive_mutex +3ffb441c 00000054 B __lock___arc4random_mutex +3ffb441c 00000054 B __lock___at_quick_exit_mutex +3ffb441c 00000054 B __lock___dd_hash_mutex +3ffb441c 00000054 B __lock___tz_mutex +3ffb441c 00000054 b s_common_mutex +""" + + +def _make_analyzer(tmp_path) -> RamStringsAnalyzer: + """Create an analyzer with a dummy ELF and a .dram0.bss section.""" + elf = tmp_path / "firmware.elf" + elf.write_bytes(b"\x7fELF") + analyzer = RamStringsAnalyzer(str(elf), platform="esp32") + analyzer.sections[".dram0.bss"] = SectionInfo(".dram0.bss", 0x3FFB0000, 0x10000) + return analyzer + + +def _run_symbol_analysis(analyzer: RamStringsAnalyzer, nm_output: str) -> None: + """Run _analyze_symbols with mocked nm output.""" + with ( + patch( + "esphome.analyze_memory.ram_strings.find_tool", + return_value="nm", + ), + patch.object(analyzer, "_run_command", return_value=nm_output), + ): + analyzer._analyze_symbols() + + +def test_aliased_symbols_counted_once(tmp_path: Path) -> None: + """Symbols sharing an address are one object, not one per name.""" + analyzer = _make_analyzer(tmp_path) + _run_symbol_analysis(analyzer, NM_OUTPUT_WITH_ALIASES) + + # Three distinct addresses, so three symbols + assert len(analyzer.ram_symbols) == 3 + total = sum(s.size for s in analyzer.ram_symbols) + assert total == 0x10 + 0x54 + 0x54 + + +def test_aliases_recorded_on_first_symbol(tmp_path: Path) -> None: + """Extra names at the same address are kept as aliases.""" + analyzer = _make_analyzer(tmp_path) + _run_symbol_analysis(analyzer, NM_OUTPUT_WITH_ALIASES) + + by_addr = {s.address: s for s in analyzer.ram_symbols} + assert len(by_addr[0x3FFB43C8].aliases) == 5 + assert len(by_addr[0x3FFB441C].aliases) == 4 + assert by_addr[0x3FFB4400].aliases == [] + assert "s_common_mutex" in by_addr[0x3FFB441C].aliases + + +def test_alias_count_shown_in_report(tmp_path: Path) -> None: + """The large symbols table notes how many aliases were merged.""" + analyzer = _make_analyzer(tmp_path) + _run_symbol_analysis(analyzer, NM_OUTPUT_WITH_ALIASES) + + report = analyzer.generate_report() + assert "(+5 aliases)" in report + assert "(+4 aliases)" in report + # Each lock name appears at most once in the report + assert report.count("__lock___") == 2 + + +def test_global_name_preferred_over_local_alias(tmp_path: Path) -> None: + """A global name becomes the primary even when nm lists a local first.""" + analyzer = _make_analyzer(tmp_path) + nm_output = """\ +3ffb43c8 00000054 b s_common_recursive_mutex +3ffb43c8 00000054 B __lock___atexit_recursive_mutex +3ffb43c8 00000054 B __lock___malloc_recursive_mutex +""" + _run_symbol_analysis(analyzer, nm_output) + + (symbol,) = analyzer.ram_symbols + assert symbol.name == "__lock___atexit_recursive_mutex" + assert symbol.sym_type == "B" + assert sorted(symbol.aliases) == [ + "__lock___malloc_recursive_mutex", + "s_common_recursive_mutex", + ] + + +def test_alias_note_survives_name_truncation(tmp_path: Path) -> None: + """Long names are truncated but the alias note is kept intact.""" + analyzer = _make_analyzer(tmp_path) + long_name = "a_very_long_symbol_name_that_exceeds_the_column_width_by_far" + nm_output = f"""\ +3ffb43c8 00000054 B {long_name} +3ffb43c8 00000054 B other_name +""" + _run_symbol_analysis(analyzer, nm_output) + + report = analyzer.generate_report() + row = next(line for line in report.splitlines() if "(+1 aliases)" in line) + name_column = row[:50].rstrip() + assert name_column.endswith("(+1 aliases)") + assert name_column.startswith("a_very_long_symbol_name") + + +def test_symbols_outside_ram_sections_skipped(tmp_path: Path) -> None: + """Symbols outside known RAM sections are ignored entirely.""" + analyzer = _make_analyzer(tmp_path) + nm_output = "40080000 00000100 B not_in_ram\n" + _run_symbol_analysis(analyzer, nm_output) + assert analyzer.ram_symbols == [] diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index 0f4444f719..f21549b48c 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -184,6 +184,18 @@ def test_get_component_cmakelists_compile_flags_excluded_from_link_opts() -> Non assert "-Wl,--gc-sections" in content +def test_get_component_cmakelists_globs_alternate_cpp_extensions() -> None: + """Both app_sources glob variants include .cc/.cxx/.c++ so vendored sources + are compiled, matching the extensions PlatformIO's builder globs by default.""" + CORE.build_flags = set() + from esphome.build_gen.espidf import get_component_cmakelists + + content = get_component_cmakelists() + for ext in ("cc", "cxx", "c++"): + assert content.count(f'"${{CMAKE_CURRENT_SOURCE_DIR}}/*.{ext}"') == 2 + assert content.count(f'"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.{ext}"') == 2 + + def test_get_project_cmakelists_emits_managed_components_property( tmp_path: Path, ) -> None: @@ -243,6 +255,7 @@ def test_get_project_cmakelists_no_cpp_standard(tmp_path: Path) -> None: patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"), patch.object(CORE, "name", "test"), patch.object(CORE, "cpp_standard", None), + patch.object(CORE, "cxx_build_flags", set()), ): from esphome.build_gen.espidf import get_project_cmakelists @@ -251,6 +264,28 @@ def test_get_project_cmakelists_no_cpp_standard(tmp_path: Path) -> None: assert "CXX_COMPILE_OPTIONS" not in content +def test_get_project_cmakelists_cxx_build_flags(tmp_path: Path) -> None: + """Flags registered via cg.add_cxx_build_flag() are appended to + CXX_COMPILE_OPTIONS (C++-only, GCC warns if they reach C compiles) + between include(project.cmake) and project().""" + with ( + patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"), + patch.object(CORE, "name", "test"), + patch.object(CORE, "cpp_standard", None), + patch.object(CORE, "cxx_build_flags", {"-Wno-volatile"}), + ): + from esphome.build_gen.espidf import get_project_cmakelists + + content = get_project_cmakelists(minimal=True) + + flag_line = 'idf_build_set_property(CXX_COMPILE_OPTIONS "-Wno-volatile" APPEND)' + assert flag_line in content + include_pos = content.index("tools/cmake/project.cmake") + flag_pos = content.index(flag_line) + project_pos = content.index("project(test)") + assert include_pos < flag_pos < project_pos + + def test_get_component_cmakelists_no_compile_features() -> None: """The C++ standard is pinned project-wide via CXX_COMPILE_OPTIONS in the top-level CMakeLists; the src component must not set its own.""" diff --git a/tests/unit_tests/build_gen/test_platformio.py b/tests/unit_tests/build_gen/test_platformio.py index 2ae3836a25..3df2fb1036 100644 --- a/tests/unit_tests/build_gen/test_platformio.py +++ b/tests/unit_tests/build_gen/test_platformio.py @@ -200,3 +200,32 @@ def test_get_ini_content_no_cpp_standard( content = platformio.get_ini_content() assert "-std=" not in content + + +def test_write_cxx_flags_script_emits_registered_flags( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Flags registered via cg.add_cxx_build_flag() are emitted as CXXFLAGS, + sorted, so they apply to C++ compiles only.""" + CORE.build_path = str(tmp_path) + monkeypatch.setattr(CORE, "cxx_build_flags", {"-Wno-volatile", "-Wno-deprecated"}) + + platformio.write_cxx_flags_script() + + content = (tmp_path / platformio.CXX_FLAGS_FILE_NAME).read_text() + assert ( + 'env.Append(CXXFLAGS=["-Wno-deprecated"])\n' + 'env.Append(CXXFLAGS=["-Wno-volatile"])\n' + ) in content + + +def test_write_cxx_flags_script_no_flags( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + CORE.build_path = str(tmp_path) + monkeypatch.setattr(CORE, "cxx_build_flags", set()) + + platformio.write_cxx_flags_script() + + content = (tmp_path / platformio.CXX_FLAGS_FILE_NAME).read_text() + assert "CXXFLAGS" not in content diff --git a/tests/unit_tests/components/api/test_client.py b/tests/unit_tests/components/api/test_client.py index 333ef70b22..4ebcecbfff 100644 --- a/tests/unit_tests/components/api/test_client.py +++ b/tests/unit_tests/components/api/test_client.py @@ -2,21 +2,22 @@ from __future__ import annotations -from unittest.mock import patch +from unittest.mock import AsyncMock, patch + +import pytest from esphome.components import esp32 from esphome.components.api import client as api_client -from esphome.core import EsphomeError +from esphome.const import CONF_PORT, KEY_CORE, KEY_TARGET_PLATFORM +from esphome.core import CORE, EsphomeError def test_decoder_swallows_esphome_error() -> None: """A failing stack-trace decode must not propagate. - on_log runs inside an asyncio protocol callback; if EsphomeError - escapes, the loop reports "Fatal error: protocol.data_received() - call failed.", tears the connection down, and ReconnectLogic loops - forever as the device replays the same crash trace on every - reconnect. + aioesphomeapi isolates exceptions raised by log handlers, so an + escaping one logs a full traceback for every line it fires on rather + than being reported once as an unavailable decoder. """ config = {"esphome": {"name": "test"}} @@ -43,6 +44,32 @@ def test_decoder_swallows_platform_handler_error() -> None: assert processor.backtrace_state is False +def test_decoder_swallows_non_esphome_error() -> None: + """Decoding failures that aren't EsphomeError must be contained too. + + A missing build directory surfaces as FileNotFoundError from the toolchain + subprocess. aioesphomeapi isolates it, so the session survives, but it logs + a traceback for every PC/BT line and decoding is never disabled, which + buries the crash dump the user is trying to read. + """ + config = {"esphome": {"name": "test"}} + + with patch.object( + esp32, + "process_stacktrace", + side_effect=FileNotFoundError( + 2, "No such file or directory", "/build/ol/build" + ), + ) as mock_process: + processor = api_client._LogLineProcessor(config, esp32.process_stacktrace) + processor.process_line("PC: 0x4010496e") + processor.process_line("BT0: 0x4010496e") + + # Disabled after the first failure rather than retried per backtrace line. + assert mock_process.call_count == 1 + assert processor.backtrace_state is False + + def test_decoder_warning_uses_fallback_for_empty_error(caplog) -> None: """_run_idedata raises EsphomeError with no message; the warning must show a useful explanation rather than empty parens. @@ -61,7 +88,7 @@ def test_decoder_warning_uses_fallback_for_empty_error(caplog) -> None: def test_decoder_short_circuits_after_failure() -> None: """After one failure, subsequent lines must not retry the decoder. - _decode_pc shells out to PlatformIO; a crash dump can contain many + _decode_pc shells out to the toolchain; a crash dump can contain many PC/BT lines and retrying the failing subprocess for each one would stall log streaming. """ @@ -112,3 +139,30 @@ def test_decoder_uses_platform_handler_when_provided() -> None: assert calls == [(config, "BT0: 0x4010496e", False)] assert mock_generic.called is False assert processor.backtrace_state is True + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("extra_config", "expected_deep_sleep"), + [({"deep_sleep": {}}, True), ({}, False)], +) +async def test_async_run_logs_passes_deep_sleep( + extra_config: dict, expected_deep_sleep: bool +) -> None: + """async_run_logs tells async_run whether the device deep sleeps, from the config.""" + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"} + config = {"esphome": {"name": "test"}, "api": {CONF_PORT: 6053}, **extra_config} + # async_run blocks forever after connecting; raise to unwind async_run_logs + # once we have captured how it was called. + sentinel = RuntimeError("stop the wait") + + with ( + patch.object( + api_client, "async_run", AsyncMock(side_effect=sentinel) + ) as mock_run, + patch.object(api_client, "APIClient"), + pytest.raises(RuntimeError, match="stop the wait"), + ): + await api_client.async_run_logs(config, ["1.2.3.4"]) + + assert mock_run.call_args.kwargs["deep_sleep"] is expected_deep_sleep diff --git a/tests/unit_tests/components/micro_wake_word/__init__.py b/tests/unit_tests/components/micro_wake_word/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/components/micro_wake_word/test_init.py b/tests/unit_tests/components/micro_wake_word/test_init.py new file mode 100644 index 0000000000..84371ab906 --- /dev/null +++ b/tests/unit_tests/components/micro_wake_word/test_init.py @@ -0,0 +1,169 @@ +"""Tests for the micro_wake_word model source validation and downloads.""" + +import json +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from esphome.components import micro_wake_word as mww +import esphome.config_validation as cv +from esphome.const import ( + CONF_FILE, + CONF_MODEL, + CONF_PATH, + CONF_REF, + CONF_TYPE, + CONF_URL, +) + + +@pytest.fixture +def mock_download_content_many() -> MagicMock: + """Patch the concurrent download helper so no network is involved.""" + with patch( + "esphome.components.micro_wake_word.external_files.download_content_many" + ) as m: + yield m + + +def test_shorthand_model_name_resolves_without_network( + mock_download_content_many: MagicMock, +) -> None: + config = mww._validate_source_shorthand("okay_nabu") + assert config[CONF_TYPE] == mww.TYPE_HTTP + assert config[CONF_URL] == ( + "https://github.com/esphome/micro-wake-word-models/raw/main/models/v2/okay_nabu.json" + ) + mock_download_content_many.assert_not_called() + + +def test_shorthand_git_with_ref_not_captured_as_model_name( + setup_core: Path, tmp_path: Path +) -> None: + repo_dir = tmp_path / "repo" + repo_dir.mkdir() + (repo_dir / "model.json").write_text("{}") + with patch( + "esphome.components.micro_wake_word.git.clone_or_update", + return_value=(repo_dir, None), + ): + config = mww._validate_source_shorthand("github://user/repo/model.json@main") + assert config[CONF_TYPE] == "git" + assert config[CONF_URL] == "https://github.com/user/repo.git" + assert config[CONF_FILE] == "model.json" + assert config[CONF_REF] == "main" + + +def test_shorthand_local_path_not_captured_as_model_name( + setup_core: Path, tmp_path: Path +) -> None: + manifest = tmp_path / "model.json" + manifest.write_text("{}") + config = mww.MODEL_SOURCE_SCHEMA(str(manifest)) + assert config[CONF_TYPE] == "local" + assert Path(config[CONF_PATH]) == manifest + + +@pytest.mark.parametrize( + "value", ["some/path/file", "name@ref", "bad:name", "okay_nabu\n", "héllo"] +) +def test_model_name_rejects_non_identifiers(value: str) -> None: + with pytest.raises(cv.Invalid): + mww._validate_source_model_name(value) + + +def _http_model(name: str) -> dict: + return { + CONF_MODEL: { + CONF_TYPE: mww.TYPE_HTTP, + CONF_URL: f"https://example.com/models/{name}.json", + } + } + + +def _write_manifest(model_config: dict, contents: str) -> Path: + path = mww._compute_local_file_path(model_config[CONF_MODEL]) + path.mkdir(parents=True, exist_ok=True) + manifest = path / "manifest.json" + manifest.write_text(contents) + return path + + +def test_download_http_models_batches_manifests_then_models( + setup_core: Path, mock_download_content_many: MagicMock +) -> None: + names = ("okay_nabu", "hey_mycroft", "vad") + models = {name: _http_model(name) for name in names} + paths = { + name: _write_manifest(models[name], json.dumps({"model": f"{name}.tflite"})) + for name in names + } + config = { + mww.CONF_MODELS: [ + models["okay_nabu"], + models["hey_mycroft"], + # non-http sources must be ignored + {CONF_MODEL: {CONF_TYPE: "local", CONF_PATH: "x"}}, + ], + mww.CONF_VAD: models["vad"], + } + + assert mww._download_http_models(config) is config + + assert mock_download_content_many.call_count == 2 + manifest_items = list(mock_download_content_many.call_args_list[0].args[0]) + assert manifest_items == [ + (f"https://example.com/models/{name}.json", paths[name] / "manifest.json") + for name in names + ] + model_items = list(mock_download_content_many.call_args_list[1].args[0]) + assert model_items == [ + (f"https://example.com/models/{name}.tflite", paths[name] / f"{name}.tflite") + for name in names + ] + + +def test_download_http_models_no_http_sources_skips_download( + mock_download_content_many: MagicMock, +) -> None: + config = {mww.CONF_MODELS: [{CONF_MODEL: {CONF_TYPE: "local", CONF_PATH: "x"}}]} + assert mww._download_http_models(config) is config + mock_download_content_many.assert_not_called() + + +@pytest.mark.parametrize( + ("contents", "message"), + [ + ("not json", "Invalid manifest file"), + ("[1, 2]", "must contain a JSON object"), + ("{}", "missing the 'model' key"), + ], +) +def test_download_http_models_bad_manifest_raises( + setup_core: Path, + mock_download_content_many: MagicMock, + contents: str, + message: str, +) -> None: + model = _http_model("okay_nabu") + config = {mww.CONF_MODELS: [model]} + _write_manifest(model, contents) + + with pytest.raises(cv.Invalid, match=message): + mww._download_http_models(config) + # manifests were still fetched in one batch; the model batch never ran + assert mock_download_content_many.call_count == 1 + + +def test_download_http_models_collects_all_manifest_errors( + setup_core: Path, mock_download_content_many: MagicMock +) -> None: + models = {name: _http_model(name) for name in ("one", "two")} + config = {mww.CONF_MODELS: list(models.values())} + _write_manifest(models["one"], "not json") + _write_manifest(models["two"], "[1]") + + with pytest.raises(cv.MultipleInvalid) as excinfo: + mww._download_http_models(config) + assert len(excinfo.value.errors) == 2 diff --git a/tests/unit_tests/components/test_esp8266.py b/tests/unit_tests/components/test_esp8266.py index 318fd2d889..fb0e437d24 100644 --- a/tests/unit_tests/components/test_esp8266.py +++ b/tests/unit_tests/components/test_esp8266.py @@ -1,9 +1,15 @@ """Tests for ESP8266 component.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + import pytest -from esphome.components.esp8266 import lambdas_use_scanf_float -from esphome.core import Lambda +from esphome.components import esp8266 +from esphome.components.esp8266 import check_rosetta, lambdas_use_scanf_float +from esphome.core import EsphomeError, Lambda from esphome.types import ConfigType @@ -60,3 +66,54 @@ def test_lambdas_use_scanf_float_nested() -> None: """Test detection in deeply nested config.""" config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}} assert lambdas_use_scanf_float(config) is True + + +@pytest.fixture +def apple_silicon_run(monkeypatch: pytest.MonkeyPatch) -> Generator[MagicMock]: + """Simulate an Apple Silicon Mac and yield the mocked subprocess.run.""" + monkeypatch.setattr(esp8266, "IS_MACOS", True) + with ( + patch("esphome.components.esp8266.platform.machine", return_value="arm64"), + patch("esphome.components.esp8266.subprocess.run") as mock_run, + ): + yield mock_run + + +@pytest.mark.parametrize( + ("is_macos", "machine"), + [ + (False, "arm64"), + (True, "x86_64"), + ], +) +def test_check_rosetta_skips_other_systems( + monkeypatch: pytest.MonkeyPatch, is_macos: bool, machine: str +) -> None: + """The check only probes on Apple Silicon Macs.""" + monkeypatch.setattr(esp8266, "IS_MACOS", is_macos) + with ( + patch("esphome.components.esp8266.platform.machine", return_value=machine), + patch("esphome.components.esp8266.subprocess.run") as mock_run, + ): + check_rosetta() + mock_run.assert_not_called() + + +def test_check_rosetta_installed(apple_silicon_run: MagicMock) -> None: + """No error when the x86_64 probe succeeds (Rosetta present).""" + apple_silicon_run.return_value = MagicMock(returncode=0) + check_rosetta() + apple_silicon_run.assert_called_once() + + +def test_check_rosetta_missing(apple_silicon_run: MagicMock) -> None: + """A failing x86_64 probe raises an actionable error.""" + apple_silicon_run.return_value = MagicMock(returncode=1) + with pytest.raises(EsphomeError, match="softwareupdate --install-rosetta"): + check_rosetta() + + +def test_check_rosetta_arch_unavailable(apple_silicon_run: MagicMock) -> None: + """The build proceeds when arch(1) cannot be executed.""" + apple_silicon_run.side_effect = OSError("no such file") + check_rosetta() diff --git a/tests/unit_tests/components/test_esp_stacktrace.py b/tests/unit_tests/components/test_esp_stacktrace.py index f231ac5fb7..eb7e63fc4d 100644 --- a/tests/unit_tests/components/test_esp_stacktrace.py +++ b/tests/unit_tests/components/test_esp_stacktrace.py @@ -137,3 +137,45 @@ def test_process_stacktrace_esp32_crash_handler( state = process_stacktrace(config, line_bt1, False) mock_esp32_decode_pc.assert_called_once_with(config, "42005ABC") assert state is False + + mock_esp32_decode_pc.reset_mock() + + # Reason line carries no address, must not trigger a decode + line_reason = "[E][esp32.crash:079]: Reason: Fault - LoadProhibited (cause 28)" + state = process_stacktrace(config, line_reason, False) + mock_esp32_decode_pc.assert_not_called() + assert state is False + + mock_esp32_decode_pc.reset_mock() + + # EXCVADDR pointing at code (e.g. jumping through a corrupted pointer) decodes + line_excvaddr = "[E][esp32.crash:081]: EXCVADDR: 0x400D9ABC (faulting address)" + state = process_stacktrace(config, line_excvaddr, False) + mock_esp32_decode_pc.assert_called_once_with(config, "400D9ABC") + assert state is False + + mock_esp32_decode_pc.reset_mock() + + # EXCVADDR pointing at data (heap/null) is not a code address, must be ignored + line_excvaddr_data = ( + "[E][esp32.crash:081]: EXCVADDR: 0x0000001C (faulting address)" + ) + state = process_stacktrace(config, line_excvaddr_data, False) + mock_esp32_decode_pc.assert_not_called() + assert state is False + + mock_esp32_decode_pc.reset_mock() + + # RISC-V MTVAL pointing at code decodes + line_mtval = "[E][esp32.crash:081]: MTVAL: 0x42001234 (faulting address)" + state = process_stacktrace(config, line_mtval, False) + mock_esp32_decode_pc.assert_called_once_with(config, "42001234") + assert state is False + + mock_esp32_decode_pc.reset_mock() + + # RISC-V MTVAL pointing at data must be ignored + line_mtval_data = "[E][esp32.crash:081]: MTVAL: 0x3FC80123 (faulting address)" + state = process_stacktrace(config, line_mtval_data, False) + mock_esp32_decode_pc.assert_not_called() + assert state is False diff --git a/tests/unit_tests/components/test_libretiny.py b/tests/unit_tests/components/test_libretiny.py new file mode 100644 index 0000000000..ee00bdc180 --- /dev/null +++ b/tests/unit_tests/components/test_libretiny.py @@ -0,0 +1,52 @@ +"""Tests for LibreTiny board detection, including renamed-board migration.""" + +import pytest + +from esphome.components.libretiny import _detect_variant +from esphome.components.libretiny.const import ( + FAMILY_LN882H, + KEY_COMPONENT_DATA, + KEY_LIBRETINY, +) +from esphome.components.ln882x import COMPONENT_DATA +import esphome.config_validation as cv +from esphome.const import CONF_BOARD, CONF_FAMILY +from esphome.core import CORE + + +@pytest.fixture +def ln882x_core_data() -> None: + """Populate CORE the way the ln882x component schema does.""" + CORE.data[KEY_LIBRETINY] = {KEY_COMPONENT_DATA: COMPONENT_DATA} + + +def test_detect_variant_known_board_passes(ln882x_core_data: None) -> None: + """A current board id resolves its family without warnings.""" + result = _detect_variant({CONF_BOARD: "generic-ln882h"}) + assert result[CONF_BOARD] == "generic-ln882h" + assert result[CONF_FAMILY] == FAMILY_LN882H + + +def test_detect_variant_renamed_board_migrates( + ln882x_core_data: None, caplog: pytest.LogCaptureFixture +) -> None: + """A pre-rename board id validates against the new id, with a warning.""" + result = _detect_variant({CONF_BOARD: "generic-ln882hki"}) + assert result[CONF_BOARD] == "generic-ln882h" + assert result[CONF_FAMILY] == FAMILY_LN882H + assert "renamed to 'generic-ln882h'" in caplog.text + + +def test_detect_variant_renamed_board_does_not_mutate_input( + ln882x_core_data: None, +) -> None: + """Migration copies the config; the caller's dict keeps the old id.""" + value = {CONF_BOARD: "generic-ln882hki"} + _detect_variant(value) + assert value[CONF_BOARD] == "generic-ln882hki" + + +def test_detect_variant_unknown_board_still_raises(ln882x_core_data: None) -> None: + """Ids outside the rename map keep the family-override error.""" + with pytest.raises(cv.Invalid, match="This board is unknown"): + _detect_variant({CONF_BOARD: "not-a-real-board"}) diff --git a/tests/unit_tests/components/test_rp2.py b/tests/unit_tests/components/test_rp2.py new file mode 100644 index 0000000000..023d926dc4 --- /dev/null +++ b/tests/unit_tests/components/test_rp2.py @@ -0,0 +1,95 @@ +"""Tests for the ``rp2`` target-platform component. + +``rp2`` is the canonical name for the Raspberry Pi RP-series target +platform. ``rp2040`` is a deprecated alias declared via +``ALIASES = ["rp2040"]`` on the rp2 component — the framework +(see ``esphome/loader.py`` and ``esphome/config.py``) handles both +Python-import aliasing (via a ``sys.meta_path`` finder) and YAML-key +aliasing (via a pre-pass in ``validate_config``), so there is no +hand-rolled shim in ``esphome/components/rp2040/``. + +These tests pin down the canonical board helpers; the alias contract +itself (Python imports, YAML key rename, deprecation warning) is covered +by the framework tests under ``tests/unit_tests/``. +""" + + +def test_board_id_has_wifi_for_known_wifi_board() -> None: + """``rpipicow`` is the canonical Pico W → True.""" + from esphome.components import rp2 + + assert rp2.board_id_has_wifi("rpipicow") is True + + +def test_board_id_has_wifi_for_known_non_wifi_board() -> None: + """Plain ``rpipico`` has no CYW43 → False.""" + from esphome.components import rp2 + + assert rp2.board_id_has_wifi("rpipico") is False + + +def test_board_id_has_wifi_for_rp2350_w_variant() -> None: + """``rpipico2w`` is the RP2350 Pico 2 W → True.""" + from esphome.components import rp2 + + assert rp2.board_id_has_wifi("rpipico2w") is True + + +def test_board_id_has_wifi_for_unknown_board_returns_true() -> None: + """Unknown ids fail open so a custom board is not rejected. + + The validator falls back to ESPHome's compile-time check; the + helper returning True here means the wizard emits a ``wifi:`` + block and any genuinely-unsupported config trips the existing + "no CYW43" guard at compile time. + """ + from esphome.components import rp2 + + assert rp2.board_id_has_wifi("not-a-real-board-id") is True + + +def test_rp2_declares_rp2040_as_alias() -> None: + """The framework-level deprecation hook is on the ``rp2`` component. + + The legacy ``rp2040:`` YAML key works because the rp2 component + opts in via ``ALIASES``; without this declaration the rename + framework wouldn't route legacy configs. + """ + from esphome.components import rp2 + + assert "rp2040" in rp2.ALIASES + assert rp2.ALIAS_REMOVAL_VERSION == "2027.7.0" + + +def test_rp2040_python_import_resolves_to_rp2() -> None: + """``from esphome.components import rp2040`` must work for external + custom components and external tooling (device-builder, the dashboard + wizard, etc.) that still import from the legacy module path. + + The ``_AliasFinder`` on ``sys.meta_path`` rewrites the lookup to + the canonical module — both should be the same object. + """ + from esphome.components import ( + rp2, + rp2040, # routed via _AliasFinder + ) + + assert rp2040 is rp2 + + +def test_rp2040_submodule_imports_resolve_to_rp2_submodules() -> None: + """Submodule imports (e.g. ``esphome.components.rp2040.boards``) must + also route to the canonical equivalents — the board-generator script + and the dashboard wizard both rely on this path. + """ + from esphome.components.rp2 import ( + boards as rp2_boards, + generate_boards as rp2_generate, + ) + from esphome.components.rp2040 import ( + boards as rp2040_boards, + generate_boards as rp2040_generate, + ) + + assert rp2040_boards is rp2_boards + assert rp2040_generate is rp2_generate diff --git a/tests/unit_tests/components/test_rp2040.py b/tests/unit_tests/components/test_rp2040.py deleted file mode 100644 index 8e726933ed..0000000000 --- a/tests/unit_tests/components/test_rp2040.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Tests for RP2040 component public helpers and variant detection.""" - -import pytest - -from esphome.components.rp2040 import _detect_variant, board_id_has_wifi -from esphome.components.rp2040.const import VARIANT_RP2040, VARIANT_RP2350 -import esphome.config_validation as cv -from esphome.const import CONF_BOARD, CONF_VARIANT - - -def test_board_id_has_wifi_for_known_wifi_board() -> None: - """``rpipicow`` is the canonical Pico W → True.""" - assert board_id_has_wifi("rpipicow") is True - - -def test_board_id_has_wifi_for_known_non_wifi_board() -> None: - """Plain ``rpipico`` has no CYW43 → False.""" - assert board_id_has_wifi("rpipico") is False - - -def test_board_id_has_wifi_for_rp2350_w_variant() -> None: - """``rpipico2w`` is the RP2350 Pico 2 W → True.""" - assert board_id_has_wifi("rpipico2w") is True - - -def test_board_id_has_wifi_for_unknown_board_returns_true() -> None: - """Unknown ids fail open so a custom board is not rejected. - - The validator falls back to ESPHome's compile-time check; the - helper returning True here means the wizard emits a ``wifi:`` - block and any genuinely-unsupported config trips the existing - "no CYW43" guard at compile time. - """ - assert board_id_has_wifi("not-a-real-board-id") is True - - -def test_detect_variant_derives_variant_from_board() -> None: - """Board alone resolves to the matching variant.""" - result = _detect_variant({CONF_BOARD: "rpipicow"}) - assert result[CONF_BOARD] == "rpipicow" - assert result[CONF_VARIANT] == VARIANT_RP2040 - - -def test_detect_variant_derives_variant_from_rp2350_board() -> None: - """An RP2350 board resolves to ``RP2350``.""" - result = _detect_variant({CONF_BOARD: "rpipico2"}) - assert result[CONF_BOARD] == "rpipico2" - assert result[CONF_VARIANT] == VARIANT_RP2350 - - -def test_detect_variant_only_picks_default_board_rp2040() -> None: - """Variant alone picks Pico W as the canonical RP2040 board.""" - result = _detect_variant({CONF_VARIANT: VARIANT_RP2040}) - assert result[CONF_BOARD] == "rpipicow" - assert result[CONF_VARIANT] == VARIANT_RP2040 - - -def test_detect_variant_only_picks_default_board_rp2350() -> None: - """Variant alone picks Pico 2 W as the canonical RP2350 board.""" - result = _detect_variant({CONF_VARIANT: VARIANT_RP2350}) - assert result[CONF_BOARD] == "rpipico2w" - assert result[CONF_VARIANT] == VARIANT_RP2350 - - -def test_detect_variant_matching_explicit_variant_passes() -> None: - """Specifying both a board and the matching variant is allowed.""" - result = _detect_variant({CONF_BOARD: "rpipico2", CONF_VARIANT: VARIANT_RP2350}) - assert result[CONF_BOARD] == "rpipico2" - assert result[CONF_VARIANT] == VARIANT_RP2350 - - -def test_detect_variant_mismatched_variant_raises() -> None: - """Board/variant mismatch must be rejected and name the offending board.""" - with pytest.raises( - cv.Invalid, match=r"does not match the selected board 'rpipicow'" - ): - _detect_variant({CONF_BOARD: "rpipicow", CONF_VARIANT: VARIANT_RP2350}) - - -def test_detect_variant_unknown_board_without_variant_raises() -> None: - """Unknown board with no variant tells the user how to recover.""" - with pytest.raises(cv.Invalid, match="please specify the chip variant"): - _detect_variant({CONF_BOARD: "not-a-real-board"}) - - -def test_detect_variant_unknown_board_with_variant_passes() -> None: - """Unknown board + explicit variant is accepted (with a warning).""" - result = _detect_variant( - {CONF_BOARD: "not-a-real-board", CONF_VARIANT: VARIANT_RP2040} - ) - assert result[CONF_BOARD] == "not-a-real-board" - assert result[CONF_VARIANT] == VARIANT_RP2040 diff --git a/tests/unit_tests/components/test_rp2040_generate_boards.py b/tests/unit_tests/components/test_rp2_generate_boards.py similarity index 98% rename from tests/unit_tests/components/test_rp2040_generate_boards.py rename to tests/unit_tests/components/test_rp2_generate_boards.py index 551e88f6f6..68bbada59b 100644 --- a/tests/unit_tests/components/test_rp2040_generate_boards.py +++ b/tests/unit_tests/components/test_rp2_generate_boards.py @@ -1,4 +1,4 @@ -"""Tests for rp2040 generate_boards.py.""" +"""Tests for rp2 generate_boards.py.""" from __future__ import annotations @@ -8,7 +8,7 @@ import textwrap import pytest -from esphome.components.rp2040.generate_boards import load_boards, parse_variant_pins +from esphome.components.rp2.generate_boards import load_boards, parse_variant_pins PICO_PINS_HEADER = textwrap.dedent("""\ #pragma once diff --git a/tests/unit_tests/components/test_wifi.py b/tests/unit_tests/components/test_wifi.py index 9598c1bdd8..3899b3d854 100644 --- a/tests/unit_tests/components/test_wifi.py +++ b/tests/unit_tests/components/test_wifi.py @@ -87,8 +87,8 @@ def test_has_native_wifi_esp32_variant_case_insensitive() -> None: def test_has_native_wifi_dispatches_rp2040_to_board_check() -> None: """RP2040 platform routes through ``rp2040.board_id_has_wifi``.""" - assert has_native_wifi(platform=Platform.RP2040, board="rpipicow") is True - assert has_native_wifi(platform=Platform.RP2040, board="rpipico") is False + assert has_native_wifi(platform=Platform.RP2, board="rpipicow") is True + assert has_native_wifi(platform=Platform.RP2, board="rpipico") is False def test_has_native_wifi_returns_false_for_nrf52() -> None: @@ -134,7 +134,7 @@ def test_has_native_wifi_esp32_without_variant_assumes_wifi() -> None: def test_has_native_wifi_rp2040_without_board_assumes_wifi() -> None: """RP2040 without a board id falls open to True (custom-board default).""" - assert has_native_wifi(platform=Platform.RP2040) is True + assert has_native_wifi(platform=Platform.RP2) is True def _wifi_config( diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index b3d87f6857..0362c40bce 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -1113,6 +1113,48 @@ def test_config_hash_different_for_different_configs() -> None: assert hash1 != hash2 +def test_config_hash_ignores_build_path() -> None: + """Test that config_hash does not depend on the build_path value. + + build_path embeds ESPHOME_BUILD_PATH and OS path separators, so it must + not make the hash differ between machines. + """ + CORE.reset() + CORE.config = {"esphome": {"name": "test", "build_path": "build\\test"}} + hash1 = CORE.config_hash + + CORE.reset() + CORE.config = {"esphome": {"name": "test", "build_path": "/build/test"}} + hash2 = CORE.config_hash + + assert hash1 == hash2 + + +def test_config_hash_same_for_different_config_dirs(tmp_path: Path) -> None: + """Test that Path values under the config dir hash the same everywhere. + + Simulates the same project checked out at two different locations; the + absolute paths differ but the layout relative to the config dir is the + same, so the hashes must match. + """ + dir1 = tmp_path / "machine_a" / "project" + dir2 = tmp_path / "machine_b" / "somewhere" / "else" + dir1.mkdir(parents=True) + dir2.mkdir(parents=True) + + CORE.reset() + CORE.config_path = dir1 / "device.yaml" + CORE.config = {"esphome": {"name": "test"}, "file": dir1 / "fonts" / "arial.ttf"} + hash1 = CORE.config_hash + + CORE.reset() + CORE.config_path = dir2 / "device.yaml" + CORE.config = {"esphome": {"name": "test"}, "file": dir2 / "fonts" / "arial.ttf"} + hash2 = CORE.config_hash + + assert hash1 == hash2 + + def test_make_app_name_cpp_no_mac_simple() -> None: """Test simple name without MAC suffix returns string literal.""" cpp_expr, global_decl, byte_len = make_app_name_cpp( @@ -1307,3 +1349,34 @@ async def test_to_code_adds_libraries(yaml_file: Callable[[str], Path]) -> None: mock_cg.add_library.assert_any_call( "noise-c", None, "https://github.com/esphome/noise-c.git" ) + + +def test_esphome_build_internals_are_yaml_only() -> None: + """Raw build-system inputs in the ``esphome:`` block are ``YAML_ONLY``. + + These knobs (compiler flags, raw PlatformIO options, C/C++ includes, + libraries, build host parallelism, the min-version gate, …) are not + meaningful as visual-editor form fields and a wrong value breaks the + build, so they must never render in a schema-aware UI. + """ + # CONFIG_SCHEMA is cv.All(cv.Schema({...}), validate_hostname). + inner = config.CONFIG_SCHEMA.validators[0].schema + markers = {str(k): k for k in inner} + yaml_only_fields = { + CONF_BUILD_PATH, + "platformio_options", + "build_flags", + "environment_variables", + "includes", + "includes_c", + "libraries", + "debug_scheduler", + } + for field in yaml_only_fields: + assert markers[field].visibility is cv.Visibility.YAML_ONLY, field + # Packaging / build-host knobs are real but rarely-touched overrides: + # surface them under the editor's advanced disclosure, not yaml-only. + for field in ("min_version", "compile_process_limit"): + assert markers[field].visibility is cv.Visibility.ADVANCED, field + # A regular device-config field stays on the main form. + assert markers[CONF_NAME_ADD_MAC_SUFFIX].visibility is None diff --git a/tests/unit_tests/fixtures/idf_tools_stub/idf_tools.py b/tests/unit_tests/fixtures/idf_tools_stub/idf_tools.py new file mode 100644 index 0000000000..aeff24fb57 --- /dev/null +++ b/tests/unit_tests/fixtures/idf_tools_stub/idf_tools.py @@ -0,0 +1,120 @@ +"""Minimal idf_tools stand-in for get_tool_downloads.py tests.""" + +from collections.abc import Iterable +import os + +CURRENT_PLATFORM = "linux-amd64" +TOOLS_FILE = "tools/tools.json" + + +class ToolBinaryError(RuntimeError): + pass + + +class _G: + idf_path: str | None = None + idf_tools_path: str | None = None + tools_json: str | None = None + + +g = _G() + + +class IDFEnv: + @classmethod + def get_idf_env(cls) -> "IDFEnv": + return cls() + + +def add_and_check_targets(idf_env_obj: IDFEnv, targets_str: str) -> list[str]: + return targets_str.split(",") + + +class _Download: + def __init__(self, url: str, size: int, sha256: str, rename_dist: str = "") -> None: + self.url = url + self.size = size + self.sha256 = sha256 + self.rename_dist = rename_dist + + +class _Version: + def __init__(self, download: _Download | None) -> None: + self._download = download + + def get_download_for_platform(self, platform_name: str) -> _Download | None: + return self._download + + +class _Tool: + def __init__( + self, + versions: dict[str, _Version], + recommended: str | None, + installed: Iterable[str] = (), + broken: bool = False, + ) -> None: + self.versions = versions + self._recommended = recommended + self.versions_installed = list(installed) + self._broken = broken + + def compatible_with_platform(self) -> bool: + return True + + def get_recommended_version(self) -> str | None: + return self._recommended + + def find_installed_versions(self) -> None: + if self._broken: + raise ToolBinaryError("broken binary") + + +_TOOLS = { + "cmake": _Tool( + {"3.30.2": _Version(_Download("https://gh.test/cmake.tar.gz", 11, "aa"))}, + "3.30.2", + ), + "ninja": _Tool( + { + "1.12.1": _Version( + _Download("https://gh.test/ninja-mac.zip", 22, "bb", "ninja-v1.zip") + ) + }, + "1.12.1", + ), + "installed-tool": _Tool( + {"1.0": _Version(_Download("https://gh.test/x.tar.gz", 33, "cc"))}, + "1.0", + installed=["1.0"], + ), + "broken-tool": _Tool( + {"2.0": _Version(_Download("https://gh.test/y.tar.gz", 44, "dd"))}, + "2.0", + broken=True, + ), + "no-recommended-tool": _Tool({"3.0": _Version(None)}, None), + "no-download-tool": _Tool({"4.0": _Version(None)}, "4.0"), +} + + +def load_tools_info() -> dict[str, _Tool]: + return _TOOLS + + +def expand_tools_arg( + tools_spec: list[str], overall_tools: dict[str, _Tool], targets: list[str] +) -> list[str]: + if "required" in tools_spec: + return list(overall_tools) + return [t for t in tools_spec if "@" not in t] + [t for t in tools_spec if "@" in t] + + +def get_idf_download_url_apply_mirrors( + args: object = None, download_url: str = "" +) -> str: + print(f"Changed download URL: {download_url}") # noise on stdout, like idf_tools + prefix = os.environ.get("TEST_MIRROR_PREFIX") + if prefix: + return prefix + download_url + return download_url diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py index f15bbf2e29..f0abcc74c6 100644 --- a/tests/unit_tests/test_bundle.py +++ b/tests/unit_tests/test_bundle.py @@ -22,10 +22,12 @@ from esphome.bundle import ( _add_bytes_to_tar, _default_target_dir, _find_used_secret_keys, + add_bundle_file, extract_bundle, is_bundle_path, prepare_bundle_for_compile, read_bundle_manifest, + remap_bundle_path, ) from esphome.core import CORE, EsphomeError from esphome.yaml_util import force_load_include_files @@ -477,7 +479,10 @@ def test_read_bundle_manifest_corrupted_tar(tmp_path: Path) -> None: def test_read_bundle_manifest(tmp_path: Path) -> None: bundle_path = _make_bundle( tmp_path, - manifest_overrides={ManifestKey.HAS_SECRETS: True}, + manifest_overrides={ + ManifestKey.HAS_SECRETS: True, + ManifestKey.CONFIG_DIR: "/original/config", + }, extra_files={"secrets.yaml": b"wifi: test\n"}, ) @@ -488,6 +493,7 @@ def test_read_bundle_manifest(tmp_path: Path) -> None: assert manifest.esphome_version == "2026.2.0-test" assert manifest.config_filename == "test.yaml" assert manifest.has_secrets is True + assert manifest.config_dir == "/original/config" def test_read_bundle_manifest_minimal(tmp_path: Path) -> None: @@ -507,6 +513,266 @@ def test_read_bundle_manifest_minimal(tmp_path: Path) -> None: assert result.esphome_version == "unknown" assert not result.files assert result.has_secrets is False + assert result.config_dir is None + + +def test_read_bundle_manifest_non_string_config_dir(tmp_path: Path) -> None: + """A malformed config_dir value is dropped rather than propagated.""" + bundle_path = _make_bundle( + tmp_path, manifest_overrides={ManifestKey.CONFIG_DIR: 42} + ) + + assert read_bundle_manifest(bundle_path).config_dir is None + + +# --------------------------------------------------------------------------- +# remap_bundle_path +# --------------------------------------------------------------------------- + + +ORIGINAL_CONFIG_DIR = "/original/config" + + +def _bundle_manifest_dict(**overrides: Any) -> dict[str, Any]: + """Manifest content an extracted bundle would contain.""" + manifest: dict[str, Any] = { + ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION, + ManifestKey.CONFIG_FILENAME: "test.yaml", + ManifestKey.CONFIG_DIR: ORIGINAL_CONFIG_DIR, + } + manifest.update(overrides) + return manifest + + +def _setup_extracted_dir( + tmp_path: Path, + manifest: dict[str, Any] | str | None, + files: dict[str, str] | None = None, +) -> Path: + """Create a directory shaped like an extracted bundle and point CORE at it.""" + extract_dir = _setup_config_dir(tmp_path, files) + if manifest is not None: + content = manifest if isinstance(manifest, str) else json.dumps(manifest) + (extract_dir / MANIFEST_FILENAME).write_text(content) + return extract_dir + + +def test_remap_bundle_path_success(tmp_path: Path) -> None: + """A stale absolute path resolves to the bundled copy next to the config.""" + extract_dir = _setup_extracted_dir( + tmp_path, _bundle_manifest_dict(), files={"boards/partitions.csv": "csv\n"} + ) + + remapped = remap_bundle_path(f"{ORIGINAL_CONFIG_DIR}/boards/partitions.csv") + + assert remapped == extract_dir / "boards" / "partitions.csv" + assert remapped.is_file() + + +@pytest.mark.parametrize( + "value", + [ + pytest.param(r"C:\Users\nick\esphome\boards\partitions.csv", id="backslashes"), + pytest.param("C:/Users/nick/esphome/boards/partitions.csv", id="forward"), + pytest.param(r"c:\users\NICK\esphome\boards\partitions.csv", id="case"), + ], +) +def test_remap_bundle_path_windows_bundle_on_posix(tmp_path: Path, value: str) -> None: + """A bundle created on Windows remaps on a build server with another layout.""" + extract_dir = _setup_extracted_dir( + tmp_path, + _bundle_manifest_dict(**{ManifestKey.CONFIG_DIR: r"C:\Users\nick\esphome"}), + files={"boards/partitions.csv": "csv\n"}, + ) + + remapped = remap_bundle_path(value) + + assert remapped == extract_dir / "boards" / "partitions.csv" + assert remapped.is_file() + + +def test_remap_bundle_path_windows_bundle_path_not_under_config_dir( + tmp_path: Path, +) -> None: + """A Windows path outside the original config dir is left alone.""" + _setup_extracted_dir( + tmp_path, + _bundle_manifest_dict(**{ManifestKey.CONFIG_DIR: r"C:\Users\nick\esphome"}), + files={"partitions.csv": "csv\n"}, + ) + + assert remap_bundle_path(r"D:\other\partitions.csv") is None + + +def test_remap_bundle_path_windows_profile_with_spaces(tmp_path: Path) -> None: + r"""A Windows profile like C:\Users\First Last remaps like any other dir.""" + extract_dir = _setup_extracted_dir( + tmp_path, + _bundle_manifest_dict( + **{ManifestKey.CONFIG_DIR: r"C:\Users\First Last\esphome"} + ), + files={"boards/my partitions.csv": "csv\n"}, + ) + + remapped = remap_bundle_path( + r"C:\Users\First Last\esphome\boards\my partitions.csv" + ) + + assert remapped == extract_dir / "boards" / "my partitions.csv" + assert remapped.is_file() + + +def test_remap_bundle_path_unc_config_dir(tmp_path: Path) -> None: + """A bundle created from a UNC share remaps like any other Windows path.""" + extract_dir = _setup_extracted_dir( + tmp_path, + _bundle_manifest_dict(**{ManifestKey.CONFIG_DIR: r"\\server\share\esphome"}), + files={"partitions.csv": "csv\n"}, + ) + + remapped = remap_bundle_path(r"\\server\share\esphome\partitions.csv") + + assert remapped == extract_dir / "partitions.csv" + + +def test_remap_bundle_path_flavor_mismatch(tmp_path: Path) -> None: + """A POSIX style value cannot come from a Windows config dir; no remap.""" + _setup_extracted_dir( + tmp_path, + _bundle_manifest_dict(**{ManifestKey.CONFIG_DIR: r"C:\Users\nick\esphome"}), + files={"partitions.csv": "csv\n"}, + ) + + assert remap_bundle_path("/original/config/partitions.csv") is None + + +def test_remap_bundle_path_rejects_traversal(tmp_path: Path) -> None: + """A remap may never escape the extracted config tree.""" + extract_dir = _setup_extracted_dir(tmp_path, _bundle_manifest_dict()) + (tmp_path / "outside.csv").write_text("csv\n") + assert (extract_dir / ".." / "outside.csv").resolve().is_file() + + assert remap_bundle_path(f"{ORIGINAL_CONFIG_DIR}/../outside.csv") is None + + +def test_remap_bundle_path_relative_value(tmp_path: Path) -> None: + """Relative references resolve normally and are never remapped.""" + _setup_extracted_dir(tmp_path, _bundle_manifest_dict()) + + assert remap_bundle_path("missing.csv") is None + + +def test_remap_bundle_path_no_manifest(tmp_path: Path) -> None: + """A config dir without a manifest is not an extracted bundle.""" + _setup_extracted_dir(tmp_path, None, files={"partitions.csv": "csv\n"}) + + assert remap_bundle_path(f"{ORIGINAL_CONFIG_DIR}/partitions.csv") is None + + +@pytest.mark.parametrize( + "manifest", + [ + pytest.param("{not json", id="malformed_json"), + pytest.param("[]", id="not_a_dict"), + pytest.param( + _bundle_manifest_dict(**{ManifestKey.MANIFEST_VERSION: "x"}), + id="version_not_int", + ), + pytest.param( + _bundle_manifest_dict(**{ManifestKey.MANIFEST_VERSION: 0}), + id="version_zero", + ), + pytest.param( + _bundle_manifest_dict(**{ManifestKey.CONFIG_FILENAME: "other.yaml"}), + id="config_filename_mismatch", + ), + pytest.param( + { + ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION, + ManifestKey.CONFIG_FILENAME: "test.yaml", + }, + id="config_dir_missing", + ), + pytest.param( + _bundle_manifest_dict(**{ManifestKey.CONFIG_DIR: ""}), + id="config_dir_empty", + ), + ], +) +def test_remap_bundle_path_untrusted_manifest( + tmp_path: Path, manifest: dict[str, Any] | str +) -> None: + """Manifests that do not look like this bundle's manifest are ignored.""" + _setup_extracted_dir(tmp_path, manifest, files={"partitions.csv": "csv\n"}) + + assert remap_bundle_path(f"{ORIGINAL_CONFIG_DIR}/partitions.csv") is None + + +def test_remap_bundle_path_unreadable_manifest_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A present but broken manifest is reported, not silently ignored.""" + _setup_extracted_dir(tmp_path, "{not json", files={"partitions.csv": "csv\n"}) + + assert remap_bundle_path(f"{ORIGINAL_CONFIG_DIR}/partitions.csv") is None + assert "ignoring unreadable" in caplog.text + + +def test_remap_bundle_path_outside_original_config_dir(tmp_path: Path) -> None: + """Paths that were not under the original config dir are left alone.""" + _setup_extracted_dir(tmp_path, _bundle_manifest_dict()) + + assert remap_bundle_path("/elsewhere/partitions.csv") is None + + +def test_remap_bundle_path_bundled_copy_missing(tmp_path: Path) -> None: + """No remap when the bundle does not contain the file.""" + _setup_extracted_dir(tmp_path, _bundle_manifest_dict()) + + assert remap_bundle_path(f"{ORIGINAL_CONFIG_DIR}/partitions.csv") is None + + +def test_remap_bundle_path_manifest_read_once(tmp_path: Path) -> None: + """The manifest lookup result is cached for the rest of the run.""" + extract_dir = _setup_extracted_dir( + tmp_path, _bundle_manifest_dict(), files={"partitions.csv": "csv\n"} + ) + + first = remap_bundle_path(f"{ORIGINAL_CONFIG_DIR}/partitions.csv") + assert first == extract_dir / "partitions.csv" + + (extract_dir / MANIFEST_FILENAME).unlink() + second = remap_bundle_path(f"{ORIGINAL_CONFIG_DIR}/partitions.csv") + assert second == first + + +def test_remap_bundle_path_round_trip(tmp_path: Path) -> None: + """A file referenced by absolute path survives bundle create and extract. + + Reproduces https://github.com/esphome/esphome/issues/17755: the config + names its partitions csv by absolute path, the bundle is extracted on a + machine where that path does not exist, and the reference must resolve + to the bundled copy. + """ + config_dir = _setup_config_dir(tmp_path, files={"partitions.csv": "nvs,data\n"}) + abs_path = (config_dir / "partitions.csv").resolve() + + creator = ConfigBundleCreator({"esp32": {"partitions": abs_path}}) + result = creator.create_bundle() + + bundle_path = tmp_path / f"device{BUNDLE_EXTENSION}" + bundle_path.write_bytes(result.data) + target = tmp_path / "build_server" + config_path = extract_bundle(bundle_path, target) + + # Simulate the build server: fresh run, original config dir gone + CORE.reset() + CORE.config_path = config_path + shutil.rmtree(config_dir) + + remapped = remap_bundle_path(str(abs_path)) + assert remapped == target.resolve() / "partitions.csv" + assert remapped.is_file() # --------------------------------------------------------------------------- @@ -611,6 +877,70 @@ def test_discover_files_includes_config(tmp_path: Path) -> None: assert "test.yaml" in paths +def test_discover_files_includes_registered_files(tmp_path: Path) -> None: + """Files registered with add_bundle_file() are included. + + The config does not name them, so discovery cannot find them on its own. + """ + config_dir = _setup_config_dir( + tmp_path, + files={"models/model.tflite": "fake model data"}, + ) + add_bundle_file(config_dir / "models" / "model.tflite") + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "models/model.tflite" in paths + + +def test_discover_files_registered_relative_file(tmp_path: Path) -> None: + """A relative registered path is taken as relative to the config directory. + + Not the working directory, which is where Path.resolve() would put it. + """ + _setup_config_dir( + tmp_path, + files={"models/model.tflite": "fake model data"}, + ) + add_bundle_file(Path("models/model.tflite")) + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "models/model.tflite" in paths + + +def test_discover_files_registered_file_outside_config_dir(tmp_path: Path) -> None: + """A registered file outside the config directory is skipped, not bundled.""" + _setup_config_dir(tmp_path) + outside = tmp_path / "outside.tflite" + outside.write_text("fake model data") + add_bundle_file(outside) + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + assert [f.path for f in files] == ["test.yaml"] + + +def test_discover_files_registered_file_deduplicated(tmp_path: Path) -> None: + """Registering the same file twice adds it once.""" + config_dir = _setup_config_dir( + tmp_path, + files={"models/model.tflite": "fake model data"}, + ) + add_bundle_file(config_dir / "models" / "model.tflite") + add_bundle_file(config_dir / "models" / "model.tflite") + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + assert [f.path for f in files].count("models/model.tflite") == 1 + + def test_discover_files_finds_path_objects(tmp_path: Path) -> None: """Path objects in validated config are discovered.""" config_dir = _setup_config_dir( @@ -1196,7 +1526,7 @@ def test_create_bundle_produces_valid_archive(tmp_path: Path) -> None: def test_create_bundle_manifest_content(tmp_path: Path) -> None: - _setup_config_dir(tmp_path) + config_dir = _setup_config_dir(tmp_path) creator = ConfigBundleCreator({}) result = creator.create_bundle() @@ -1204,6 +1534,7 @@ def test_create_bundle_manifest_content(tmp_path: Path) -> None: manifest = result.manifest assert manifest[ManifestKey.MANIFEST_VERSION] == CURRENT_MANIFEST_VERSION assert manifest[ManifestKey.CONFIG_FILENAME] == "test.yaml" + assert manifest[ManifestKey.CONFIG_DIR] == str(config_dir.resolve()) assert "test.yaml" in manifest[ManifestKey.FILES] diff --git a/tests/unit_tests/test_compiled_config.py b/tests/unit_tests/test_compiled_config.py index e12107152b..e17271e2b4 100644 --- a/tests/unit_tests/test_compiled_config.py +++ b/tests/unit_tests/test_compiled_config.py @@ -74,13 +74,13 @@ def _write_storage( "framework": "arduino", "core_platform": core_platform, } - storage_path.write_text(json.dumps(data)) + storage_path.write_text(json.dumps(data), encoding="utf-8") def _write_cache(cache_path: Path, body: str = _VALIDATED_CONFIG_YAML) -> Path: """Write the cache file and return it.""" cache_path.parent.mkdir(parents=True, exist_ok=True) - cache_path.write_text(body) + cache_path.write_text(body, encoding="utf-8") return cache_path @@ -220,7 +220,7 @@ def test_run_esphome_upload_and_logs_use_cache_when_fresh( with ( caplog.at_level("INFO", logger="esphome.__main__"), - patch("esphome.__main__.read_config") as mock_read, + patch("esphome.config.read_config") as mock_read, patch.dict("esphome.__main__.POST_CONFIG_ACTIONS", {command: _stub}), ): assert run_esphome(["esphome", command, str(fresh_cache_files)]) == 0 @@ -242,7 +242,7 @@ def test_run_esphome_upload_and_logs_fall_back_when_no_cache( yaml_path.write_text("esphome:\n name: lite_test\n") with ( - patch("esphome.__main__.read_config", return_value=None) as mock_read, + patch("esphome.config.read_config", return_value=None) as mock_read, patch.dict( "esphome.__main__.POST_CONFIG_ACTIONS", {command: lambda args, config: 0}, @@ -266,7 +266,7 @@ def test_run_esphome_upload_does_not_refresh_cache_without_sidecar( with ( patch( - "esphome.__main__.read_config", + "esphome.config.read_config", return_value={"esphome": {"name": "lite_test"}}, ), patch("esphome.compiled_config.save_compiled_config") as mock_save, @@ -299,7 +299,7 @@ def test_run_esphome_upload_and_logs_refresh_cache_on_fallback( fresh_config = {"esphome": {"name": "lite_test"}, "logger": {}} with ( - patch("esphome.__main__.read_config", return_value=fresh_config), + patch("esphome.config.read_config", return_value=fresh_config), patch( "esphome.compiled_config.save_compiled_config", wraps=save_compiled_config ) as mock_save, @@ -322,7 +322,7 @@ def test_run_esphome_upload_with_substitution_does_not_refresh_cache( """`-s` substitutions skip the cache on both read and write -- saving here would clobber the cache with a substitution-specific config.""" with ( - patch("esphome.__main__.read_config", return_value={"esphome": {}}), + patch("esphome.config.read_config", return_value={"esphome": {}}), patch("esphome.compiled_config.save_compiled_config") as mock_save, patch.dict( "esphome.__main__.POST_CONFIG_ACTIONS", @@ -341,7 +341,7 @@ def test_run_esphome_compile_does_not_refresh_cache_via_fallback( upload/logs fallback path -- the fallback save would skip the storage_should_clean check.""" with ( - patch("esphome.__main__.read_config", return_value={"esphome": {}}), + patch("esphome.config.read_config", return_value={"esphome": {}}), patch("esphome.compiled_config.save_compiled_config") as mock_save, patch.dict( "esphome.__main__.POST_CONFIG_ACTIONS", @@ -360,7 +360,7 @@ def test_run_esphome_upload_with_substitution_skips_cache( against the prior substitution set, so reusing it would silently ignore the override.""" with ( - patch("esphome.__main__.read_config", return_value=None) as mock_read, + patch("esphome.config.read_config", return_value=None) as mock_read, patch.dict( "esphome.__main__.POST_CONFIG_ACTIONS", {"upload": lambda args, config: 0}, @@ -374,7 +374,7 @@ def test_run_esphome_upload_with_substitution_skips_cache( def test_run_esphome_compile_does_not_use_cache(fresh_cache_files: Path) -> None: """The compile subcommand always re-validates -- it's what writes the cache.""" with ( - patch("esphome.__main__.read_config", return_value=None) as mock_read, + patch("esphome.config.read_config", return_value=None) as mock_read, patch.dict( "esphome.__main__.POST_CONFIG_ACTIONS", {"compile": lambda args, config: 0}, diff --git a/tests/unit_tests/test_config_normalization.py b/tests/unit_tests/test_config_normalization.py index a06b2da621..c8b7b63094 100644 --- a/tests/unit_tests/test_config_normalization.py +++ b/tests/unit_tests/test_config_normalization.py @@ -1,6 +1,6 @@ """Unit tests for esphome.config module.""" -from collections.abc import Generator +from collections.abc import Callable, Generator import logging from pathlib import Path from unittest.mock import MagicMock, Mock, patch @@ -8,7 +8,8 @@ from unittest.mock import MagicMock, Mock, patch import pytest from esphome import config, yaml_util -from esphome.core import CORE +from esphome.core import CORE, AutoLoad +from esphome.types import ConfigType @pytest.fixture @@ -116,6 +117,86 @@ def test_ota_with_platform_list_and_captive_portal(fixtures_dir: Path) -> None: assert "web_server" in platforms, f"Expected web_server platform in {platforms}" +# --------------------------------------------------------------------------- +# LEGACY_CONFIG_MIGRATE hook on LoadValidationStep -- the removable shim that +# lets a platform component rewrite a pre-platform top-level config. +# --------------------------------------------------------------------------- + + +def _run_load_step( + domain: str, + conf: object, + migrate: Callable[[ConfigType], list | None] | None, +) -> config.Config: + """Run a LoadValidationStep for a platform component with a given migrate hook.""" + component = Mock() + component.is_platform_component = True + component.multi_conf_no_default = False + component.legacy_config_migrate = migrate + + result = config.Config() + with ( + patch("esphome.config.get_component", return_value=component), + patch("esphome.config._process_auto_load"), + patch("esphome.config._process_platform_config"), + ): + config.LoadValidationStep(domain, conf).run(result) + return result + + +def test_legacy_migrate_rewrites_conf() -> None: + """A legacy config that the hook migrates is replaced with the new list.""" + migrated = [{"platform": "file", "id": "a"}] + migrate = Mock(return_value=migrated) + + result = _run_load_step("image", [{"id": "a", "file": "x.png"}], migrate) + + migrate.assert_called_once_with([{"id": "a", "file": "x.png"}]) + assert result["image"] == migrated + + +def test_legacy_migrate_none_keeps_new_format() -> None: + """When the hook returns None the already-new config is left untouched.""" + new_format = [{"platform": "file", "id": "a"}] + migrate = Mock(return_value=None) + + result = _run_load_step("image", new_format, migrate) + + migrate.assert_called_once_with(new_format) + assert result["image"] == new_format + + +def test_legacy_migrate_absent_hook_is_noop() -> None: + """A platform component without the hook normalizes without migration.""" + result = _run_load_step("image", {"id": "a"}, None) + + # Bare dict still gets wrapped into a list by the normal normalization path. + assert result["image"] == [{"id": "a"}] + + +def test_legacy_migrate_skipped_for_empty_conf() -> None: + """An empty config short-circuits before the hook is consulted.""" + migrate = Mock(return_value=[{"platform": "file"}]) + + result = _run_load_step("image", [], migrate) + + migrate.assert_not_called() + assert result["image"] == [] + + +def test_legacy_migrate_skipped_for_autoload() -> None: + """An auto-loaded (AutoLoad) config is never migrated.""" + migrate = Mock(return_value=[{"platform": "file"}]) + auto = AutoLoad() + auto["id"] = "a" + + result = _run_load_step("image", auto, migrate) + + migrate.assert_not_called() + # AutoLoad is dict-like, so normalization wraps it into a single-entry list. + assert result["image"] == [auto] + + def _write_merge_conflict_config(tmp_path: Path, *, suppress: bool) -> Path: """Create a config where two `<<` includes both define `logger:`. diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index ea3a4ecb53..79bfc303b7 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -1,3 +1,5 @@ +import json +import logging from pathlib import Path import string @@ -39,7 +41,7 @@ from esphome.const import ( PLATFORM_ESP8266, PLATFORM_HOST, PLATFORM_LN882X, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_RTL87XX, SCHEDULER_DONT_RUN, TYPE_GIT, @@ -438,7 +440,7 @@ def hex_int__valid(value): ("esp-idf", PLATFORM_ESP32, VARIANT_ESP32C6, "16", "16", "14", "14"), ("arduino", PLATFORM_ESP32, VARIANT_ESP32H2, "18", "17", "18", "17"), ("esp-idf", PLATFORM_ESP32, VARIANT_ESP32H2, "19", "19", "17", "17"), - ("arduino", PLATFORM_RP2040, None, "20", "20", "20", "20"), + ("arduino", PLATFORM_RP2, None, "20", "20", "20", "20"), ("arduino", PLATFORM_BK72XX, None, "21", "21", "21", "21"), ("arduino", PLATFORM_RTL87XX, None, "22", "22", "22", "22"), ("arduino", PLATFORM_LN882X, None, "23", "23", "23", "23"), @@ -469,7 +471,7 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) "esp32_c3": "11", "esp32_c6": "14", "esp32_h2": "17", - "rp2040": "20", + "rp2": "20", "bk72xx": "21", "rtl87xx": "22", "ln882x": "23", @@ -517,7 +519,7 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) ("arduino", PLATFORM_ESP32, "ESP32 using arduino framework"), ("esp-idf", PLATFORM_ESP32, "ESP32 using esp-idf framework"), ("arduino", PLATFORM_ESP8266, "ESP8266 using arduino framework"), - ("arduino", PLATFORM_RP2040, "RP2040 using arduino framework"), + ("arduino", PLATFORM_RP2, "RP2 using arduino framework"), ("arduino", PLATFORM_BK72XX, "BK72XX using arduino framework"), ("host", PLATFORM_HOST, "HOST using host framework"), ], @@ -540,7 +542,7 @@ def test_require_framework_version(framework, platform, message): esp_idf=cv.Version(0, 5, 0), esp32_arduino=cv.Version(0, 5, 0), esp8266_arduino=cv.Version(0, 5, 0), - rp2040_arduino=cv.Version(0, 5, 0), + rp2_arduino=cv.Version(0, 5, 0), bk72xx_arduino=cv.Version(0, 5, 0), host=cv.Version(0, 5, 0), extra_message="test 1", @@ -556,7 +558,7 @@ def test_require_framework_version(framework, platform, message): esp_idf=cv.Version(2, 0, 0), esp32_arduino=cv.Version(2, 0, 0), esp8266_arduino=cv.Version(2, 0, 0), - rp2040_arduino=cv.Version(2, 0, 0), + rp2_arduino=cv.Version(2, 0, 0), bk72xx_arduino=cv.Version(2, 0, 0), host=cv.Version(2, 0, 0), extra_message="test 2", @@ -567,7 +569,7 @@ def test_require_framework_version(framework, platform, message): esp_idf=cv.Version(1, 5, 0), esp32_arduino=cv.Version(1, 5, 0), esp8266_arduino=cv.Version(1, 5, 0), - rp2040_arduino=cv.Version(1, 5, 0), + rp2_arduino=cv.Version(1, 5, 0), bk72xx_arduino=cv.Version(1, 5, 0), host=cv.Version(1, 5, 0), max_version=True, @@ -584,7 +586,7 @@ def test_require_framework_version(framework, platform, message): esp_idf=cv.Version(0, 5, 0), esp32_arduino=cv.Version(0, 5, 0), esp8266_arduino=cv.Version(0, 5, 0), - rp2040_arduino=cv.Version(0, 5, 0), + rp2_arduino=cv.Version(0, 5, 0), bk72xx_arduino=cv.Version(0, 5, 0), host=cv.Version(0, 5, 0), max_version=True, @@ -599,6 +601,194 @@ def test_require_framework_version(framework, platform, message): )("test") +def _setup_core_for_framework(platform: str, framework: str) -> None: + """Wire CORE.data with the minimum keys for require_framework_version / + SplitDefault to evaluate without raising KeyError.""" + from esphome.const import ( + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + ) + + CORE.data[KEY_CORE] = { + KEY_TARGET_PLATFORM: platform, + KEY_TARGET_FRAMEWORK: framework, + KEY_FRAMEWORK_VERSION: cv.Version(1, 0, 0), + } + + +def test_only_on_rp2_passes_on_rp2_platform() -> None: + """``cv.only_on_rp2`` is the canonical family gate. It accepts any value + untouched when the configured platform is rp2.""" + _setup_core_for_framework(PLATFORM_RP2, "arduino") + assert cv.only_on_rp2("anything") == "anything" + + +def test_only_on_rp2_rejects_other_platforms() -> None: + """The same gate raises ``Invalid`` outside the rp2 platform.""" + _setup_core_for_framework(PLATFORM_ESP32, "arduino") + with pytest.raises(Invalid, match="rp2"): + cv.only_on_rp2("anything") + + +def test_only_on_rp2040_delegates_and_warns_once(caplog) -> None: + """``cv.only_on_rp2040`` is a deprecation shim — it logs a one-shot + warning, dedupes via CORE.data, and delegates to ``only_on_rp2``. + Repeated calls in the same run must not log again.""" + import logging + + _setup_core_for_framework(PLATFORM_RP2, "arduino") + # Reset the dedupe flag so this test is independent of order. + CORE.data.pop(cv._ONLY_ON_RP2040_DEPRECATED_KEY, None) + + with caplog.at_level(logging.WARNING, logger="esphome.config_validation"): + assert cv.only_on_rp2040("ok") == "ok" + first_warnings = [r for r in caplog.records if "only_on_rp2040" in r.message] + assert len(first_warnings) == 1 + assert "2027.7.0" in first_warnings[0].message + + # Second call dedupes — no additional warning is emitted. + assert cv.only_on_rp2040("ok") == "ok" + warnings_after_second = [ + r for r in caplog.records if "only_on_rp2040" in r.message + ] + assert len(warnings_after_second) == 1 + + +def test_only_on_rp2040_still_gates_on_non_rp2(caplog) -> None: + """The deprecation shim must still raise on non-rp2 platforms — it + delegates to ``only_on_rp2``, so the gating behavior is preserved.""" + import logging + + _setup_core_for_framework(PLATFORM_ESP32, "arduino") + CORE.data.pop(cv._ONLY_ON_RP2040_DEPRECATED_KEY, None) + + with ( + caplog.at_level(logging.WARNING, logger="esphome.config_validation"), + pytest.raises(Invalid, match="rp2"), + ): + cv.only_on_rp2040("anything") + + +def test_require_framework_version_esp32_variant_specific_key() -> None: + """ESP32 variant-specific kwargs (``esp32_c3_arduino``) must win over + the base ``esp32_arduino`` key when the configured variant matches.""" + from esphome.components.esp32 import KEY_ESP32 + from esphome.const import ( + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + KEY_VARIANT, + ) + + CORE.data[KEY_CORE] = { + KEY_TARGET_PLATFORM: PLATFORM_ESP32, + KEY_TARGET_FRAMEWORK: "arduino", + KEY_FRAMEWORK_VERSION: cv.Version(1, 2, 0), + } + CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32C3} + + # Variant-specific entry permits this version; base key would reject it. + assert ( + cv.require_framework_version( + esp32_arduino=cv.Version(5, 0, 0), # would reject + esp32_c3_arduino=cv.Version(1, 0, 0), # wins, ok + )("test") + == "test" + ) + + +def test_require_framework_version_rp2_variant_specific_key() -> None: + """RP2 variant kwargs (``rp2_2040_arduino``) must win over the base + ``rp2_arduino`` key when ``CORE.data['rp2']['variant']`` is wired.""" + from esphome.const import ( + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + ) + + CORE.data[KEY_CORE] = { + KEY_TARGET_PLATFORM: PLATFORM_RP2, + KEY_TARGET_FRAMEWORK: "arduino", + KEY_FRAMEWORK_VERSION: cv.Version(1, 2, 0), + } + CORE.data["rp2"] = {"variant": "RP2040"} + + # Variant key wins — base ``rp2_arduino`` (which would reject) is ignored. + assert ( + cv.require_framework_version( + rp2_arduino=cv.Version(5, 0, 0), # would reject + rp2_2040_arduino=cv.Version(1, 0, 0), # wins, ok + )("test") + == "test" + ) + + # Without a variant kwarg the base ``rp2_arduino`` is used (fallback). + CORE.data["rp2"] = {"variant": "RP2350"} + assert ( + cv.require_framework_version( + rp2_arduino=cv.Version(1, 0, 0), + )("test") + == "test" + ) + + +def test_split_default_rp2_variant_keys() -> None: + """``SplitDefault`` resolves ``rp2__`` first, falling + back to ``rp2_`` and ``rp2_`` before the base key.""" + from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM + + CORE.data[KEY_CORE] = { + KEY_TARGET_PLATFORM: PLATFORM_RP2, + KEY_TARGET_FRAMEWORK: "arduino", + } + CORE.data["rp2"] = {"variant": "RP2040"} + + schema = cv.Schema( + { + cv.SplitDefault( + "full", + rp2="base", + rp2_arduino="base-framework", + rp2_2040="variant-only", + rp2_2040_arduino="variant-framework", + ): str, + } + ) + # Most specific (variant + framework) wins. + assert schema({}).get("full") == "variant-framework" + + # Drop the most-specific kwarg → variant-only wins. + schema = cv.Schema( + { + cv.SplitDefault( + "full", + rp2="base", + rp2_arduino="base-framework", + rp2_2040="variant-only", + ): str, + } + ) + assert schema({}).get("full") == "variant-only" + + # RP2350 variant — no rp2_2350_* kwargs → fall through to base framework. + CORE.data["rp2"] = {"variant": "RP2350"} + schema = cv.Schema( + { + cv.SplitDefault( + "full", + rp2="base", + rp2_arduino="base-framework", + rp2_2040="not-this", + ): str, + } + ) + assert schema({}).get("full") == "base-framework" + + def test_only_with_single_component_loaded() -> None: """Test OnlyWith with single component when component is loaded.""" CORE.loaded_integrations = {"mqtt"} @@ -986,9 +1176,10 @@ def test_update_interval__never_passes_through() -> None: def test_optional_default_visibility_is_none() -> None: """An ``Optional`` with no ``visibility`` kwarg reports ``None``. - Consumers can read the attribute directly with plain attribute - access; absence (``None``) means "render on the editor's main - form." + The marker stays faithful to what the author wrote: ESPHome does + not encode the default on it. Resolving ``None`` to an effective + visibility is the consumer's job — a schema-aware editor treats an + unset ``Optional`` as ``ADVANCED`` (see :class:`Visibility`). """ o = cv.Optional("foo") assert o.visibility is None @@ -1006,6 +1197,17 @@ def test_optional_visibility_yaml_only() -> None: assert o.visibility is cv.Visibility.YAML_ONLY +def test_optional_visibility_ui() -> None: + """``visibility=Visibility.UI`` is recorded on the marker. + + ``UI`` promotes an ``Optional`` onto the editor's main form, + overriding the consumer's default of ``ADVANCED`` for unset + optionals. + """ + o = cv.Optional("foo", visibility=cv.Visibility.UI) + assert o.visibility is cv.Visibility.UI + + def test_visibility_str_values_match_dump_emission() -> None: """``Visibility`` is a ``StrEnum`` whose values are the literal strings the schema dumper emits. @@ -1015,6 +1217,7 @@ def test_visibility_str_values_match_dump_emission() -> None: field — pinning the on-the-wire spelling here keeps the dump contract stable. """ + assert str(cv.Visibility.UI) == "ui" assert str(cv.Visibility.ADVANCED) == "advanced" assert str(cv.Visibility.YAML_ONLY) == "yaml_only" @@ -1137,6 +1340,57 @@ def test_visibility_marker_is_per_field_no_mutation() -> None: assert inner_yaml_only.visibility is cv.Visibility.YAML_ONLY +def test_entity_metadata_visibility_hints() -> None: + """Entity and value-describing metadata is classified for visual editors. + + The headline ``name`` stays on the main form (``UI``); descriptive + metadata (device_class, unit, …), presentation options, and per-entity + integration plumbing (MQTT, web_server ordering) fall to the advanced + disclosure (``ADVANCED``). + """ + advanced = cv.Visibility.ADVANCED + + entity_base = {str(k): k for k in cv.ENTITY_BASE_SCHEMA.schema} + assert entity_base["name"].visibility is cv.Visibility.UI + for field in ( + "icon", + "internal", + "disabled_by_default", + "entity_category", + "device_id", + ): + assert entity_base[field].visibility is advanced, field + + mqtt = {str(k): k for k in cv.MQTT_COMPONENT_SCHEMA.schema} + for field in ("qos", "retain", "discovery", "state_topic", "availability"): + assert mqtt[field].visibility is advanced, field + + from esphome.components import binary_sensor, number, sensor + from esphome.components.web_server import WEBSERVER_SORTING_SCHEMA + + sensor_markers = {str(k): k for k in sensor.sensor_schema().schema} + for field in ( + "unit_of_measurement", + "accuracy_decimals", + "device_class", + "state_class", + "force_update", + ): + assert sensor_markers[field].visibility is advanced, field + + binary = {str(k): k for k in binary_sensor.binary_sensor_schema().schema} + assert binary["device_class"].visibility is advanced + + number_markers = {str(k): k for k in number.number_schema(number.Number).schema} + assert number_markers["mode"].visibility is advanced + assert number_markers["device_class"].visibility is advanced + + # The whole per-entity web_server block is advanced; children inherit + # via the consumer cascade, so only the parent key carries the hint. + web = {str(k): k for k in WEBSERVER_SORTING_SCHEMA.schema} + assert web["web_server"].visibility is advanced + + def _wrap_str(value: str) -> ESPHomeDataBase: """Wrap a raw string as an ESPHomeDataBase, mimicking a YAML-loaded value.""" return make_data_base(value) @@ -1184,9 +1438,41 @@ def test_version_parse_with_extra() -> None: assert version.extra == "dev20240101" -def test_version_parse_invalid() -> None: +def test_version_parse_without_patch() -> None: + """A two-part version parses with patch defaulting to 0, so framework + shorthands like '6.0' and '6.0-rc1' are accepted.""" + version = cv.Version.parse("6.0") + assert (version.major, version.minor, version.patch, version.extra) == ( + 6, + 0, + 0, + "", + ) + version = cv.Version.parse("6.0-rc1") + assert (version.major, version.minor, version.patch, version.extra) == ( + 6, + 0, + 0, + "rc1", + ) + + +def test_version_parse_numeric_extra() -> None: + """Four-part versions keep the trailing component as extra (pioarduino + packaging revisions, e.g. 5.5.3.1).""" + version = cv.Version.parse("5.5.3.1") + assert (version.major, version.minor, version.patch, version.extra) == ( + 5, + 5, + 3, + "1", + ) + + +@pytest.mark.parametrize("value", ["not.a.version", "6", "a.b", ""]) +def test_version_parse_invalid(value: str) -> None: with pytest.raises(ValueError, match="Not a valid version number"): - cv.Version.parse("not.a.version") + cv.Version.parse(value) def test_version_is_beta() -> None: @@ -2142,6 +2428,58 @@ def test_one_of_string_and_space() -> None: assert cv.one_of("a_b", string=True, space="_")("a b") == "a_b" +def test_one_of_string_and_underscore() -> None: + assert cv.one_of("a-b", string=True, underscore="-")("a_b") == "a-b" + assert cv.one_of("a-b", string=True, underscore="-")("a-b") == "a-b" + + +def test_one_of_string_lower_space_and_underscore() -> None: + validator = cv.one_of("output-mode", lower=True, space="-", underscore="-") + assert validator("output_mode") == "output-mode" + assert validator("OUTPUT_MODE") == "output-mode" + assert validator("output mode") == "output-mode" + assert validator("output-mode") == "output-mode" + + +def test_one_of_string_underscore_unknown() -> None: + with pytest.raises(Invalid): + cv.one_of("a-b", string=True, underscore="-")("c_d") + + +def test_one_of_string_underscore_default_unchanged() -> None: + with pytest.raises(Invalid): + cv.one_of("a-b", string=True)("a_b") + + +def test_one_of_string_and_hyphen() -> None: + assert cv.one_of("a_b", string=True, hyphen="_")("a-b") == "a_b" + assert cv.one_of("a_b", string=True, hyphen="_")("a_b") == "a_b" + + +def test_one_of_string_lower_space_and_hyphen() -> None: + validator = cv.one_of("output_mode", lower=True, space="_", hyphen="_") + assert validator("output-mode") == "output_mode" + assert validator("OUTPUT-MODE") == "output_mode" + assert validator("output mode") == "output_mode" + assert validator("output_mode") == "output_mode" + + +def test_one_of_string_hyphen_unknown() -> None: + with pytest.raises(Invalid): + cv.one_of("a_b", string=True, hyphen="_")("c-d") + + +def test_one_of_string_hyphen_default_unchanged() -> None: + with pytest.raises(Invalid): + cv.one_of("a_b", string=True)("a-b") + + +def test_one_of_string_underscore_hyphen_swap_no_cascade() -> None: + validator = cv.one_of("a-b", "a_b", string=True, underscore="-", hyphen="_") + assert validator("a_b") == "a-b" + assert validator("a-b") == "a_b" + + def test_one_of_int() -> None: assert cv.one_of(1, 2, int=True)("2") == 2 @@ -2180,6 +2518,20 @@ def test_enum_valid() -> None: assert result.enum_value == 10 +def test_enum_valid_with_underscore() -> None: + mapping = {"a-b": 1} + result = cv.enum(mapping, string=True, underscore="-")("a_b") + assert result == "a-b" + assert result.enum_value == 1 + + +def test_enum_valid_with_hyphen() -> None: + mapping = {"a_b": 1} + result = cv.enum(mapping, string=True, hyphen="_")("a-b") + assert result == "a_b" + assert result.enum_value == 1 + + # --------------------------------------------------------------------------- # lambda_ / returning_lambda # --------------------------------------------------------------------------- @@ -2628,3 +2980,135 @@ def test_rename_key_present() -> None: def test_rename_key_absent() -> None: assert cv.rename_key("old", "new")({"other": 5}) == {"other": 5} + + +def test_rename_key_no_removed_in_is_silent( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING, logger="esphome.config_validation"): + assert cv.rename_key("old", "new")({"old": 5}) == {"new": 5} + assert not caplog.records + + +def test_rename_key_removed_in_renames_and_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING, logger="esphome.config_validation"): + result = cv.rename_key("old", "new", removed_in="2026.8.0")({"old": 5}) + assert result == {"new": 5} + assert "'old' is deprecated, use 'new'. Will be removed in 2026.8.0" in caplog.text + + +def test_rename_key_removed_in_absent_key_no_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING, logger="esphome.config_validation"): + result = cv.rename_key("old", "new", removed_in="2026.8.0")({"other": 5}) + assert result == {"other": 5} + assert not caplog.records + + +def test_rename_key_removed_in_with_component_prefixes_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING, logger="esphome.config_validation"): + result = cv.rename_key( + "old", "new", removed_in="2026.8.0", component="my_component" + )({"old": 5}) + assert result == {"new": 5} + assert ( + "[my_component] 'old' is deprecated, use 'new'. Will be removed in 2026.8.0" + in caplog.text + ) + + +def test_rename_key_both_keys_rejected() -> None: + with pytest.raises(Invalid, match="Cannot specify more than one of"): + cv.rename_key("old", "new")({"old": 5, "new": 6}) + + +def test_rename_key_both_keys_rejected_with_removed_in( + caplog: pytest.LogCaptureFixture, +) -> None: + with ( + caplog.at_level(logging.WARNING, logger="esphome.config_validation"), + pytest.raises(Invalid, match="Cannot specify more than one of"), + ): + cv.rename_key("old", "new", removed_in="2026.8.0")({"old": 5, "new": 6}) + assert not caplog.records + + +def test_file__existing_relative_path(setup_core: Path) -> None: + (setup_core / "partitions.csv").write_text("csv\n") + + assert cv.file_("partitions.csv") == setup_core / "partitions.csv" + + +def test_file__missing_raises(setup_core: Path) -> None: + with pytest.raises(Invalid, match="Could not find file"): + cv.file_("partitions.csv") + + +def test_file__remaps_bundle_absolute_path(setup_core: Path) -> None: + """A stale absolute path in an extracted bundle resolves to the bundled copy.""" + manifest = { + "manifest_version": 1, + "config_filename": "test.yaml", + "config_dir": "/original/config", + } + (setup_core / "manifest.json").write_text(json.dumps(manifest)) + (setup_core / "partitions.csv").write_text("csv\n") + + assert cv.file_("/original/config/partitions.csv") == setup_core / "partitions.csv" + + +def test_file__missing_absolute_path_without_bundle(setup_core: Path) -> None: + with pytest.raises(Invalid, match="Could not find file"): + cv.file_("/original/config/partitions.csv") + + +def test_file__remaps_windows_bundle_absolute_path(setup_core: Path) -> None: + """A bundle created on Windows resolves on a host with another layout.""" + manifest = { + "manifest_version": 1, + "config_filename": "test.yaml", + "config_dir": "C:\\Users\\nick\\esphome", + } + (setup_core / "manifest.json").write_text(json.dumps(manifest)) + (setup_core / "partitions.csv").write_text("csv\n") + + result = cv.file_("C:\\Users\\nick\\esphome\\partitions.csv") + + assert result == setup_core / "partitions.csv" + + +def test_directory_remaps_bundle_absolute_path(setup_core: Path) -> None: + """A stale absolute directory in an extracted bundle resolves to the bundled copy.""" + manifest = { + "manifest_version": 1, + "config_filename": "test.yaml", + "config_dir": "/original/config", + } + (setup_core / "manifest.json").write_text(json.dumps(manifest)) + (setup_core / "headers").mkdir() + + assert cv.directory("/original/config/headers") == setup_core / "headers" + + +def test_directory_missing_raises(setup_core: Path) -> None: + with pytest.raises(Invalid, match="Could not find directory"): + cv.directory("/original/config/headers") + + +def test_file__remapped_path_is_directory_raises(setup_core: Path) -> None: + """A remapped path that is a directory still fails file validation.""" + manifest = { + "manifest_version": 1, + "config_filename": "test.yaml", + "config_dir": "/original/config", + } + (setup_core / "manifest.json").write_text(json.dumps(manifest)) + (setup_core / "headers").mkdir() + + with pytest.raises(Invalid, match="is not a file"): + cv.file_("/original/config/headers") diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index a61b6ae7ae..0cb0c1f62d 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -591,6 +591,36 @@ class TestEsphomeCore: assert target.is_esp32 is False assert target.is_esp8266 is True + def test_is_rp2(self, target): + """The canonical RP2 family gate flips on for the rp2 platform.""" + target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "rp2"} + + assert target.is_rp2 is True + assert target.is_esp32 is False + assert target.is_esp8266 is False + + def test_is_rp2040_deprecated_alias_matches_is_rp2(self, target, caplog): + """``is_rp2040`` is kept as a deprecation shim that returns whatever + ``is_rp2`` returns; both must agree across platform values. A + one-shot deprecation warning is emitted on first access and + deduped via ``CORE.data`` for the rest of the run.""" + import logging + + target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "rp2"} + with caplog.at_level(logging.WARNING, logger="esphome.core"): + assert target.is_rp2040 is True + assert target.is_rp2040 == target.is_rp2 + + warnings = [r for r in caplog.records if "is_rp2040" in r.message] + assert len(warnings) == 1 + assert "2027.7.0" in warnings[0].message + + # Reset the dedupe so the False-platform branch also runs the shim. + target.data.pop("_core_is_rp2040_deprecated_warned", None) + target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "esp32"} + assert target.is_rp2040 is False + assert target.is_rp2040 == target.is_rp2 + def test_firmware_bin__default(self, target): """Default platforms produce //firmware.bin.""" target.name = "test-device" diff --git a/tests/unit_tests/test_espidf_component.py b/tests/unit_tests/test_espidf_component.py index a50024b8e9..879d98c0a7 100644 --- a/tests/unit_tests/test_espidf_component.py +++ b/tests/unit_tests/test_espidf_component.py @@ -1,3 +1,4 @@ +import glob import hashlib import json import os @@ -86,6 +87,48 @@ def test_collect_filtered_files_exclude(tmp_path): assert str(f2) not in result +def test_collect_filtered_files_exclude_pattern_in_subdir(tmp_path): + src = tmp_path / "lib" / "src" + src.mkdir(parents=True) + kept = src / "a.c" + excluded = src / "hasty.c" + kept.write_text("int a;") + excluded.write_text("int b;") + + result = collect_filtered_files(tmp_path, ["+", "-"]) + assert str(kept) in result + assert str(excluded) not in result + + +def test_collect_filtered_files_exclude_unnormalized_glob_output(tmp_path, monkeypatch): + # On Windows, glob keeps the pattern's literal separators for non-wildcard + # path components, so the "+" wildcard pattern and the "-" literal pattern + # yield the same file spelled differently and the exclude set difference + # misses it. Backslash is a regular filename character on POSIX (such paths + # fail the final is_file filter), so reproduce the unnormalized-output + # mismatch portably with dot segments, which normpath also collapses. + src = tmp_path / "lib" / "src" + src.mkdir(parents=True) + kept = src / "a.c" + excluded = src / "hasty.c" + kept.write_text("int a;") + excluded.write_text("int b;") + + real_glob = glob.glob + + def unnormalized_glob(pattern, recursive=False): + if "*" in pattern: + base = str(tmp_path) + return [base + "/lib/./src/a.c", base + "/lib/./src/hasty.c"] + return real_glob(pattern, recursive=recursive) + + monkeypatch.setattr(glob, "glob", unnormalized_glob) + + result = collect_filtered_files(tmp_path, ["+", "-"]) + assert [Path(r).name for r in result] == ["a.c"] + assert str(kept) in result + + def test_split_list_by_condition(): items = ["-Iinclude", "-Llib", "-Wall"] @@ -126,30 +169,98 @@ def test_generate_cmakelists_txt_with_flags(tmp_component, tmp_path): } content = generate_cmakelists_txt(tmp_component) - sep = "\\\\" if os.name == "nt" else "/" + # Paths are always emitted with forward slashes so the CMakeLists is + # portable; on Windows os.path.relpath would otherwise yield backslashes + # that break CMake's list re-parsing. assert ( content - == f"""idf_component_register( - SRCS "src{sep}main.c" + == """idf_component_register( + SRCS "src/main.c" INCLUDE_DIRS "src" - REQUIRES dep ${{ESPHOME_PROJECT_MANAGED_COMPONENTS}} ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}} + REQUIRES dep ${ESPHOME_PROJECT_MANAGED_COMPONENTS} ${ESPHOME_PROJECT_BUILTIN_COMPONENTS} ) -target_compile_options(${{COMPONENT_LIB}} PUBLIC +target_compile_options(${COMPONENT_LIB} PUBLIC "-DTEST" ) -target_compile_options(${{COMPONENT_LIB}} PRIVATE +target_compile_options(${COMPONENT_LIB} PRIVATE "-Wall" ) -target_link_directories(${{COMPONENT_LIB}} INTERFACE +target_link_directories(${COMPONENT_LIB} INTERFACE "lib" ) -target_link_libraries(${{COMPONENT_LIB}} INTERFACE +target_link_libraries(${COMPONENT_LIB} INTERFACE "mylib" ) """ ) +def test_generate_cmakelists_txt_uses_forward_slashes_on_windows( + tmp_component, monkeypatch: pytest.MonkeyPatch +) -> None: + # os.path.relpath yields backslash paths on Windows, which CMake rejects + # when it re-parses the SRCS list (e.g. "\b" in "src\backend" is an invalid + # character escape). Simulate that output and confirm the generated + # CMakeLists normalizes the separators to forward slashes. + src_dir = tmp_component.path / "src" / "backend" + src_dir.mkdir(parents=True) + (src_dir / "cipher.c").write_text("int f() {}") + + tmp_component.data = {} + + monkeypatch.setattr("esphome.espidf.component.os.sep", "\\") + monkeypatch.setattr( + "esphome.espidf.component.os.path.relpath", + lambda *args, **kwargs: "src\\backend\\cipher.c", + ) + + content = generate_cmakelists_txt(tmp_component) + + assert 'SRCS "src/backend/cipher.c"' in content + assert "\\" not in content + + +def test_generate_cmakelists_txt_multi_token_flag(tmp_component): + # PlatformIO shell-lexes each build.flags entry, so a single entry can + # carry a flag and its argument. The generated CMakeLists must emit them + # as separate compile options, not one argument with an embedded space. + src_dir = tmp_component.path / "src" + src_dir.mkdir() + (src_dir / "main.c").write_text("int main() {}") + + tmp_component.data = {"build": {"flags": ["-include cp_custom_alloc.h", "-DTEST"]}} + + content = generate_cmakelists_txt(tmp_component) + assert '"-include cp_custom_alloc.h"' not in content + assert ' "-include"\n "cp_custom_alloc.h"\n' in content + + +def test_generate_cmakelists_txt_space_separated_classified_flags(tmp_component): + # Space-separated -I/-L/-l entries routed to INCLUDE_DIRS and the link + # handling before the shlex split was added; splitting must not leak + # them into raw compile options. + src_dir = tmp_component.path / "src" + src_dir.mkdir() + (src_dir / "main.c").write_text("int main() {}") + (tmp_component.path / "extra_inc").mkdir() + + tmp_component.data = { + "build": {"flags": ["-I extra_inc", "-L extra_lib", "-l extralib", "-DTEST"]} + } + + content = generate_cmakelists_txt(tmp_component) + assert 'INCLUDE_DIRS "src" "extra_inc"' in content + assert 'target_link_directories(${COMPONENT_LIB} INTERFACE\n "extra_lib"\n)' in ( + content + ) + assert 'target_link_libraries(${COMPONENT_LIB} INTERFACE\n "extralib"\n)' in ( + content + ) + assert '"-I"' not in content + assert '"-L"' not in content + assert '"-l"' not in content + + def test_generate_cmakelists_txt_references_project_managed_components_variable( tmp_component: IDFComponent, ) -> None: @@ -372,6 +483,84 @@ def test_node_key_git_no_ref(): assert locator == ("https://github.com/foo/bar.git", None) +def test_node_key_url_in_name_is_git(): + # add_library("https://github.com/x/y", None): PlatformIO accepted a bare + # git URL as the library name, so the converter must too. + key, is_git, locator = _node_key( + "https://github.com/pstolarz/OneWireNg", None, None + ) + assert key == "pstolarz/OneWireNg" + assert is_git is True + assert locator == ("https://github.com/pstolarz/OneWireNg", None) + + +def test_node_key_url_in_name_with_ref(): + key, is_git, locator = _node_key( + "https://github.com/foo/bar.git#v1.2.3", None, None + ) + assert (key, is_git, locator) == ( + "foo/bar", + True, + ("https://github.com/foo/bar.git", "v1.2.3"), + ) + + +def test_node_key_url_in_name_git_plus_prefix(): + key, is_git, locator = _node_key("git+https://github.com/foo/bar", None, None) + assert (key, is_git, locator) == ( + "foo/bar", + True, + ("https://github.com/foo/bar", None), + ) + + +def test_node_key_git_plus_prefix_in_repository(): + _key, is_git, locator = _node_key("name", None, "git+https://github.com/foo/bar") + assert (is_git, locator) == (True, ("https://github.com/foo/bar", None)) + + +def test_node_key_custom_name_equals_url_is_git(): + key, is_git, locator = _node_key( + "OneWireNg=https://github.com/pstolarz/OneWireNg", None, None + ) + assert (key, is_git, locator) == ( + "pstolarz/OneWireNg", + True, + ("https://github.com/pstolarz/OneWireNg", None), + ) + + +def test_node_key_url_in_name_with_query_containing_equals(): + # A bare URL whose query string contains ``=`` must not be split by the + # CustomName=URL handling. + key, is_git, locator = _node_key("https://host/x/y.git?ref=main", None, None) + assert (key, is_git, locator) == ( + "x/y", + True, + ("https://host/x/y.git?ref=main", None), + ) + + +@pytest.mark.parametrize("name", ["http://[::1", "CustomName=http://[::1"]) +def test_node_key_malformed_url_in_name_raises(name: str) -> None: + # A name that was clearly meant to be a URL but does not parse must fail + # fast instead of degrading to a confusing registry lookup error. + with pytest.raises(RuntimeError, match="Invalid PIO library URL"): + _node_key(name, None, None) + + +def test_node_key_name_with_equals_but_no_url_is_registry(): + key, is_git, locator = _node_key("FOO=BAR", "1.0", None) + assert (key, is_git, locator) == ("FOO=BAR", False, (None, "FOO=BAR")) + + +def test_node_key_version_url_still_ignored_when_name_plain(): + # A version that is a URL is handled by the dependency walk, not here; + # a plain name must stay a registry spec regardless of version shape. + key, is_git, _locator = _node_key("bar", "https://github.com/foo/bar", None) + assert (key, is_git) == ("bar", False) + + def test_node_key_registry_owner_name(): key, is_git, locator = _node_key("foo/bar", "^1.0.0", None) assert (key, is_git, locator) == ("foo/bar", False, ("foo", "bar")) diff --git a/tests/unit_tests/test_espidf_framework.py b/tests/unit_tests/test_espidf_framework.py index c5d9ddbaf1..5912facbb3 100644 --- a/tests/unit_tests/test_espidf_framework.py +++ b/tests/unit_tests/test_espidf_framework.py @@ -3,10 +3,14 @@ # pylint: disable=protected-access from contextlib import contextmanager +import importlib.util import io import json import logging +import os from pathlib import Path +import runpy +import subprocess import sys import tarfile from types import SimpleNamespace @@ -15,7 +19,10 @@ from unittest.mock import patch import pytest from esphome.espidf.framework import ( + ESPHOME_STAMP_FILE, + STAMP_SCHEMA_VERSION, _ccache_env, + _check_esphome_idf_framework_install, _check_stamp, _check_windows_path_length, _clone_idf_with_submodules, @@ -25,7 +32,11 @@ from esphome.espidf.framework import ( _get_python_env_path, _get_python_version, _parse_git_source, + _patch_tools_json_demote_unused_tools, _patch_tools_json_for_linux_arm64, + _prefetch_idf_tool_archives, + _read_stamp, + _stamp_covers, _windows_long_paths_enabled, _write_idf_version_txt, _write_stamp, @@ -131,10 +142,17 @@ def test_parse_git_source_rejected(source: str) -> None: assert _parse_git_source(source) is None -def _make_idf_tree(framework_path: Path) -> None: - """Create the minimum tree _clone_idf_with_submodules sanity-checks for.""" +def _make_idf_tree(framework_path: Path, *, gitmodules: bool = True) -> None: + """Create the minimum tree _clone_idf_with_submodules sanity-checks for. + + ``gitmodules=False`` simulates a fork that vendors components in-tree + instead of declaring submodules; update_submodules skips the git call + when that file is missing. + """ (framework_path / "tools").mkdir(parents=True) (framework_path / "tools" / "idf_tools.py").write_text("# stub\n") + if gitmodules: + (framework_path / ".gitmodules").write_text("# stub\n") def test_clone_idf_with_submodules_without_ref(tmp_path: Path) -> None: @@ -208,6 +226,28 @@ def test_clone_idf_with_submodules_raises_when_tree_missing( ) +def test_clone_idf_accepts_flattened_fork_without_gitmodules( + tmp_path: Path, +) -> None: + """A fork that vendors components in-tree instead of as submodules is valid. + + No .gitmodules means the submodule step is skipped entirely. + """ + framework_path = tmp_path / "idf" + framework_path.mkdir() + _make_idf_tree(framework_path, gitmodules=False) + + with patch("esphome.git.run_git_command", return_value="") as run_git_command_mock: + _clone_idf_with_submodules( + framework_path, + "https://github.com/example/flattened-esp-idf.git", + None, + ) + + calls = [c.args[0] for c in run_git_command_mock.call_args_list] + assert not any(c[1] == "submodule" for c in calls) + + # --------------------------------------------------------------------------- # Helpers for _tar_extract_all hard-link prefix-stripping tests # --------------------------------------------------------------------------- @@ -310,6 +350,21 @@ class TestTarExtractHardLinkPrefixStripping: _IDF_VERSION = "5.1.2" +def _fake_download_from_mirrors( + mirrors: list[str], + substitutions: dict[str, str], + target: object, + **kwargs: object, +) -> str: + """Stand-in for download_from_mirrors that creates path targets, since + the framework code opens the downloaded tarball afterwards.""" + if isinstance(target, (str, os.PathLike)): + path = Path(target) + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + return "https://example.com/idf.tar.xz" + + @pytest.fixture def espidf_mocks(setup_core: Path): """Patch the heavy I/O of check_esp_idf_install and pre-create the framework dir.""" @@ -317,10 +372,10 @@ def espidf_mocks(setup_core: Path): # extracted-marker touch writes into. _get_framework_path(_IDF_VERSION).mkdir(parents=True, exist_ok=True) with ( - patch("esphome.espidf.framework.rmdir"), + patch("esphome.espidf.framework.rmdir") as rmdir_mock, patch( "esphome.espidf.framework.download_from_mirrors", - return_value="https://example.com/idf.tar.xz", + side_effect=_fake_download_from_mirrors, ) as download, patch("esphome.espidf.framework.archive_extract_all") as extract, patch("esphome.espidf.framework.create_venv") as venv, @@ -331,8 +386,11 @@ def espidf_mocks(setup_core: Path): patch("esphome.espidf.framework._clone_idf_with_submodules") as clone, patch("esphome.espidf.framework._write_idf_version_txt"), patch("esphome.espidf.framework._patch_tools_json_for_linux_arm64"), + patch("esphome.espidf.framework._patch_tools_json_demote_unused_tools"), + patch("esphome.espidf.framework._prefetch_idf_tool_archives"), patch("esphome.espidf.framework._write_stamp"), patch("esphome.espidf.framework._check_stamp", return_value=True), + patch("esphome.espidf.framework._stamp_covers", return_value=True), patch("esphome.espidf.framework._get_idf_version", return_value=_IDF_VERSION), patch("esphome.espidf.framework._get_python_version", return_value="3.11.0"), patch("esphome.espidf.framework.get_system_python_path", return_value="python"), @@ -344,6 +402,7 @@ def espidf_mocks(setup_core: Path): run_ok=run_ok, tool_paths=tool_paths, clone=clone, + rmdir=rmdir_mock, ) @@ -358,6 +417,27 @@ def test_check_esp_idf_install_fresh(espidf_mocks: SimpleNamespace) -> None: espidf_mocks.extract.assert_called_once() espidf_mocks.venv.assert_called_once() espidf_mocks.clone.assert_not_called() + # the tool download cache (/dist) is pruned after install + espidf_mocks.rmdir.assert_any_call( + get_idf_tools_path() / "dist", msg="Remove ESP-IDF tool download cache" + ) + + +def test_check_esp_idf_install_dist_prune_failure_ignored( + espidf_mocks: SimpleNamespace, +) -> None: + """A failure to prune the tool download cache must not fail the install.""" + tools_dist = get_idf_tools_path() / "dist" + + def rmdir_side_effect(directory: Path, msg: str | None = None) -> None: + if directory == tools_dist: + raise RuntimeError("cannot remove dist") + + espidf_mocks.rmdir.side_effect = rmdir_side_effect + + # install still succeeds despite the failed prune + framework_path, _ = check_esp_idf_install(_IDF_VERSION, force=True) + assert framework_path == _get_framework_path(_IDF_VERSION) def test_check_esp_idf_install_git_source(espidf_mocks: SimpleNamespace) -> None: @@ -389,6 +469,20 @@ def test_check_esp_idf_install_already_installed(espidf_mocks: SimpleNamespace) espidf_mocks.venv.assert_not_called() +def test_corrupt_tarball_removed_when_extraction_fails( + espidf_mocks: SimpleNamespace, +) -> None: + """A tarball that fails to extract (e.g. torn by an unclean shutdown) is + deleted so the next run re-downloads instead of failing forever.""" + espidf_mocks.extract.side_effect = RuntimeError("xz: unexpected end of input") + tarball = get_idf_tools_path() / "dist" / f"esp-idf-{_IDF_VERSION}.tar.xz" + + with pytest.raises(RuntimeError, match="unexpected end of input"): + check_esp_idf_install(_IDF_VERSION, force=True) + + assert not tarball.exists() + + def test_check_esp_idf_install_framework_failure(espidf_mocks: SimpleNamespace) -> None: """A failing idf_tools install raises.""" espidf_mocks.run_ok.side_effect = [False] @@ -426,13 +520,17 @@ def _mark_installed() -> None: def test_check_esp_idf_install_stamp_mismatch_reinstalls( espidf_mocks: SimpleNamespace, ) -> None: - """A stamp mismatch reinstalls tools (marker present, so no re-extract).""" + """A stamp mismatch reinstalls tools (marker present, so no re-extract). + + The python env is left alone: it depends on the framework version and + features, not on which toolchains are installed. + """ _mark_installed() - with patch("esphome.espidf.framework._check_stamp", return_value=False): + with patch("esphome.espidf.framework._stamp_covers", return_value=False): check_esp_idf_install(_IDF_VERSION) espidf_mocks.extract.assert_not_called() # marker present -> no re-extract - espidf_mocks.venv.assert_called_once() # tools reinstall -> venv rebuilt + espidf_mocks.venv.assert_not_called() # tools-only install -> venv kept def test_check_esp_idf_install_check_command_failure_reinstalls( @@ -445,7 +543,7 @@ def test_check_esp_idf_install_check_command_failure_reinstalls( check_esp_idf_install(_IDF_VERSION, features=["fb"]) espidf_mocks.extract.assert_not_called() - espidf_mocks.venv.assert_called_once() + espidf_mocks.venv.assert_not_called() # tools-only install -> venv kept def test_check_esp_idf_install_unknown_python_version_reinstalls( @@ -465,8 +563,8 @@ def test_check_esp_idf_install_python_stamp_mismatch_rebuilds_venv( ) -> None: """Framework stamp matches but the python-env stamp does not -> venv rebuilt.""" - # _check_stamp passes for the framework (no python_version key) and fails - # for the python env (carries python_version), so only the venv rebuilds. + # _check_stamp only guards the python env now (the framework uses + # _stamp_covers, patched True by the fixture); failing it rebuilds the venv. def stamp_ok(_stamp_file, info: dict) -> bool: return "python_version" not in info @@ -478,6 +576,174 @@ def test_check_esp_idf_install_python_stamp_mismatch_rebuilds_venv( espidf_mocks.venv.assert_called_once() +def _requested_stamp(targets: list[str], tools: list[str] | None = None) -> dict: + return { + "schema_version": STAMP_SCHEMA_VERSION, + "targets": targets, + "tools": tools or ["required"], + } + + +@pytest.mark.parametrize( + ("stored", "targets", "expected"), + [ + # a stored "all" covers any target + (_requested_stamp(["all"]), ["esp32"], True), + # exact match and superset both cover + (_requested_stamp(["esp32"]), ["esp32"], True), + (_requested_stamp(["esp32", "esp32c3"]), ["esp32"], True), + # a new target is not covered + (_requested_stamp(["esp32"]), ["esp32c3"], False), + # tools and schema_version must match exactly + (_requested_stamp(["all"], tools=["cmake", "required"]), ["esp32"], False), + (_requested_stamp(["all"]) | {"schema_version": "no"}, ["esp32"], False), + # an unknown extra field participates in invalidation by default + (_requested_stamp(["all"]) | {"module_version": 1}, ["esp32"], False), + # missing/corrupt stamps never cover + (None, ["esp32"], False), + ( + {"schema_version": STAMP_SCHEMA_VERSION, "tools": ["required"]}, + ["esp32"], + False, + ), + ], +) +def test_stamp_covers(stored: dict | None, targets: list[str], expected: bool) -> None: + assert _stamp_covers(stored, _requested_stamp(targets)) is expected + + +@contextmanager +def _framework_install_patches(): + """Patches for calling _check_esphome_idf_framework_install directly with + real stamp files (unlike espidf_mocks, which stubs the stamp layer).""" + with ( + patch("esphome.espidf.framework.run_command_ok", return_value=True) as run_ok, + patch("esphome.espidf.framework._get_idf_tool_paths", return_value=([], {})), + patch("esphome.espidf.framework.get_system_python_path", return_value="python"), + patch("esphome.espidf.framework.rmdir"), + ): + yield run_ok + + +def _extracted_framework_with_stamp(stamp: dict) -> Path: + framework_path = _get_framework_path(_IDF_VERSION) + framework_path.mkdir(parents=True, exist_ok=True) + (framework_path / ".esphome_extracted").touch() + _write_stamp(framework_path / ESPHOME_STAMP_FILE, stamp) + return framework_path + + +def test_framework_install_target_subset_skips_install() -> None: + """A stamp holding a superset of the requested targets skips the installer.""" + framework_path = _extracted_framework_with_stamp(_requested_stamp(["all"])) + + with _framework_install_patches() as run_ok: + _, fresh_extract = _check_esphome_idf_framework_install( + _IDF_VERSION, ["esp32"], ["required"] + ) + + run_ok.assert_not_called() + assert fresh_extract is False + # the stamp is untouched + stamp = json.loads((framework_path / ESPHOME_STAMP_FILE).read_text()) + assert stamp["targets"] == ["all"] + + +def test_framework_install_new_target_installs_and_merges_stamp() -> None: + """A new target runs the installer for just that target and the stamp + records the union of everything installed so far.""" + framework_path = _extracted_framework_with_stamp(_requested_stamp(["esp32"])) + + with _framework_install_patches() as run_ok: + _, fresh_extract = _check_esphome_idf_framework_install( + _IDF_VERSION, ["esp32c3"], ["required"] + ) + + assert fresh_extract is False + assert "--targets=esp32c3" in run_ok.call_args[0][0] + stamp = json.loads((framework_path / ESPHOME_STAMP_FILE).read_text()) + assert stamp["targets"] == ["esp32", "esp32c3"] + + +def test_check_esp_idf_install_env_targets_override_wins( + espidf_mocks: SimpleNamespace, +) -> None: + """An explicitly set ESPHOME_IDF_DEFAULT_TARGETS overrides per-variant targets.""" + with patch("esphome.espidf.framework._IDF_DEFAULT_TARGETS_EXPLICIT", True): + check_esp_idf_install(_IDF_VERSION, force=True, targets=["esp32"]) + + install_cmd = espidf_mocks.run_ok.call_args_list[0][0][0] + assert "--targets=all" in install_cmd + + +def test_check_esp_idf_install_uses_requested_targets( + espidf_mocks: SimpleNamespace, +) -> None: + """Without the env override, the caller's per-variant targets are installed.""" + check_esp_idf_install(_IDF_VERSION, force=True, targets=["esp32"]) + + install_cmd = espidf_mocks.run_ok.call_args_list[0][0][0] + assert "--targets=esp32" in install_cmd + + +def test_framework_install_all_request_collapses_merged_stamp_to_all() -> None: + """Requesting "all" over a per-variant stamp merges and collapses to + ["all"], not ["all", "esp32"], so the stamp shape stays canonical.""" + framework_path = _extracted_framework_with_stamp(_requested_stamp(["esp32"])) + + with _framework_install_patches() as run_ok: + _check_esphome_idf_framework_install(_IDF_VERSION, ["all"], ["required"]) + + run_ok.assert_called_once() + stamp = json.loads((framework_path / ESPHOME_STAMP_FILE).read_text()) + assert stamp["targets"] == ["all"] + + +def test_framework_install_tools_change_resets_stamp_targets() -> None: + """A reinstall triggered by a tools change must not carry the old stamp's + targets forward: the installer only ran for this build's targets, so a + merged stamp would let other variants skip the reinstall they need.""" + framework_path = _extracted_framework_with_stamp( + _requested_stamp(["all"], tools=["cmake", "required"]) + ) + + with _framework_install_patches() as run_ok: + _check_esphome_idf_framework_install(_IDF_VERSION, ["esp32"], ["required"]) + + run_ok.assert_called_once() + stamp = json.loads((framework_path / ESPHOME_STAMP_FILE).read_text()) + assert stamp["targets"] == ["esp32"] + assert stamp["tools"] == ["required"] + + +@pytest.mark.parametrize( + ("lib", "expect_hint"), + [ + (None, True), + ("libusb-1.0.so.0", False), + ], +) +def test_check_esp_idf_install_failure_libusb_hint( + espidf_mocks: SimpleNamespace, + caplog: pytest.LogCaptureFixture, + lib: str | None, + expect_hint: bool, +) -> None: + """A failed tools install only shows the libusb hint when libusb-1.0 is + actually missing.""" + espidf_mocks.run_ok.return_value = False + # Fake Linux so the gate is exercised on all CI hosts; faking Linux is safe + # everywhere (unlike faking Windows, which pulls in winreg on other hosts) + with ( + patch("esphome.espidf.framework.find_library", return_value=lib), + patch("esphome.espidf.framework.platform.system", return_value="Linux"), + caplog.at_level(logging.ERROR, logger="esphome.espidf.framework"), + pytest.raises(RuntimeError, match="framework installation failure"), + ): + check_esp_idf_install(_IDF_VERSION, force=True) + assert ("libusb-1.0.so.0 was not found" in caplog.text) == expect_hint + + def test_check_esp_idf_install_unparseable_version( espidf_mocks: SimpleNamespace, ) -> None: @@ -489,6 +755,29 @@ def test_check_esp_idf_install_unparseable_version( espidf_mocks.extract.assert_called_once() +@pytest.mark.parametrize( + ("version", "short_version"), + [ + ("6.0.0", "6.0"), + ("6.0.0-rc1", "6.0-rc1"), + ("5.5.4", None), # vX.Y tags only exist for X.Y.0 releases + ], +) +def test_check_esp_idf_install_short_version_substitution( + espidf_mocks: SimpleNamespace, version: str, short_version: str | None +) -> None: + """SHORT_VERSION is only offered for x.y.0 releases, so the vX.Y mirror + template is never tried for versions whose tag cannot exist.""" + _get_framework_path(version).mkdir(parents=True, exist_ok=True) + check_esp_idf_install(version, force=True) + + # First call downloads the framework archive; a later call fetches the + # constraints file with its own substitutions. + substitutions = espidf_mocks.download.call_args_list[0][0][1] + assert substitutions.get("SHORT_VERSION") == short_version + assert substitutions["VERSION"] == version + + # --------------------------------------------------------------------------- # _patch_tools_json_for_linux_arm64 (arm64-only ninja backport) # --------------------------------------------------------------------------- @@ -561,6 +850,415 @@ def test_patch_tools_json_already_patched_is_noop(tmp_path: Path) -> None: assert tools_json.read_text(encoding="utf-8") == before +# --------------------------------------------------------------------------- +# _prefetch_idf_tool_archives +# --------------------------------------------------------------------------- + + +_PREFETCH_JSON = json.dumps( + [ + { + "name": "cmake@3.30.2", + "url": "https://example.com/cmake.tar.gz", + "size": 123, + "sha256": "ab" * 32, + "dest": "cmake-3.30.2.tar.gz", + }, + { + "name": "ninja@1.12.1", + "url": "https://example.com/ninja.zip", + "size": 45, + "sha256": "cd" * 32, + "dest": "ninja.zip", + }, + ] +) + + +def test_prefetch_downloads_each_archive_with_resume(tmp_path: Path) -> None: + with ( + patch( + "esphome.espidf.framework.run_command", + return_value=(True, _PREFETCH_JSON, ""), + ), + patch("esphome.espidf.framework.download_with_resume") as download, + patch("esphome.espidf.framework.get_system_python_path", return_value="python"), + ): + _prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None) + + dist = get_idf_tools_path() / "dist" + assert download.call_count == 2 + assert download.call_args_list[0][0] == ( + "https://example.com/cmake.tar.gz", + dist / "cmake-3.30.2.tar.gz", + ) + assert download.call_args_list[0][1] == {"sha256": "ab" * 32, "size": 123} + + +def test_prefetch_skips_already_downloaded_archives(tmp_path: Path) -> None: + dist = get_idf_tools_path() / "dist" + dist.mkdir(parents=True) + (dist / "cmake-3.30.2.tar.gz").write_bytes(b"cached") + with ( + patch( + "esphome.espidf.framework.run_command", + return_value=(True, _PREFETCH_JSON, ""), + ), + patch("esphome.espidf.framework.download_with_resume") as download, + patch("esphome.espidf.framework.get_system_python_path", return_value="python"), + ): + _prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None) + + # only the missing archive is downloaded + assert download.call_count == 1 + assert download.call_args[0][1] == dist / "ninja.zip" + + +@pytest.mark.parametrize( + ("run_result", "download_error", "expected_log"), + [ + ((False, "", "script exploded"), None, "tool downloads"), # script failure + ((True, "{ not json", ""), None, "prefetch failed"), # unparsable output + ( + (True, _PREFETCH_JSON, ""), + OSError("network down"), + "Could not prefetch", + ), # download failure + ], +) +def test_prefetch_failures_never_raise( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + run_result: tuple[bool, str, str], + download_error: Exception | None, + expected_log: str, +) -> None: + """The prefetch is best-effort; idf_tools downloads whatever is missing.""" + with ( + patch("esphome.espidf.framework.run_command", return_value=run_result), + patch( + "esphome.espidf.framework.download_with_resume", + side_effect=download_error, + ), + patch("esphome.espidf.framework.get_system_python_path", return_value="python"), + ): + _prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None) + + assert expected_log in caplog.text + + +def test_prefetch_one_failed_archive_does_not_stop_the_rest( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A single archive failing its download must not abort the prefetch of + the remaining archives.""" + with ( + patch( + "esphome.espidf.framework.run_command", + return_value=(True, _PREFETCH_JSON, ""), + ), + patch( + "esphome.espidf.framework.download_with_resume", + side_effect=[OSError("network down"), None], + ) as download, + patch("esphome.espidf.framework.get_system_python_path", return_value="python"), + ): + _prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None) + + assert download.call_count == 2 + assert "Could not prefetch cmake@3.30.2" in caplog.text + + +def test_prefetch_passes_targets_and_tools_to_script(tmp_path: Path) -> None: + with ( + patch( + "esphome.espidf.framework.run_command", return_value=(True, "[]", "") + ) as run, + patch("esphome.espidf.framework.get_system_python_path", return_value="python"), + ): + _prefetch_idf_tool_archives( + tmp_path, "esp32,esp32c3", ["required", "cmake"], {"IDF_TOOLS_PATH": "/x"} + ) + + cmd = run.call_args[0][0] + assert cmd[-3:] == ["esp32,esp32c3", "required", "cmake"] + assert cmd[1].endswith("get_tool_downloads.py") + # the script inherits the caller's env plus the framework tools PYTHONPATH + env = run.call_args[1]["env"] + assert env["IDF_TOOLS_PATH"] == "/x" + assert env["PYTHONPATH"] == str(tmp_path / "tools") + + +def test_framework_install_prefetches_before_installer( + espidf_mocks: SimpleNamespace, +) -> None: + """The prefetch runs before idf_tools.py install so the installer finds + the archives already in dist/.""" + calls: list[str] = [] + with ( + patch( + "esphome.espidf.framework._prefetch_idf_tool_archives", + side_effect=lambda *a, **k: calls.append("prefetch"), + ), + ): + espidf_mocks.run_ok.side_effect = lambda *a, **k: ( + calls.append("install") or True + ) + check_esp_idf_install(_IDF_VERSION, force=True) + + assert calls.index("prefetch") < calls.index("install") + + +# --------------------------------------------------------------------------- +# get_tool_downloads.py (against the stub idf_tools module in fixtures/) +# --------------------------------------------------------------------------- + + +_IDF_TOOLS_STUB_DIR = Path(__file__).parent / "fixtures" / "idf_tools_stub" + + +def _run_downloads_script( + tmp_path: Path, *args: str, env_extra: dict[str, str] | None = None +) -> subprocess.CompletedProcess[str]: + """Run the real get_tool_downloads.py against the stub idf_tools module.""" + script = Path(__file__).parents[2] / "esphome" / "espidf" / "get_tool_downloads.py" + env = os.environ | { + "PYTHONPATH": str(_IDF_TOOLS_STUB_DIR), + "IDF_TOOLS_PATH": str(tmp_path / "tp"), + } + if env_extra: + env |= env_extra + return subprocess.run( + [sys.executable, str(script), str(tmp_path / "fw"), *args], + capture_output=True, + text=True, + env=env, + check=False, + ) + + +def test_get_tool_downloads_lists_missing_tools(tmp_path: Path) -> None: + """Installed versions are skipped, tools that fail their binary check are + still listed, rename_dist decides the dist filename, and idf_tools' stdout + chatter stays off the JSON channel.""" + result = _run_downloads_script(tmp_path, "esp32", "required") + + assert result.returncode == 0, result.stderr + downloads = {d["name"]: d for d in json.loads(result.stdout)} + # installed-tool@1.0 is already installed and must not be listed + assert set(downloads) == {"cmake@3.30.2", "ninja@1.12.1", "broken-tool@2.0"} + assert downloads["cmake@3.30.2"]["dest"] == "cmake.tar.gz" + assert downloads["cmake@3.30.2"]["size"] == 11 + assert downloads["cmake@3.30.2"]["sha256"] == "aa" + # rename_dist overrides the URL basename + assert downloads["ninja@1.12.1"]["dest"] == "ninja-v1.zip" + # the stub prints informational lines; they must be on stderr + assert "Changed download URL" in result.stderr + + +def test_get_tool_downloads_applies_mirror_rewrite(tmp_path: Path) -> None: + result = _run_downloads_script( + tmp_path, + "esp32", + "required", + env_extra={"TEST_MIRROR_PREFIX": "https://mirror.test/"}, + ) + + assert result.returncode == 0, result.stderr + downloads = json.loads(result.stdout) + assert all(d["url"].startswith("https://mirror.test/") for d in downloads) + + +def _run_downloads_inprocess( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + *args: str, +) -> list[dict]: + """Execute get_tool_downloads.py in-process against the stub idf_tools. + + Unlike the subprocess variant this runs under coverage, exercising the + script's own lines. + """ + spec = importlib.util.spec_from_file_location( + "idf_tools", _IDF_TOOLS_STUB_DIR / "idf_tools.py" + ) + stub = importlib.util.module_from_spec(spec) + spec.loader.exec_module(stub) + monkeypatch.setitem(sys.modules, "idf_tools", stub) + monkeypatch.setenv("IDF_TOOLS_PATH", str(tmp_path / "tp")) + script = Path(__file__).parents[2] / "esphome" / "espidf" / "get_tool_downloads.py" + monkeypatch.setattr(sys, "argv", [str(script), str(tmp_path / "fw"), *args]) + runpy.run_path(str(script)) + return json.loads(capsys.readouterr().out) + + +def test_get_tool_downloads_inprocess_full_flow( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """In-process run covering the whole script: required expansion, + installed/broken tools, rename_dist, and version pinning via tool@version.""" + downloads = { + d["name"]: d + for d in _run_downloads_inprocess( + tmp_path, monkeypatch, capsys, "esp32", "required" + ) + } + assert set(downloads) == {"cmake@3.30.2", "ninja@1.12.1", "broken-tool@2.0"} + assert downloads["ninja@1.12.1"]["dest"] == "ninja-v1.zip" + assert downloads["cmake@3.30.2"]["url"] == "https://gh.test/cmake.tar.gz" + + +def test_get_tool_downloads_inprocess_explicit_tool_specs( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Explicit tool names and tool@version specs resolve; unknown tools and + unknown versions are skipped.""" + downloads = _run_downloads_inprocess( + tmp_path, + monkeypatch, + capsys, + "esp32", + "cmake@3.30.2", + "no-such-tool", + "cmake@9.9.9", + ) + assert [d["name"] for d in downloads] == ["cmake@3.30.2"] + + +# --------------------------------------------------------------------------- +# _patch_tools_json_demote_unused_tools (openocd, gdb, ULP toolchain optional) +# --------------------------------------------------------------------------- + + +def test_demote_unused_tools_patches_install_type(tmp_path: Path) -> None: + tools_json = _write_tools_json( + tmp_path, + { + "tools": [ + {"name": "openocd-esp32", "install": "always"}, + {"name": "xtensa-esp-elf-gdb", "install": "always"}, + {"name": "riscv32-esp-elf-gdb", "install": "always"}, + {"name": "esp32ulp-elf", "install": "always"}, + {"name": "xtensa-esp-elf", "install": "always"}, + {"name": "esp-rom-elfs", "install": "always"}, + ] + }, + ) + _patch_tools_json_demote_unused_tools(tmp_path) + + data = json.loads(tools_json.read_text(encoding="utf-8")) + install_types = {t["name"]: t["install"] for t in data["tools"]} + assert install_types == { + "openocd-esp32": "on_request", + "xtensa-esp-elf-gdb": "on_request", + "riscv32-esp-elf-gdb": "on_request", + "esp32ulp-elf": "on_request", + # the compiler toolchain and ROM ELFs stay required + "xtensa-esp-elf": "always", + "esp-rom-elfs": "always", + } + + +def test_demote_unused_tools_drops_xtensa_from_riscv_targets(tmp_path: Path) -> None: + """riscv32-esp-elf loses the xtensa chips (ULP-RISC-V only, which ESPHome + never builds) but keeps its RISC-V targets; other tools are untouched.""" + tools_json = _write_tools_json( + tmp_path, + { + "tools": [ + { + "name": "riscv32-esp-elf", + "install": "always", + "supported_targets": ["esp32s2", "esp32s3", "esp32c3", "esp32p4"], + }, + { + "name": "xtensa-esp-elf", + "install": "always", + "supported_targets": ["esp32", "esp32s2", "esp32s3"], + }, + ] + }, + ) + _patch_tools_json_demote_unused_tools(tmp_path) + + data = json.loads(tools_json.read_text(encoding="utf-8")) + riscv = next(t for t in data["tools"] if t["name"] == "riscv32-esp-elf") + xtensa = next(t for t in data["tools"] if t["name"] == "xtensa-esp-elf") + assert riscv["supported_targets"] == ["esp32c3", "esp32p4"] + assert riscv["install"] == "always" + assert xtensa["supported_targets"] == ["esp32", "esp32s2", "esp32s3"] + + +def test_demote_unused_tools_bad_supported_targets_type_still_demotes( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A non-list supported_targets on riscv32-esp-elf must not abort the + other demotions; the targets patch is best-effort and logs the skip so a + silently resumed riscv download is diagnosable.""" + tools_json = _write_tools_json( + tmp_path, + { + "tools": [ + { + "name": "riscv32-esp-elf", + "install": "always", + "supported_targets": None, + }, + {"name": "openocd-esp32", "install": "always"}, + ] + }, + ) + with caplog.at_level(logging.WARNING, logger="esphome.espidf.framework"): + _patch_tools_json_demote_unused_tools(tmp_path) + + data = json.loads(tools_json.read_text(encoding="utf-8")) + openocd = next(t for t in data["tools"] if t["name"] == "openocd-esp32") + riscv = next(t for t in data["tools"] if t["name"] == "riscv32-esp-elf") + assert openocd["install"] == "on_request" + assert riscv["supported_targets"] is None + assert "Unexpected supported_targets" in caplog.text + + +def test_patch_tools_json_unexpected_structure_warns_and_skips( + tmp_path: Path, +) -> None: + """Valid JSON with an unexpected shape must skip the patch, not raise.""" + tools_dir = tmp_path / "tools" + tools_dir.mkdir() + tools_json = tools_dir / "tools.json" + tools_json.write_text('["not", "a", "dict"]', encoding="utf-8") + before = tools_json.read_text(encoding="utf-8") + _patch_tools_json_demote_unused_tools(tmp_path) # AttributeError -> skip + assert tools_json.read_text(encoding="utf-8") == before + + +def test_demote_unused_tools_already_patched_is_noop(tmp_path: Path) -> None: + tools_json = _write_tools_json( + tmp_path, + { + "tools": [ + {"name": "openocd-esp32", "install": "on_request"}, + {"name": "xtensa-esp-elf-gdb", "install": "on_request"}, + {"name": "riscv32-esp-elf-gdb", "install": "on_request"}, + {"name": "esp32ulp-elf", "install": "on_request"}, + { + "name": "riscv32-esp-elf", + "install": "always", + "supported_targets": ["esp32c3", "esp32p4"], + }, + ] + }, + ) + before = tools_json.read_text(encoding="utf-8") + _patch_tools_json_demote_unused_tools(tmp_path) + assert tools_json.read_text(encoding="utf-8") == before + + # --------------------------------------------------------------------------- # Subprocess-backed helpers (_exec -> run_command rename) and get_framework_env # --------------------------------------------------------------------------- @@ -787,6 +1485,54 @@ def test_check_stamp_corrupt_file(tmp_path: Path) -> None: assert _check_stamp(f, {"a": "1"}) is False +def test_read_stamp_corrupt_file_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + # A corrupt stamp forces a full reinstall on every build, so it warns + # where the normal missing-file case stays silent. + f = tmp_path / "s.json" + f.write_text("{ not json", encoding="utf-8") + with caplog.at_level(logging.WARNING, logger="esphome.espidf.framework"): + assert _read_stamp(f) is None + assert "Ignoring corrupt stamp file" in caplog.text + + +def test_read_stamp_unreadable_file_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + # An I/O fault (permissions, disk error) is distinguished from a simply + # missing stamp with a warning before falling back to reinstall. + f = tmp_path / "s.json" + f.write_text(json.dumps({"a": "1"}), encoding="utf-8") + with ( + patch.object(Path, "open", side_effect=PermissionError("denied")), + caplog.at_level(logging.WARNING, logger="esphome.espidf.framework"), + ): + assert _read_stamp(f) is None + assert "Could not read stamp file" in caplog.text + + +def test_read_stamp_non_dict_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + # Well-formed JSON that is not an object is a fault, not a first install; + # it must leave a trace before forcing reinstalls. + f = tmp_path / "s.json" + f.write_text("null", encoding="utf-8") + with caplog.at_level(logging.WARNING, logger="esphome.espidf.framework"): + assert _read_stamp(f) is None + assert "unexpected type NoneType" in caplog.text + + +def test_read_stamp_missing_file_is_silent( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + # Missing stamps are the normal first-install case and must not log. + with caplog.at_level(logging.DEBUG, logger="esphome.espidf.framework"): + assert _read_stamp(tmp_path / "nope.json") is None + assert "stamp file" not in caplog.text + + def test_write_idf_version_txt_writes_when_missing(tmp_path: Path) -> None: _write_idf_version_txt(tmp_path, "5.1.2") assert (tmp_path / "version.txt").read_text(encoding="utf-8") == "v5.1.2\n" diff --git a/tests/unit_tests/test_espidf_toolchain.py b/tests/unit_tests/test_espidf_toolchain.py index 017d8c49b4..56f358a24c 100644 --- a/tests/unit_tests/test_espidf_toolchain.py +++ b/tests/unit_tests/test_espidf_toolchain.py @@ -5,10 +5,19 @@ import json import os from pathlib import Path +import subprocess from unittest.mock import patch -from esphome.const import CONF_FRAMEWORK, CONF_SOURCE -from esphome.core import CORE +import pytest + +from esphome.components.esp32.const import KEY_ESP32, KEY_VARIANT +from esphome.const import ( + CONF_COMPILE_PROCESS_LIMIT, + CONF_ESPHOME, + CONF_FRAMEWORK, + CONF_SOURCE, +) +from esphome.core import CORE, EsphomeError from esphome.espidf import toolchain @@ -47,7 +56,7 @@ def test_get_esphome_esp_idf_paths_forwards_source_override(): toolchain, "check_esp_idf_install", return_value=("/fw", "/penv") ) as mock_install: toolchain._get_esphome_esp_idf_paths("5.5.4") - mock_install.assert_called_once_with("5.5.4", source_url=url) + mock_install.assert_called_once_with("5.5.4", targets=None, source_url=url) def test_get_esphome_esp_idf_paths_no_override(): @@ -58,7 +67,28 @@ def test_get_esphome_esp_idf_paths_no_override(): toolchain, "check_esp_idf_install", return_value=("/fw", "/penv") ) as mock_install: toolchain._get_esphome_esp_idf_paths("5.5.4") - mock_install.assert_called_once_with("5.5.4", source_url=None) + mock_install.assert_called_once_with("5.5.4", targets=None, source_url=None) + + +def test_get_configured_targets_from_variant(monkeypatch: pytest.MonkeyPatch): + """The configured variant restricts the toolchain install to its target.""" + monkeypatch.delenv("CI", raising=False) + CORE.data[KEY_ESP32] = {KEY_VARIANT: "ESP32S3"} + assert toolchain._get_configured_targets() == ["esp32s3"] + + +def test_get_configured_targets_without_variant(monkeypatch: pytest.MonkeyPatch): + """No stored variant (e.g. tooling outside a build) keeps the default.""" + monkeypatch.delenv("CI", raising=False) + CORE.data.pop(KEY_ESP32, None) + assert toolchain._get_configured_targets() is None + + +def test_get_configured_targets_ci_installs_all(monkeypatch: pytest.MonkeyPatch): + """CI installs every target so the shared cache covers all variants.""" + monkeypatch.setenv("CI", "true") + CORE.data[KEY_ESP32] = {KEY_VARIANT: "ESP32S3"} + assert toolchain._get_configured_targets() is None def _setup_build(setup_core: Path) -> tuple[Path, Path]: @@ -100,7 +130,7 @@ def test_get_idedata_uses_cache_when_valid(setup_core: Path) -> None: compile_commands.parent.mkdir(parents=True, exist_ok=True) compile_commands.write_text("[]") cache.parent.mkdir(parents=True, exist_ok=True) - cache.write_text('{"cxx_path": "cached"}') + cache.write_text('{"cc_path": "cached-gcc", "cxx_path": "cached"}') cc_mtime = compile_commands.stat().st_mtime os.utime(cache, (cc_mtime + 1, cc_mtime + 1)) @@ -108,7 +138,31 @@ def test_get_idedata_uses_cache_when_valid(setup_core: Path) -> None: result = toolchain.get_idedata() mock_transform.assert_not_called() - assert result == {"cxx_path": "cached"} + assert result == {"cc_path": "cached-gcc", "cxx_path": "cached"} + + +def test_get_idedata_regenerates_cache_without_cc_path(setup_core: Path) -> None: + """A cache predating cc_path is rebuilt even though it is newer. + + Such a cache stays newer than the compile DB forever, so consumers that + derive the binutils paths from cc_path would keep failing on it. + """ + compile_commands, cache = _setup_build(setup_core) + compile_commands.parent.mkdir(parents=True, exist_ok=True) + compile_commands.write_text("[]") + cache.parent.mkdir(parents=True, exist_ok=True) + cache.write_text('{"cxx_path": "cached"}') + cc_mtime = compile_commands.stat().st_mtime + os.utime(cache, (cc_mtime + 1, cc_mtime + 1)) + + with patch( + "esphome.espidf.idedata.idedata_from_build", + return_value={"cc_path": "gcc", "cxx_path": "g++"}, + ) as mock_transform: + result = toolchain.get_idedata() + + mock_transform.assert_called_once() + assert result["cc_path"] == "gcc" def test_get_idedata_regenerates_when_compile_commands_newer(setup_core: Path) -> None: @@ -131,6 +185,33 @@ def test_get_idedata_regenerates_when_compile_commands_newer(setup_core: Path) - assert result == {"cxx_path": "fresh", "prog_path": str(toolchain.get_elf_path())} +@pytest.mark.parametrize("cached", ['"cc_path is a string"', "[]", "42"]) +def test_get_idedata_regenerates_on_non_dict_cache( + setup_core: Path, cached: str +) -> None: + """A newer cache holding valid JSON that is not an object is regenerated. + + A bare string would otherwise pass the cc_path check by substring and be + handed to consumers expecting a dict. + """ + compile_commands, cache = _setup_build(setup_core) + compile_commands.parent.mkdir(parents=True, exist_ok=True) + compile_commands.write_text("[]") + cache.parent.mkdir(parents=True, exist_ok=True) + cache.write_text(cached) + cc_mtime = compile_commands.stat().st_mtime + os.utime(cache, (cc_mtime + 1, cc_mtime + 1)) + + with patch( + "esphome.espidf.idedata.idedata_from_build", + return_value={"cc_path": "gcc", "cxx_path": "g++"}, + ) as mock_transform: + result = toolchain.get_idedata() + + mock_transform.assert_called_once() + assert isinstance(result, dict) + + def test_get_idedata_regenerates_on_corrupted_cache(setup_core: Path) -> None: """An unparseable (but newer) cache falls back to regeneration.""" compile_commands, cache = _setup_build(setup_core) @@ -184,6 +265,129 @@ def test_get_idf_env_sets_git_ceiling_directories(setup_core: Path) -> None: assert str(CORE.config_dir) in env["GIT_CEILING_DIRECTORIES"].split(os.pathsep) +def test_get_cmake_output_without_build_dir(setup_core: Path) -> None: + """A build dir that was never created raises EsphomeError. + + Without this, subprocess.run(cwd=build_dir) raises FileNotFoundError, which + the log stack-trace decoder doesn't recognise as a decode failure. + """ + _setup_build(setup_core) + build_dir = CORE.relative_build_path("build") + assert not build_dir.exists() + + with pytest.raises(EsphomeError, match="No ESP-IDF build found"): + toolchain._get_cmake_output(build_dir) + + +def test_get_cmake_output_without_cmake_cache(setup_core: Path) -> None: + """A build dir that exists but was never configured raises EsphomeError.""" + _setup_build(setup_core) + build_dir = CORE.relative_build_path("build") + build_dir.mkdir(parents=True) + + with pytest.raises(EsphomeError, match="No ESP-IDF build found"): + toolchain._get_cmake_output(build_dir) + + +def test_get_cmake_output_with_configured_build(setup_core: Path) -> None: + """A configured build still runs cmake and caches the output. + + The missing-build guard must not get in the way of a real build. + """ + _setup_build(setup_core) + build_dir = CORE.relative_build_path("build") + build_dir.mkdir(parents=True) + (build_dir / "CMakeCache.txt").write_text("") + + completed = subprocess.CompletedProcess( + args=[], returncode=0, stdout="CMAKE_ADDR2LINE:FILEPATH=/tool/addr2line\n" + ) + with ( + patch.object(toolchain, "_get_idf_env", return_value={}), + patch.object(toolchain.subprocess, "run", return_value=completed) as mock_run, + ): + assert toolchain._get_cmake_output(build_dir) == completed.stdout + # Second call is served from the cache rather than re-running cmake. + assert toolchain._get_cmake_output(build_dir) == completed.stdout + + mock_run.assert_called_once() + assert toolchain._get_cmake_tool_path("CMAKE_ADDR2LINE") == Path("/tool/addr2line") + + +def test_get_cmake_output_missing_build_does_not_resolve_idf_env( + setup_core: Path, +) -> None: + """The build check runs before the env is resolved. + + Resolving the env calls check_esp_idf_install(), which can download and + extract the whole framework. A doomed call must never start that. + """ + _setup_build(setup_core) + build_dir = CORE.relative_build_path("build") + + with ( + patch.object(toolchain, "_get_idf_env") as mock_env, + patch.object(toolchain.subprocess, "run") as mock_run, + pytest.raises(EsphomeError), + ): + toolchain._get_cmake_output(build_dir) + + mock_env.assert_not_called() + mock_run.assert_not_called() + + +def test_run_idf_py_jobs_sets_build_jobs_env(setup_core: Path) -> None: + """The jobs argument is exported to idf.py as IDF_PY_BUILD_JOBS.""" + _setup_build(setup_core) + + with ( + patch.object(toolchain, "_get_idf_path", return_value=Path("/idf")), + patch.object(toolchain, "_get_idf_env", return_value={"PATH": "/bin"}), + patch.object(toolchain, "_get_idf_tool", return_value="python"), + patch.object(toolchain.subprocess, "run") as mock_run, + ): + mock_run.return_value.returncode = 0 + + toolchain.run_idf_py("build", jobs=2) + env = mock_run.call_args.kwargs["env"] + assert env["IDF_PY_BUILD_JOBS"] == "2" + assert env["PATH"] == "/bin" + + toolchain.run_idf_py("build") + env = mock_run.call_args.kwargs["env"] + assert "IDF_PY_BUILD_JOBS" not in env + + +def test_run_compile_passes_compile_process_limit(setup_core: Path) -> None: + """compile_process_limit is forwarded to run_idf_py as the job limit.""" + _setup_build(setup_core) + config = {CONF_ESPHOME: {CONF_COMPILE_PROCESS_LIMIT: 1}} + + with ( + patch.object(toolchain, "need_reconfigure", return_value=False), + patch.object(toolchain, "run_idf_py", return_value=0) as mock_run, + patch.object(toolchain, "print_summary"), + ): + assert toolchain.run_compile(config, verbose=False) == 0 + + mock_run.assert_called_once_with("build", "size", jobs=1) + + +def test_run_compile_without_compile_process_limit(setup_core: Path) -> None: + """When no compile_process_limit is set, no job limit is passed to idf.py.""" + _setup_build(setup_core) + config = {CONF_ESPHOME: {}} + + with ( + patch.object(toolchain, "need_reconfigure", return_value=False), + patch.object(toolchain, "run_idf_py", return_value=0) as mock_run, + patch.object(toolchain, "print_summary"), + ): + assert toolchain.run_compile(config, verbose=False) == 0 + + mock_run.assert_called_once_with("build", "size", jobs=None) + + def test_get_core_framework_version_from_core_data(): """The version is read from CORE.data when validation populated it.""" from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION diff --git a/tests/unit_tests/test_framework_helpers.py b/tests/unit_tests/test_framework_helpers.py index 6fe62dcc8c..08751879c2 100644 --- a/tests/unit_tests/test_framework_helpers.py +++ b/tests/unit_tests/test_framework_helpers.py @@ -2,8 +2,10 @@ # pylint: disable=protected-access +import hashlib import importlib.util import io +import json import logging import os from pathlib import Path @@ -16,6 +18,8 @@ import zipfile import pytest import requests as req +from esphome import framework_helpers +from esphome.core import EsphomeError from esphome.framework_helpers import ( _7z_extract_all, _detect_archive_root, @@ -25,7 +29,9 @@ from esphome.framework_helpers import ( archive_extract_all, create_venv, download_from_mirrors, + download_with_resume, get_project_compile_flags, + get_project_cxx_compile_flags, get_project_link_flags, get_python_env_executable_path, get_system_python_path, @@ -505,7 +511,7 @@ class TestArchiveExtractAll: # --------------------------------------------------------------------------- -# download_from_mirrors +# download_from_mirrors / download_with_resume # --------------------------------------------------------------------------- @@ -513,6 +519,8 @@ def _mock_response(content: bytes, ok: bool = True) -> MagicMock: r = MagicMock() r.__enter__.return_value = r r.__exit__.return_value = False + r.status_code = 200 + r.ok = ok if ok: r.raise_for_status.return_value = None else: @@ -522,6 +530,563 @@ def _mock_response(content: bytes, ok: bool = True) -> MagicMock: return r +def _interrupted_response(content: bytes, etag: str | None = None) -> MagicMock: + """A response whose body yields ``content`` and then drops mid-stream. + + ``etag`` makes the response resumable: without a validator the retry + logic restarts from zero rather than stitching unverified bytes. + """ + + def body(chunk_size): + yield content + raise req.exceptions.ChunkedEncodingError("connection dropped") + + r = _mock_response(b"") + if etag is not None: + r.headers = {**r.headers, "ETag": etag} + r.iter_content.side_effect = body + return r + + +def _resumed_response(content: bytes) -> MagicMock: + """An HTTP 206 response continuing an interrupted download.""" + r = _mock_response(content) + r.status_code = 206 + return r + + +class TestOpenRanged: + def test_fresh_download_sends_no_range(self) -> None: + with patch("requests.get", return_value=_mock_response(b"x")) as mock_get: + resp, offset = framework_helpers._open_ranged("https://e.com/f", 0, 30) + assert offset == 0 + assert mock_get.call_args[1]["headers"] == {} + assert resp is mock_get.return_value + + def test_resume_kept_on_206(self) -> None: + with patch("requests.get", return_value=_resumed_response(b"x")): + _, offset = framework_helpers._open_ranged("https://e.com/f", 7, 30) + assert offset == 7 + + def test_resume_downgraded_on_200(self) -> None: + """A server that ignores the Range header forces a restart.""" + with patch("requests.get", return_value=_mock_response(b"x")): + _, offset = framework_helpers._open_ranged("https://e.com/f", 7, 30) + assert offset == 0 + + def test_http_error_closes_response_and_raises(self) -> None: + r = _mock_response(b"", ok=False) + with ( + patch("requests.get", return_value=r), + pytest.raises(req.HTTPError), + ): + framework_helpers._open_ranged("https://e.com/f", 0, 30) + r.close.assert_called_once() + + def test_connect_error_propagates(self) -> None: + with ( + patch("requests.get", side_effect=req.ConnectionError("refused")), + pytest.raises(req.ConnectionError), + ): + framework_helpers._open_ranged("https://e.com/f", 0, 30) + + +class TestDownloadWithResume: + def test_downloads_and_renames(self, tmp_path: Path) -> None: + dest = tmp_path / "tool.tar.gz" + with patch("requests.get", return_value=_mock_response(b"data")) as mock_get: + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"data" + assert not (tmp_path / "tool.tar.gz.part").exists() + # a fresh download must not send a Range header + assert "Range" not in mock_get.call_args[1]["headers"] + + def test_mid_stream_drop_resumes_with_range(self, tmp_path: Path) -> None: + dest = tmp_path / "tool.tar.gz" + first = _interrupted_response(b"1234", etag='"v1"') + first.headers = {**first.headers, "content-length": "8"} + with patch( + "requests.get", + side_effect=[first, _resumed_response(b"5678")], + ) as mock_get: + download_with_resume("https://example.com/t", dest) + # earlier bytes were kept, remainder appended conditionally + assert dest.read_bytes() == b"12345678" + assert mock_get.call_args_list[1][1]["headers"] == { + "Range": "bytes=4-", + "If-Range": '"v1"', + } + + def test_unverifiable_drop_without_length_restarts(self, tmp_path: Path) -> None: + """A validator alone is not enough to stitch when nothing can prove + the stitched file complete (no sha/size and no content-length).""" + dest = tmp_path / "tool.tar.gz" + with patch( + "requests.get", + side_effect=[ + _interrupted_response(b"1234", etag='"v1"'), + _mock_response(b"full"), + ], + ) as mock_get: + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"full" + assert "Range" not in mock_get.call_args_list[1][1]["headers"] + + def test_resumed_clean_but_short_body_discarded(self, tmp_path: Path) -> None: + """A resumed stream that ends cleanly but short of the advertised + total is rejected and re-downloaded, not promoted.""" + dest = tmp_path / "tool.tar.gz" + first = _interrupted_response(b"abcd", etag='"v1"') + first.headers = {**first.headers, "content-length": "8"} + # resume ends cleanly after only 2 of the 4 missing bytes + short = _resumed_response(b"ef") + full = _mock_response(b"abcdefgh") + full.headers = {**full.headers, "content-length": "8"} + with patch("requests.get", side_effect=[first, short, full]) as mock_get: + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"abcdefgh" + # the short stitch was discarded; the final attempt started fresh + assert "Range" not in mock_get.call_args_list[2][1]["headers"] + + def test_unverifiable_drop_without_validator_restarts(self, tmp_path: Path) -> None: + """No sha/size and no server validator: the retry must not stitch.""" + dest = tmp_path / "tool.tar.gz" + with patch( + "requests.get", + side_effect=[_interrupted_response(b"1234"), _mock_response(b"full")], + ) as mock_get: + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"full" + assert "Range" not in mock_get.call_args_list[1][1]["headers"] + + def test_resume_across_invocations_from_part_file(self, tmp_path: Path) -> None: + """A .part file left by a previous run is resumed, not restarted, + when sha/size verification will vouch for the stitched result.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"12345") + good = hashlib.sha256(b"12345678").hexdigest() + with patch("requests.get", return_value=_resumed_response(b"678")) as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good, size=8) + assert dest.read_bytes() == b"12345678" + assert mock_get.call_args[1]["headers"] == {"Range": "bytes=5-"} + + def test_unverifiable_leftover_part_file_ignored(self, tmp_path: Path) -> None: + """Without sha/size there is no way to vouch for a cross-run stitch, + so a leftover part file starts over.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"12345") + with patch("requests.get", return_value=_mock_response(b"fresh")) as mock_get: + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"fresh" + assert "Range" not in mock_get.call_args[1]["headers"] + + def test_server_without_range_support_restarts(self, tmp_path: Path) -> None: + """HTTP 200 in response to a Range request truncates and restarts.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"sta") + good = hashlib.sha256(b"fresh").hexdigest() + with patch("requests.get", return_value=_mock_response(b"fresh")) as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good, size=5) + # the Range request was sent (verifiable resume) and downgraded + assert mock_get.call_args[1]["headers"] == {"Range": "bytes=3-"} + assert dest.read_bytes() == b"fresh" + + def test_size_only_leftover_part_restarts(self, tmp_path: Path) -> None: + """A size alone cannot detect a same-length content change on the + server, so a cross-run part without sha256 restarts from zero.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"12") + with patch("requests.get", return_value=_mock_response(b"1234")) as mock_get: + download_with_resume("https://example.com/t", dest, size=4) + assert "Range" not in mock_get.call_args[1]["headers"] + assert dest.read_bytes() == b"1234" + + def test_size_only_in_run_drop_resumes_with_validator(self, tmp_path: Path) -> None: + """Within a run the If-Range validator proves identity, so size-only + callers still resume mid-stream drops.""" + dest = tmp_path / "tool.tar.gz" + with patch( + "requests.get", + side_effect=[ + _interrupted_response(b"12", etag='"v1"'), + _resumed_response(b"34"), + ], + ) as mock_get: + download_with_resume("https://example.com/t", dest, size=4) + assert dest.read_bytes() == b"1234" + assert mock_get.call_args_list[1][1]["headers"] == { + "Range": "bytes=2-", + "If-Range": '"v1"', + } + + def test_unverifiable_download_logged( + self, tmp_path: Path, caplog: pytest.LogCaptureFixture + ) -> None: + """No sha, no size, no content-length: the download is promoted with + a debug note (routine for e.g. the constraints host, so not a + warning) that completeness could not be verified.""" + dest = tmp_path / "tool.tar.gz" + with ( + caplog.at_level(logging.DEBUG, logger="esphome.framework_helpers"), + patch("requests.get", return_value=_mock_response(b"data")), + ): + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"data" + assert "without any way to verify completeness" in caplog.text + + def test_416_promotes_complete_part_when_size_unknown(self, tmp_path: Path) -> None: + """sha256-only caller with a byte-complete part file: the server's + 416 confirms nothing is missing, verification promotes in place, and + the 416 must not loop as a retryable error.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"data") + good = hashlib.sha256(b"data").hexdigest() + r416 = _mock_response(b"", ok=False) + r416.status_code = 416 + with patch("requests.get", return_value=r416) as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good) + assert mock_get.call_count == 1 + r416.close.assert_called_once() + assert dest.read_bytes() == b"data" + + def test_416_with_corrupt_part_discards_and_redownloads( + self, tmp_path: Path + ) -> None: + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"bad!") + good = hashlib.sha256(b"data").hexdigest() + r416 = _mock_response(b"", ok=False) + r416.status_code = 416 + with patch( + "requests.get", side_effect=[r416, _mock_response(b"data")] + ) as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good) + assert "Range" not in mock_get.call_args_list[1][1]["headers"] + assert dest.read_bytes() == b"data" + + def test_hash_mismatch_discards_and_retries(self, tmp_path: Path) -> None: + dest = tmp_path / "tool.tar.gz" + good = hashlib.sha256(b"good").hexdigest() + with patch( + "requests.get", + side_effect=[_mock_response(b"bad!"), _mock_response(b"good")], + ) as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good, size=4) + assert dest.read_bytes() == b"good" + # the corrupt part file was discarded, so the retry starts fresh + assert "Range" not in mock_get.call_args_list[1][1]["headers"] + + def test_size_mismatch_discards_part(self, tmp_path: Path) -> None: + dest = tmp_path / "tool.tar.gz" + with ( + patch("requests.get", return_value=_mock_response(b"xx")), + pytest.raises(EsphomeError, match="after 2 attempts"), + ): + download_with_resume("https://example.com/t", dest, size=99, attempts=2) + assert not (tmp_path / "tool.tar.gz.part").exists() + assert not dest.exists() + + def test_attempts_exhausted_keeps_part_file(self, tmp_path: Path) -> None: + """Mid-stream failures keep the partial file so a later run resumes.""" + dest = tmp_path / "tool.tar.gz" + first = _interrupted_response(b"12", etag='"v1"') + first.headers = {**first.headers, "content-length": "4"} + second = _interrupted_response(b"34") + second.status_code = 206 + with ( + patch("requests.get", side_effect=[first, second]), + pytest.raises(EsphomeError, match="after 2 attempts"), + ): + download_with_resume("https://example.com/t", dest, attempts=2) + assert (tmp_path / "tool.tar.gz.part").read_bytes() == b"1234" + + def test_multiple_drops_accumulate_across_attempts(self, tmp_path: Path) -> None: + """Each attempt appends its bytes; three partial responses complete + the file.""" + dest = tmp_path / "tool.tar.gz" + first = _interrupted_response(b"ab", etag='"v1"') + first.headers = {**first.headers, "content-length": "6"} + second = _interrupted_response(b"cd") + second.status_code = 206 + third = _resumed_response(b"ef") + with patch( + "requests.get", + side_effect=[first, second, third], + ) as mock_get: + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"abcdef" + expected = {"Range": "bytes=2-", "If-Range": '"v1"'} + assert mock_get.call_args_list[1][1]["headers"] == expected + expected = {"Range": "bytes=4-", "If-Range": '"v1"'} + assert mock_get.call_args_list[2][1]["headers"] == expected + + def test_connect_error_then_success(self, tmp_path: Path) -> None: + """A connect error (no response at all) consumes an attempt and the + next attempt succeeds.""" + dest = tmp_path / "tool.tar.gz" + with patch( + "requests.get", + side_effect=[req.ConnectionError("refused"), _mock_response(b"data")], + ): + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"data" + + def test_http_error_keeps_part_file(self, tmp_path: Path) -> None: + """A transient HTTP error (e.g. 503) must not discard resume state.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"keep") + error = _mock_response(b"", ok=False) + error.status_code = 503 + with ( + patch("requests.get", return_value=error), + pytest.raises(EsphomeError, match="after 1 attempts"), + ): + download_with_resume("https://example.com/t", dest, attempts=1) + assert (tmp_path / "tool.tar.gz.part").read_bytes() == b"keep" + + def test_creates_missing_parent_directories(self, tmp_path: Path) -> None: + dest = tmp_path / "dist" / "nested" / "tool.tar.gz" + with patch("requests.get", return_value=_mock_response(b"data")): + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"data" + + def test_verifies_both_size_and_sha(self, tmp_path: Path) -> None: + dest = tmp_path / "tool.tar.gz" + good = hashlib.sha256(b"data").hexdigest() + with patch("requests.get", return_value=_mock_response(b"data")): + download_with_resume("https://example.com/t", dest, sha256=good, size=4) + assert dest.read_bytes() == b"data" + + def test_corrupt_partial_resumed_then_discarded_then_redownloaded( + self, tmp_path: Path + ) -> None: + """The full recovery cycle for a corrupted partial download: the + resume completes it, verification fails, the poisoned part file is + discarded, and the next attempt re-downloads from scratch.""" + dest = tmp_path / "tool.tar.gz" + # a previous run left a corrupted 4-byte prefix behind + (tmp_path / "tool.tar.gz.part").write_bytes(b"BAD!") + good = hashlib.sha256(b"data66").hexdigest() + with patch( + "requests.get", + side_effect=[ + _resumed_response(b"66"), # resume "completes" the bad part + _mock_response(b"data66"), # clean retry from zero + ], + ) as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good, size=6) + # first attempt resumed at the corrupt offset, failed verification; + # second attempt started fresh (no Range header) and succeeded + assert mock_get.call_args_list[0][1]["headers"] == {"Range": "bytes=4-"} + assert "Range" not in mock_get.call_args_list[1][1]["headers"] + assert dest.read_bytes() == b"data66" + assert not (tmp_path / "tool.tar.gz.part").exists() + + def test_existing_dest_passing_verification_kept(self, tmp_path: Path) -> None: + """A dest completed by an earlier run is reused without any request.""" + dest = tmp_path / "tool.tar.gz" + dest.write_bytes(b"data") + good = hashlib.sha256(b"data").hexdigest() + with patch("requests.get") as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good, size=4) + mock_get.assert_not_called() + assert dest.read_bytes() == b"data" + + @pytest.mark.parametrize( + "stale", + [ + pytest.param(b"corrupt!", id="wrong-size"), + pytest.param(b"bad!", id="right-size-wrong-hash"), + ], + ) + def test_existing_dest_failing_verification_redownloaded( + self, tmp_path: Path, stale: bytes + ) -> None: + dest = tmp_path / "tool.tar.gz" + dest.write_bytes(stale) + good = hashlib.sha256(b"data").hexdigest() + with patch("requests.get", return_value=_mock_response(b"data")): + download_with_resume("https://example.com/t", dest, sha256=good, size=4) + assert dest.read_bytes() == b"data" + + def test_existing_dest_with_size_only_kept(self, tmp_path: Path) -> None: + dest = tmp_path / "tool.tar.gz" + dest.write_bytes(b"data") + with patch("requests.get") as mock_get: + download_with_resume("https://example.com/t", dest, size=4) + mock_get.assert_not_called() + + def test_existing_dest_with_sha_only_kept(self, tmp_path: Path) -> None: + """sha-only verification also authorizes reusing a completed dest.""" + dest = tmp_path / "tool.tar.gz" + dest.write_bytes(b"data") + good = hashlib.sha256(b"data").hexdigest() + with patch("requests.get") as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good) + mock_get.assert_not_called() + + def test_meta_write_failure_is_best_effort(self, tmp_path: Path) -> None: + """A failure to persist the resume sidecar must not fail the + download itself.""" + dest = tmp_path / "f.tar.xz" + first = _mock_response(b"data") + first.headers = {**first.headers, "ETag": '"v1"', "content-length": "4"} + with ( + patch("requests.get", return_value=first), + patch.object(Path, "write_text", side_effect=OSError("read-only")), + ): + download_with_resume("https://example.com/f", dest) + assert dest.read_bytes() == b"data" + + def test_meta_sidecar_written_and_removed(self, tmp_path: Path) -> None: + """The validator sidecar appears while downloading and is cleaned up + with the promotion.""" + dest = tmp_path / "f.tar.xz" + meta = tmp_path / "f.tar.xz.part.meta" + seen: list[bool] = [] + first = _interrupted_response(b"1234", etag='"v1"') + first.headers = {**first.headers, "content-length": "8"} + responses = [first] + + def get(*args: object, **kwargs: object) -> MagicMock: + if responses: + return responses.pop(0) + # the resume request: the sidecar written by the first response + # must already be on disk at this point + seen.append(meta.is_file()) + return _resumed_response(b"5678") + + with patch("requests.get", side_effect=get): + download_with_resume("https://example.com/f", dest) + assert dest.read_bytes() == b"12345678" + assert seen == [True] # sidecar existed during the resume attempt + assert not meta.exists() # cleaned up on success + + def test_locked_promotion_keeps_verified_part(self, tmp_path: Path) -> None: + """A rename that stays blocked (e.g. a long-lived Windows file lock) + must not delete the verified download; the next attempt retries just + the rename without touching the network.""" + dest = tmp_path / "tool.tar.gz" + good = hashlib.sha256(b"data").hexdigest() + with ( + patch("requests.get", return_value=_mock_response(b"data")) as mock_get, + patch( + "esphome.framework_helpers._rename_with_retry", + side_effect=[PermissionError("locked"), None], + ) as rename, + ): + download_with_resume("https://example.com/t", dest, sha256=good, size=4) + # one download; the second attempt only redid the rename + assert mock_get.call_count == 1 + assert rename.call_count == 2 + + def test_locked_promotion_exhausted_keeps_part_for_next_run( + self, tmp_path: Path + ) -> None: + dest = tmp_path / "tool.tar.gz" + good = hashlib.sha256(b"data").hexdigest() + with ( + patch("requests.get", return_value=_mock_response(b"data")), + patch( + "esphome.framework_helpers._rename_with_retry", + side_effect=PermissionError("locked"), + ), + pytest.raises(EsphomeError, match="after 1 attempts"), + ): + download_with_resume( + "https://example.com/t", dest, sha256=good, size=4, attempts=1 + ) + # the verified bytes survive for the next run + assert (tmp_path / "tool.tar.gz.part").read_bytes() == b"data" + + def test_meta_sidecar_resumes_across_runs_without_sha(self, tmp_path: Path) -> None: + """A later run resumes an unfinished download using the validator the + first run stored — the cross-run fix for the framework tarball.""" + dest = tmp_path / "f.tar.xz" + (tmp_path / "f.tar.xz.part").write_bytes(b"1234") + (tmp_path / "f.tar.xz.part.meta").write_text( + json.dumps( + {"url": "https://example.com/f", "validator": '"v1"', "total": 8} + ) + ) + with patch("requests.get", return_value=_resumed_response(b"5678")) as mock_get: + download_with_resume("https://example.com/f", dest) + assert dest.read_bytes() == b"12345678" + assert mock_get.call_args[1]["headers"] == { + "Range": "bytes=4-", + "If-Range": '"v1"', + } + + def test_meta_sidecar_for_other_url_ignored(self, tmp_path: Path) -> None: + """Metadata from a different mirror URL must not authorize a stitch.""" + dest = tmp_path / "f.tar.xz" + (tmp_path / "f.tar.xz.part").write_bytes(b"1234") + (tmp_path / "f.tar.xz.part.meta").write_text( + json.dumps({"url": "https://other.com/f", "validator": '"v1"', "total": 8}) + ) + full = _mock_response(b"12345678") + with patch("requests.get", return_value=full) as mock_get: + download_with_resume("https://example.com/f", dest) + assert "Range" not in mock_get.call_args[1]["headers"] + assert dest.read_bytes() == b"12345678" + + def test_complete_part_file_promoted_without_network(self, tmp_path: Path) -> None: + """A .part holding every byte (killed between write and rename) is + verified in place and promoted; no request is made, so no 416 loop.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"data") + good = hashlib.sha256(b"data").hexdigest() + with patch("requests.get") as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good, size=4) + mock_get.assert_not_called() + assert dest.read_bytes() == b"data" + + def test_complete_but_corrupt_part_file_redownloaded(self, tmp_path: Path) -> None: + """A full-size .part with a wrong hash is discarded and re-downloaded + from scratch.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"bad!") + good = hashlib.sha256(b"data").hexdigest() + with patch("requests.get", return_value=_mock_response(b"data")) as mock_get: + download_with_resume("https://example.com/t", dest, sha256=good, size=4) + assert "Range" not in mock_get.call_args[1]["headers"] + assert dest.read_bytes() == b"data" + + def test_oversized_part_file_discarded(self, tmp_path: Path) -> None: + """A .part larger than the expected size fails verification and is + replaced by a fresh download.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"toolong") + good = hashlib.sha256(b"data").hexdigest() + with patch("requests.get", return_value=_mock_response(b"data")): + download_with_resume("https://example.com/t", dest, sha256=good, size=4) + assert dest.read_bytes() == b"data" + + def test_malformed_content_length_degrades_gracefully(self, tmp_path: Path) -> None: + """A garbage Content-Length must not crash the attempt; it means + "unknown", so a drop restarts instead of stitching and a clean + download still succeeds.""" + dest = tmp_path / "tool.tar.gz" + first = _interrupted_response(b"1234", etag='"v1"') + first.headers = {**first.headers, "content-length": "explode"} + retry = _mock_response(b"full") + retry.headers = {**retry.headers, "content-length": "explode"} + with patch("requests.get", side_effect=[first, retry]) as mock_get: + download_with_resume("https://example.com/t", dest) + assert dest.read_bytes() == b"full" + # unknown length -> completeness unprovable -> no resume attempted + assert "Range" not in mock_get.call_args_list[1][1]["headers"] + + def test_zero_byte_part_file_sends_no_range(self, tmp_path: Path) -> None: + """An empty leftover part file is a fresh download, not a resume.""" + dest = tmp_path / "tool.tar.gz" + (tmp_path / "tool.tar.gz.part").write_bytes(b"") + with patch("requests.get", return_value=_mock_response(b"data")) as mock_get: + download_with_resume("https://example.com/t", dest) + assert mock_get.call_args[1]["headers"] == {} + assert dest.read_bytes() == b"data" + + class TestDownloadFromMirrors: def test_success_returns_url_and_writes_content(self, tmp_path: Path) -> None: target = tmp_path / "out.bin" @@ -545,7 +1110,101 @@ class TestDownloadFromMirrors: ) assert mock_get.call_args[0][0] == "https://example.com/1.2.3.bin" - def test_falls_back_to_second_mirror(self, tmp_path: Path) -> None: + def test_template_with_missing_substitution_is_skipped( + self, tmp_path: Path + ) -> None: + """A template referencing an unavailable substitution is skipped, not + formatted into a bogus URL (e.g. SHORT_VERSION only exists for x.y.0 + framework versions).""" + with patch( + "requests.get", + return_value=_mock_response(b"x"), + ) as mock_get: + url = download_from_mirrors( + [ + "https://example.com/{SHORT_VERSION}.bin", + "https://example.com/{VERSION}.bin", + ], + {"VERSION": "1.2.3"}, + tmp_path / "out.bin", + ) + assert url == "https://example.com/1.2.3.bin" + assert mock_get.call_count == 1 + + def test_all_templates_skipped_raises_esphome_error(self, tmp_path: Path) -> None: + with ( + patch("requests.get") as mock_get, + pytest.raises(EsphomeError, match="No mirror URL template matched") as ei, + ): + download_from_mirrors( + ["https://example.com/{MISSING}.bin"], + {"VERSION": "1.2.3"}, + tmp_path / "out.bin", + ) + mock_get.assert_not_called() + # The skipped template and its missing substitution are named + assert "https://example.com/{MISSING}.bin" in str(ei.value) + assert "MISSING" in str(ei.value) + + def test_failure_message_includes_skipped_templates(self, tmp_path: Path) -> None: + """When downloads fail, templates that were skipped for missing + substitutions are also listed so a typo'd custom mirror is + attributable.""" + with ( + patch( + "requests.get", + return_value=_mock_response(b"", ok=False), + ), + pytest.raises(EsphomeError, match="all mirrors") as ei, + ): + download_from_mirrors( + [ + "https://example.com/{TYPO}.bin", + "https://example.com/{VERSION}.bin", + ], + {"VERSION": "1.2.3"}, + tmp_path / "out.bin", + ) + message = str(ei.value) + assert "https://example.com/1.2.3.bin" in message + assert ( + "https://example.com/{TYPO}.bin\n not applicable (TYPO not available)" + in message + ) + + def test_malformed_template_warns_and_is_reported( + self, tmp_path: Path, caplog: pytest.LogCaptureFixture + ) -> None: + """A structurally malformed template is an authoring error: warned + about even when another mirror succeeds, and named in the aggregate + error when everything fails.""" + with ( + patch("requests.get", return_value=_mock_response(b"x")), + caplog.at_level(logging.WARNING, logger="esphome.framework_helpers"), + ): + url = download_from_mirrors( + ["https://example.com/{oops.bin", "https://example.com/{VERSION}.bin"], + {"VERSION": "1.2.3"}, + tmp_path / "out.bin", + ) + assert url == "https://example.com/1.2.3.bin" + assert "malformed mirror URL template" in caplog.text + + with ( + patch("requests.get", return_value=_mock_response(b"", ok=False)), + pytest.raises(EsphomeError, match="all mirrors") as ei, + ): + download_from_mirrors( + ["https://example.com/{oops.bin", "https://example.com/{VERSION}.bin"], + {"VERSION": "1.2.3"}, + tmp_path / "out.bin", + ) + assert "https://example.com/{oops.bin\n skipped (ValueError(" in str( + ei.value + ) + + def test_falls_back_to_second_mirror(self) -> None: + buf = io.BytesIO() with patch( "requests.get", side_effect=[_mock_response(b"", ok=False), _mock_response(b"second")], @@ -553,20 +1212,169 @@ class TestDownloadFromMirrors: url = download_from_mirrors( ["https://mirror1.com/f", "https://mirror2.com/f"], {}, - tmp_path / "out.bin", + buf, ) assert url == "https://mirror2.com/f" - assert (tmp_path / "out.bin").read_bytes() == b"second" + assert buf.getvalue() == b"second" - def test_all_mirrors_fail_reraises_last_exception(self, tmp_path: Path) -> None: + def test_mid_stream_drop_resumes_same_mirror(self) -> None: + """A mid-stream failure retries the same mirror with Range and + If-Range headers, keeping the bytes already received, before falling + to the next.""" + first = _interrupted_response(b"1234", etag='"v1"') + first.headers = {**first.headers, "content-length": "8"} + buf = io.BytesIO() + with patch( + "requests.get", + side_effect=[first, _resumed_response(b"5678")], + ) as mock_get: + url = download_from_mirrors( + ["https://mirror1.com/f", "https://mirror2.com/f"], + {}, + buf, + ) + assert url == "https://mirror1.com/f" + assert buf.getvalue() == b"12345678" + assert mock_get.call_count == 2 + assert mock_get.call_args_list[1][0][0] == "https://mirror1.com/f" + # the resume is conditional on the content being unchanged + assert mock_get.call_args_list[1][1]["headers"] == { + "Range": "bytes=4-", + "If-Range": '"v1"', + } + + def test_mid_stream_drop_without_validator_restarts(self) -> None: + """A server offering no ETag/Last-Modified cannot be resumed safely; + the retry restarts from zero instead of stitching unverified bytes.""" + buf = io.BytesIO() + with patch( + "requests.get", + side_effect=[_interrupted_response(b"1234"), _mock_response(b"full")], + ) as mock_get: + download_from_mirrors(["https://mirror1.com/f"], {}, buf) + assert buf.getvalue() == b"full" + assert "Range" not in mock_get.call_args_list[1][1]["headers"] + + def test_drop_after_last_byte_recovers_via_416(self) -> None: + """A connection drop after the final body byte leaves a complete file; + the retry's 416 answer plus the length check turn it into success + instead of a wasted refetch.""" + first = _interrupted_response(b"1234", etag='"v1"') + first.headers = {**first.headers, "content-length": "4"} + r416 = _mock_response(b"", ok=False) + r416.status_code = 416 + buf = io.BytesIO() + with patch("requests.get", side_effect=[first, r416]) as mock_get: + url = download_from_mirrors(["https://mirror1.com/f"], {}, buf) + assert url == "https://mirror1.com/f" + assert buf.getvalue() == b"1234" + assert mock_get.call_count == 2 + + def test_mirror_drop_without_length_restarts(self) -> None: + """With no content-length there is no way to prove a stitched file + complete, so the retry restarts even though a validator exists.""" + buf = io.BytesIO() + with patch( + "requests.get", + side_effect=[ + _interrupted_response(b"1234", etag='"v1"'), + _mock_response(b"full"), + ], + ) as mock_get: + download_from_mirrors(["https://mirror1.com/f"], {}, buf) + assert buf.getvalue() == b"full" + assert "Range" not in mock_get.call_args_list[1][1]["headers"] + + def test_path_target_resumes_across_runs(self, tmp_path: Path) -> None: + """A path target routes through download_with_resume: a part file and + metadata from a previous run resume instead of restarting.""" + dest = tmp_path / "idf.tar.xz" + (tmp_path / "idf.tar.xz.part").write_bytes(b"1234") + (tmp_path / "idf.tar.xz.part.meta").write_text( + json.dumps( + {"url": "https://mirror1.com/f", "validator": '"v1"', "total": 8} + ) + ) + with patch("requests.get", return_value=_resumed_response(b"5678")) as mock_get: + url = download_from_mirrors(["https://mirror1.com/f"], {}, dest) + assert url == "https://mirror1.com/f" + assert dest.read_bytes() == b"12345678" + assert mock_get.call_args[1]["headers"] == { + "Range": "bytes=4-", + "If-Range": '"v1"', + } + + def test_path_target_falls_back_to_next_mirror(self, tmp_path: Path) -> None: + dest = tmp_path / "idf.tar.xz" + with patch( + "requests.get", + side_effect=[req.ConnectionError("down"), _mock_response(b"data")], + ): + url = download_from_mirrors( + ["https://mirror1.com/f", "https://mirror2.com/f"], {}, dest + ) + assert url == "https://mirror2.com/f" + assert dest.read_bytes() == b"data" + + def test_resumed_short_body_fails_length_check(self) -> None: + """A stitched file whose final length disagrees with the advertised + total is rejected instead of reported as success.""" + first = _interrupted_response(b"1234", etag='"v1"') + first.headers = {**first.headers, "content-length": "8"} + # the resume ends early (5 of 8 bytes); the poisoned part is then + # discarded and the fresh retry also delivers a short body + short_resume = _resumed_response(b"5") + short_fresh = _mock_response(b"56") + short_fresh.headers = {**short_fresh.headers, "content-length": "8"} + buf = io.BytesIO() + with ( + patch("requests.get", side_effect=[first, short_resume, short_fresh]), + pytest.raises(EsphomeError, match="all mirrors"), + ): + download_from_mirrors(["https://mirror1.com/f"], {}, buf) + + def test_failed_mirror_leftovers_not_kept_for_next_mirror(self) -> None: + """Bytes from a mirror that failed all attempts must not leak into the + next mirror's download (no bogus Range request, fresh content).""" + exhausted = [_interrupted_response(b"AAAA", etag='"a1"')] + for _ in range(2): + r = _interrupted_response(b"BB") + r.status_code = 206 + exhausted.append(r) + buf = io.BytesIO() + with patch( + "requests.get", + side_effect=exhausted + [_mock_response(b"clean")], + ) as mock_get: + url = download_from_mirrors( + ["https://mirror1.com/f", "https://mirror2.com/f"], + {}, + buf, + ) + assert url == "https://mirror2.com/f" + assert buf.getvalue() == b"clean" + # the second mirror starts fresh, without a Range header + assert mock_get.call_args_list[3][0][0] == "https://mirror2.com/f" + assert "Range" not in mock_get.call_args_list[3][1]["headers"] + + def test_all_mirrors_fail_raises_error_listing_every_attempt(self) -> None: with ( patch( "requests.get", return_value=_mock_response(b"", ok=False), ), - pytest.raises(req.HTTPError), + pytest.raises(EsphomeError, match="all mirrors") as excinfo, ): - download_from_mirrors(["https://example.com/f"], {}, tmp_path / "out.bin") + download_from_mirrors( + ["https://mirror1.com/f", "https://mirror2.com/f"], + {}, + io.BytesIO(), + ) + # Every attempted URL appears in the message, and the first mirror's + # exception (the primary URL, usually the one that matters) is chained. + assert "https://mirror1.com/f" in str(excinfo.value) + assert "https://mirror2.com/f" in str(excinfo.value) + assert isinstance(excinfo.value.__cause__, req.HTTPError) def test_empty_mirrors_raises_value_error(self, tmp_path: Path) -> None: with pytest.raises(ValueError, match="empty mirrors list"): @@ -625,8 +1433,10 @@ def test_importing_framework_helpers_does_not_import_requests() -> None: [ sys.executable, "-c", - "import sys\nimport esphome.framework_helpers\n" - "print('\\n'.join(sys.modules))", + ( + "import sys\nimport esphome.framework_helpers\n" + "print('\\n'.join(sys.modules))" + ), ], capture_output=True, text=True, @@ -1048,3 +1858,25 @@ class TestGetProjectLinkFlags: ): result = get_project_link_flags() assert result == sorted(result) + + +def _make_core_cxx(flags: set[str]) -> MagicMock: + core = MagicMock() + core.cxx_build_flags = flags + return core + + +class TestGetProjectCxxCompileFlags: + def test_returns_sorted_flags(self) -> None: + with patch( + "esphome.core.CORE", + _make_core_cxx({"-Wno-volatile", "-Wno-deprecated"}), + ): + assert get_project_cxx_compile_flags() == [ + "-Wno-deprecated", + "-Wno-volatile", + ] + + def test_empty_flags(self) -> None: + with patch("esphome.core.CORE", _make_core_cxx(set())): + assert get_project_cxx_compile_flags() == [] diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index 62d2344069..ec1becf3e8 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -1,15 +1,22 @@ """Tests for git.py module.""" +from collections.abc import Callable +import errno +import logging import os from pathlib import Path +import subprocess +import threading import time from typing import Any from unittest.mock import Mock, patch +from filelock import FileLock import pytest from esphome import git -from esphome.core import CORE, TimePeriodSeconds +import esphome.config_validation as cv +from esphome.core import CORE, EsphomeError, TimePeriodSeconds from esphome.git import GitCommandError @@ -19,6 +26,15 @@ def _compute_repo_dir(url: str, ref: str | None, domain: str) -> Path: return git._compute_destination_path(key, domain) +# The tests must probe the exact location the implementation uses +_marker_path = git._clone_complete_marker_path + + +def _mark_clone_complete(repo_dir: Path) -> None: + """Write the completion marker so a hand-made repo dir is treated as valid.""" + _marker_path(repo_dir).write_text("test") + + def _setup_old_repo(repo_dir: Path, days_old: int = 2) -> None: """Helper to set up a git repo directory structure with an old timestamp. @@ -30,6 +46,7 @@ def _setup_old_repo(repo_dir: Path, days_old: int = 2) -> None: repo_dir.mkdir(parents=True) git_dir = repo_dir / ".git" git_dir.mkdir() + _mark_clone_complete(repo_dir) # Create FETCH_HEAD file with old timestamp fetch_head = git_dir / "FETCH_HEAD" @@ -54,6 +71,52 @@ def _get_git_command_type(cmd: list[str]) -> str | None: return None +def _simulate_cloned_repo(repo_dir: Path) -> None: + """Create the directory structure a successful git clone would leave.""" + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + + +def _make_clone_side_effect( + repo_dir: Path, + gitmodules: bool = False, + on_clone: Callable[[], None] | None = None, +) -> Callable[..., str]: + """Return a run_git_command side effect whose clone creates the repo dir. + + With ``gitmodules`` the cloned repo also declares submodules. ``on_clone`` + runs at clone time before the repo dir appears, so a test can probe or + block mid-clone. + """ + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) == "clone": + if on_clone is not None: + on_clone() + _simulate_cloned_repo(repo_dir) + if gitmodules: + (repo_dir / ".gitmodules").write_text("test") + return "" + + return git_command_side_effect + + +def _submodule_calls(mock: Mock) -> list[Any]: + """Return the mock's `git submodule` calls.""" + return [ + c for c in mock.call_args_list if _get_git_command_type(c[0][0]) == "submodule" + ] + + +def _assert_submodule_runs_without_isolation(call: Any, repo_dir: Path) -> None: + """Assert a git submodule call ran with plain cwd, not GIT_DIR/GIT_WORK_TREE + isolation, which breaks the submodule porcelain on some installations.""" + assert call.kwargs.get("git_dir") is None + assert call.kwargs.get("cwd") == repo_dir + + def test_run_git_command_success(tmp_path: Path) -> None: """Test that run_git_command returns output on success.""" # Create a simple git repo to test with @@ -70,6 +133,22 @@ def test_run_git_command_success(tmp_path: Path) -> None: assert isinstance(result, str) +def test_run_git_command_debug_log_redacts_credentials( + tmp_path: Path, mock_subprocess_run: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Embedded URL credentials never reach the debug log; -v output is + routinely pasted into public issues. subprocess is mocked so no real + git ever sees the URL (the path is not creatable on Windows).""" + mock_subprocess_run.return_value = Mock(returncode=0, stdout=b"", stderr=b"") + with caplog.at_level(logging.DEBUG, logger="esphome.git"): + git.run_git_command( + ["git", "clone", "https://user:hunter2@github.com/test/repo"], + cwd=tmp_path, + ) + assert "hunter2" not in caplog.text + assert "://***@github.com/test/repo" in caplog.text + + def test_run_git_command_with_git_dir_isolation( tmp_path: Path, mock_subprocess_run: Mock ) -> None: @@ -86,10 +165,17 @@ def test_run_git_command_with_git_dir_isolation( stderr=b"", ) - result = git.run_git_command( - ["git", "rev-parse", "HEAD"], - git_dir=repo_dir, - ) + # Ambient repo-scoping vars simulate a git hook invoking ESPHome; an + # ambient GIT_INDEX_FILE surviving into a git_dir invocation fails + # silently (git operates on the caller's index and exits 0). + with patch.dict( + os.environ, + {"GIT_INDEX_FILE": "/caller/index", "GIT_OBJECT_DIRECTORY": "/caller/objects"}, + ): + result = git.run_git_command( + ["git", "rev-parse", "HEAD"], + git_dir=repo_dir, + ) # Verify subprocess.run was called assert mock_subprocess_run.called @@ -101,6 +187,9 @@ def test_run_git_command_with_git_dir_isolation( assert "GIT_WORK_TREE" in env assert env["GIT_DIR"] == str(repo_dir / ".git") assert env["GIT_WORK_TREE"] == str(repo_dir) + # The ambient scoping vars must be stripped, not passed through. + assert "GIT_INDEX_FILE" not in env + assert "GIT_OBJECT_DIRECTORY" not in env assert result == "test output" @@ -186,6 +275,89 @@ def test_run_git_command_without_git_dir(mock_subprocess_run: Mock) -> None: assert result == "Cloning into 'test_repo'..." +@pytest.mark.parametrize("relative", [False, True], ids=["absolute", "relative"]) +def test_run_git_command_with_cwd_runs_in_dir_without_isolation( + tmp_path: Path, + mock_subprocess_run: Mock, + monkeypatch: pytest.MonkeyPatch, + relative: bool, +) -> None: + """The cwd parameter sets the working directory without GIT_DIR/GIT_WORK_TREE. + + Ambient GIT_DIR/GIT_WORK_TREE (e.g. from a git hook or CI wrapper) must be + stripped too, and GIT_CEILING_DIRECTORIES must stop git from walking up to + an enclosing repository if the target repo's .git is missing or corrupt. + Git silently ignores a relative ceiling entry, so the variable must come + out absolute even when the given cwd is relative. + """ + repo_dir = tmp_path / "test_repo" + repo_dir.mkdir() + if relative: + monkeypatch.chdir(tmp_path) + cwd_arg = Path("test_repo") + else: + cwd_arg = repo_dir + + mock_subprocess_run.return_value = Mock( + returncode=0, + stdout=b"test output", + stderr=b"", + ) + + with patch.dict( + os.environ, + { + "GIT_DIR": "/ambient/.git", + "GIT_WORK_TREE": "/ambient", + "GIT_INDEX_FILE": "/ambient/.git/index", + }, + ): + result = git.run_git_command(["git", "submodule", "update"], cwd=cwd_arg) + + call_args = mock_subprocess_run.call_args + env = call_args[1]["env"] + assert "GIT_DIR" not in env + assert "GIT_WORK_TREE" not in env + assert "GIT_INDEX_FILE" not in env + ceiling = Path(env["GIT_CEILING_DIRECTORIES"]) + assert ceiling.is_absolute() + assert ceiling.samefile(tmp_path) + assert call_args[1]["cwd"] == cwd_arg + assert result == "test output" + + +def test_run_git_command_raises_on_nonfatal_stderr( + tmp_path: Path, mock_subprocess_run: Mock +) -> None: + """Nonzero exit with stderr lacking a fatal: prefix raises with full stderr.""" + mock_subprocess_run.return_value = Mock( + returncode=1, + stdout=b"", + stderr=b"error: pathspec 'nope' did not match any file(s)\n", + ) + + with pytest.raises(GitCommandError, match="did not match"): + git.run_git_command(["git", "checkout", "nope"], git_dir=tmp_path) + + +def test_run_git_command_raises_on_nonzero_exit_without_stderr( + tmp_path: Path, mock_subprocess_run: Mock +) -> None: + """A nonzero exit must raise even when git printed nothing to stderr. + + Silent nonzero exits were previously treated as success, which is how + broken checkouts could be cached as complete. + """ + mock_subprocess_run.return_value = Mock( + returncode=1, + stdout=b"", + stderr=b"", + ) + + with pytest.raises(GitCommandError, match="exited with code 1"): + git.run_git_command(["git", "submodule", "update"], cwd=tmp_path) + + def test_run_git_command_without_git_dir_raises_error( mock_subprocess_run: Mock, ) -> None: @@ -217,6 +389,7 @@ def test_clone_or_update_with_never_refresh( repo_dir.mkdir(parents=True) git_dir = repo_dir / ".git" git_dir.mkdir() + _mark_clone_complete(repo_dir) # Create FETCH_HEAD file with current timestamp fetch_head = git_dir / "FETCH_HEAD" @@ -250,6 +423,7 @@ def test_clone_or_update_skips_when_core_skip_external_update( repo_dir.mkdir(parents=True) git_dir = repo_dir / ".git" git_dir.mkdir() + _mark_clone_complete(repo_dir) (git_dir / "FETCH_HEAD").write_text("test") CORE.skip_external_update = True @@ -281,6 +455,7 @@ def test_clone_or_update_with_refresh_updates_old_repo( repo_dir.mkdir(parents=True) git_dir = repo_dir / ".git" git_dir.mkdir() + _mark_clone_complete(repo_dir) # Create FETCH_HEAD file with old timestamp (2 days ago) fetch_head = git_dir / "FETCH_HEAD" @@ -314,7 +489,7 @@ def test_clone_or_update_with_refresh_updates_old_repo( def test_clone_or_update_with_refresh_skips_fresh_repo( - tmp_path: Path, mock_run_git_command: Mock + tmp_path: Path, mock_run_git_command: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test that refresh doesn't update fresh repos.""" # Set up CORE.config_path so data_dir uses tmp_path @@ -329,6 +504,7 @@ def test_clone_or_update_with_refresh_skips_fresh_repo( repo_dir.mkdir(parents=True) git_dir = repo_dir / ".git" git_dir.mkdir() + _mark_clone_complete(repo_dir) # Create FETCH_HEAD file with recent timestamp (1 hour ago) fetch_head = git_dir / "FETCH_HEAD" @@ -338,20 +514,74 @@ def test_clone_or_update_with_refresh_skips_fresh_repo( # Set modification time to 1 hour ago os.utime(fetch_head, (recent_time, recent_time)) + # Freeze the clock at 1 hour (plus a margin larger than any filesystem + # mtime rounding) after the mtime so the logged countdown is deterministic + frozen_now = fetch_head.stat().st_mtime + 3600.5 + # Call with refresh=1d (1 day) refresh = TimePeriodSeconds(days=1) - result_dir, revert = git.clone_or_update( - url=url, - ref=ref, - refresh=refresh, - domain=domain, - ) + with ( + patch("esphome.git.time.time", return_value=frozen_now), + caplog.at_level(logging.INFO, logger="esphome.git"), + ): + result_dir, revert = git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) # Should NOT call git fetch since repo is fresh mock_run_git_command.assert_not_called() assert result_dir == repo_dir assert revert is None + # Should tell the user the update was skipped and when the next refresh is + assert f"Skipping update for {url}@{ref}" in caplog.text + assert "will refresh on the next run after 22h 59min" in caplog.text + assert "(refresh: 1d)" in caplog.text + + +def test_clone_or_update_with_refresh_never_logs_refresh_disabled( + tmp_path: Path, mock_run_git_command: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test that a config-level refresh: never skips without a countdown log.""" + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = None + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Create the git repo directory structure + repo_dir.mkdir(parents=True) + git_dir = repo_dir / ".git" + git_dir.mkdir() + _mark_clone_complete(repo_dir) + + # Create FETCH_HEAD file with current timestamp + fetch_head = git_dir / "FETCH_HEAD" + fetch_head.write_text("test") + + # refresh: never validates to 365250 days, not the NEVER_REFRESH sentinel + refresh = cv.source_refresh("never") + with caplog.at_level(logging.DEBUG, logger="esphome.git"): + result_dir, revert = git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + mock_run_git_command.assert_not_called() + assert result_dir == repo_dir + assert revert is None + + # Should log refresh disabled at debug level, not a countdown + assert f"Skipping update for {url}@{ref} (refresh disabled)" in caplog.text + assert "will refresh on the next run" not in caplog.text + def test_clone_or_update_clones_missing_repo( tmp_path: Path, mock_run_git_command: Mock @@ -371,6 +601,8 @@ def test_clone_or_update_clones_missing_repo( # repo_dir should NOT exist assert not repo_dir.exists() + mock_run_git_command.side_effect = _make_clone_side_effect(repo_dir) + # Test with NEVER_REFRESH - should still clone since repo doesn't exist result_dir, revert = git.clone_or_update( url=url, @@ -405,6 +637,7 @@ def test_clone_or_update_with_none_refresh_always_updates( repo_dir.mkdir(parents=True) git_dir = repo_dir / ".git" git_dir.mkdir() + _mark_clone_complete(repo_dir) # Create FETCH_HEAD file with very recent timestamp (1 second ago) fetch_head = git_dir / "FETCH_HEAD" @@ -486,6 +719,9 @@ def test_clone_or_update_recovers_from_git_failures( # Default successful responses if cmd_type == "rev-parse": return "abc123" + if cmd_type == "clone": + # Simulate the recovery re-clone creating the repo directory + _simulate_cloned_repo(repo_dir) return "" mock_run_git_command.side_effect = git_command_side_effect @@ -813,6 +1049,273 @@ def test_clone_or_update_stale_clone_is_retried_after_cleanup( assert call_count["fetch"] == 2 +def test_clone_or_update_recloned_when_marker_missing_with_never_refresh( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """A repo dir without the completion marker is an interrupted clone. + + It must be removed and re-cloned even with NEVER_REFRESH, which would + otherwise trust the broken directory forever. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "1.8.4" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Simulate an interrupted clone: directory exists, no marker + repo_dir.mkdir(parents=True) + (repo_dir / ".git").mkdir() + + mock_run_git_command.side_effect = _make_clone_side_effect(repo_dir) + + result_dir, _ = git.clone_or_update( + url=url, ref=ref, refresh=git.NEVER_REFRESH, domain=domain + ) + + clone_calls = [c for c in mock_run_git_command.call_args_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + assert result_dir == repo_dir + # The fresh clone completed, so the marker must now be present + assert _marker_path(repo_dir).is_file() + + +def test_clone_or_update_recloned_when_marker_missing_with_skip_external_update( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """skip_external_update must not preserve an interrupted clone.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + + repo_dir.mkdir(parents=True) + (repo_dir / ".git").mkdir() + + mock_run_git_command.side_effect = _make_clone_side_effect(repo_dir) + + CORE.skip_external_update = True + result_dir, _ = git.clone_or_update( + url=url, ref=None, refresh=TimePeriodSeconds(days=1), domain=domain + ) + + clone_calls = [c for c in mock_run_git_command.call_args_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + assert result_dir == repo_dir + assert _marker_path(repo_dir).is_file() + + +def test_fresh_clone_writes_completion_marker_with_debug_info( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """The marker is written after a fresh clone and records key and hash dir.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "main" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + mock_run_git_command.side_effect = _make_clone_side_effect(repo_dir) + + git.clone_or_update(url=url, ref=ref, refresh=git.NEVER_REFRESH, domain=domain) + + marker = _marker_path(repo_dir) + assert marker.is_file() + content = marker.read_text() + assert f"{url}@{ref}" in content + assert repo_dir.name in content + + +def test_marker_is_deleted_before_rmtree( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """The marker must be gone even if rmtree fails partway. + + Simulated by an rmtree that does nothing: the directory survives but the + marker must already have been deleted, so the next run still re-clones + instead of trusting a partially deleted worktree. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "main" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + _setup_old_repo(repo_dir) + assert _marker_path(repo_dir).is_file() + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) == "stash": + raise GitCommandError("fatal: unable to write new index file") + return "abc123" + + mock_run_git_command.side_effect = git_command_side_effect + + with ( + patch("esphome.git.rmtree"), + pytest.raises(GitCommandError), + ): + git.clone_or_update( + url=url, ref=ref, refresh=TimePeriodSeconds(days=1), domain=domain + ) + + # rmtree never deleted anything, yet the marker is gone + assert repo_dir.is_dir() + assert not _marker_path(repo_dir).is_file() + + +def test_failed_marker_write_does_not_fail_the_clone( + tmp_path: Path, + mock_run_git_command: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """A marker write failure must not fail an otherwise complete clone. + + The clone is valid; the missing marker only costs a re-clone on the next + run, so the error is logged as a warning instead of propagating. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + + mock_run_git_command.side_effect = _make_clone_side_effect(repo_dir) + + with patch( + "esphome.git.write_file", side_effect=EsphomeError("Could not write file") + ): + result_dir, _ = git.clone_or_update( + url=url, ref=None, refresh=git.NEVER_REFRESH, domain=domain + ) + + assert result_dir == repo_dir + assert not _marker_path(repo_dir).is_file() + assert "Could not write clone completion marker" in caplog.text + + +def test_corrupt_git_dir_without_head_recovers( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """A .git with neither FETCH_HEAD nor HEAD must recover, not crash. + + The age check stats FETCH_HEAD falling back to HEAD; if both are gone + (partially deleted clone) the stat raised an unhandled FileNotFoundError + before the broken-repository recovery could run. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + + # Marker present but .git gutted: no FETCH_HEAD, no HEAD + repo_dir.mkdir(parents=True) + (repo_dir / ".git").mkdir() + _mark_clone_complete(repo_dir) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "rev-parse": + raise GitCommandError("ambiguous argument 'HEAD': unknown revision") + if cmd_type == "clone": + _simulate_cloned_repo(repo_dir) + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + result_dir, _ = git.clone_or_update( + url=url, ref=None, refresh=TimePeriodSeconds(days=1), domain=domain + ) + + assert result_dir == repo_dir + clone_calls = [c for c in mock_run_git_command.call_args_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + assert _marker_path(repo_dir).is_file() + + +def test_remove_repo_dir_tolerates_marker_unlink_failure(tmp_path: Path) -> None: + """A locked marker file must not abort the directory removal.""" + repo_dir = tmp_path / "repo" + repo_dir.mkdir() + (repo_dir / ".git").mkdir() + _mark_clone_complete(repo_dir) + + with patch.object(Path, "unlink", side_effect=PermissionError("locked")): + git._remove_repo_dir(repo_dir) + + # rmtree still removed the directory, marker included + assert not repo_dir.exists() + + +def test_clone_or_update_recovery_preserves_subpath( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Recovery must re-clone into the same subpath-ed directory. + + Without passing subpath through, the recursive recovery call would + recompute the destination without the subpath and clone (and write the + completion marker) at the wrong location. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "main" + domain = "test" + subpath = Path("mylib") + repo_dir = _compute_repo_dir(url, ref, domain) / subpath + + _setup_old_repo(repo_dir) + + call_counts: dict[str, int] = {} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type: + call_counts[cmd_type] = call_counts.get(cmd_type, 0) + 1 + # First rev-parse fails (broken repo) to trigger recovery + if cmd_type == "rev-parse" and call_counts[cmd_type] == 1: + raise GitCommandError( + "ambiguous argument 'HEAD': unknown revision or path not in the working tree." + ) + if cmd_type == "clone": + # Create whatever directory the clone was asked to target + target = Path(cmd[-1]) + target.mkdir(parents=True, exist_ok=True) + (target / ".git").mkdir(exist_ok=True) + if cmd_type == "rev-parse": + return "abc123" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + result_dir, _ = git.clone_or_update( + url=url, + ref=ref, + refresh=TimePeriodSeconds(days=1), + domain=domain, + subpath=subpath, + ) + + # The recovery re-clone must target the subpath-ed directory and the + # completion marker must land there too + clone_calls = [c for c in mock_run_git_command.call_args_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + assert clone_calls[0][0][0][-1] == str(repo_dir) + assert result_dir == repo_dir + assert _marker_path(repo_dir).is_file() + + def test_clone_with_ref_uses_shallow_fetch( tmp_path: Path, mock_run_git_command: Mock ) -> None: @@ -849,46 +1352,6 @@ def test_clone_with_ref_uses_shallow_fetch( assert ref in fetch_calls[0][0][0] -def test_clone_with_submodules_uses_shallow_submodule_update( - tmp_path: Path, mock_run_git_command: Mock -) -> None: - """Submodule init on a fresh clone should use --depth=1.""" - CORE.config_path = tmp_path / "test.yaml" - - url = "https://github.com/test/repo" - domain = "test" - repo_dir = _compute_repo_dir(url, None, domain) - - def git_command_side_effect( - cmd: list[str], cwd: str | None = None, **kwargs: Any - ) -> str: - if _get_git_command_type(cmd) == "clone": - repo_dir.mkdir(parents=True, exist_ok=True) - (repo_dir / ".git").mkdir(exist_ok=True) - return "" - - mock_run_git_command.side_effect = git_command_side_effect - - git.clone_or_update( - url=url, - ref=None, - refresh=None, - domain=domain, - submodules=["components/foo"], - ) - - submodule_calls = [ - c for c in mock_run_git_command.call_args_list if "submodule" in c[0][0] - ] - assert len(submodule_calls) == 1 - cmd = submodule_calls[0][0][0] - assert "--depth=1" in cmd - assert "components/foo" in cmd - # The `--` terminator must precede the submodule paths so a path - # beginning with `-` cannot be parsed as an option. - assert cmd.index("--") < cmd.index("components/foo") - - def test_refresh_fetch_is_shallow(tmp_path: Path, mock_run_git_command: Mock) -> None: """The refresh-path fetch should use --depth=1.""" CORE.config_path = tmp_path / "test.yaml" @@ -913,10 +1376,91 @@ def test_refresh_fetch_is_shallow(tmp_path: Path, mock_run_git_command: Mock) -> assert cmd[-1] == ref -def test_refresh_submodule_update_is_shallow( +@pytest.mark.parametrize( + "refresh", [None, TimePeriodSeconds(days=1)], ids=["clone", "refresh"] +) +def test_all_submodules_skipped_without_gitmodules( + tmp_path: Path, mock_run_git_command: Mock, refresh: TimePeriodSeconds | None +) -> None: + """init_submodules is a no-op for repos with no .gitmodules. + + This is the esp-idf toolchain library scenario from issue #17860: the + PlatformIO library converter requests "all submodules" for every git + library, and most libraries declare none. The git submodule porcelain + must not run at all in that case — it fails outright on some git + installations. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + + if refresh is None: + mock_run_git_command.side_effect = _make_clone_side_effect(repo_dir) + else: + _setup_old_repo(repo_dir) + mock_run_git_command.return_value = "abc123" + + git.clone_or_update( + url=url, + ref=None, + refresh=refresh, + domain=domain, + init_submodules=True, + ) + + assert not _submodule_calls(mock_run_git_command) + + +@pytest.mark.parametrize( + "refresh", [None, TimePeriodSeconds(days=1)], ids=["clone", "refresh"] +) +def test_all_submodules_updated_with_gitmodules( + tmp_path: Path, mock_run_git_command: Mock, refresh: TimePeriodSeconds | None +) -> None: + """init_submodules initializes all submodules when .gitmodules exists.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + + if refresh is None: + mock_run_git_command.side_effect = _make_clone_side_effect( + repo_dir, gitmodules=True + ) + else: + _setup_old_repo(repo_dir) + (repo_dir / ".gitmodules").write_text("test") + mock_run_git_command.return_value = "abc123" + + git.clone_or_update( + url=url, + ref=None, + refresh=refresh, + domain=domain, + init_submodules=True, + ) + + submodule_calls = _submodule_calls(mock_run_git_command) + # Which submodules get populated is git's own policy, so no status + # verification follows the update. + assert len(submodule_calls) == 1 + cmd = submodule_calls[0][0][0] + assert cmd[2] == "update" + assert "--depth=1" in cmd + # Recursive, mirroring PlatformIO's recursive library clones. + assert "--recursive" in cmd + _assert_submodule_runs_without_isolation(submodule_calls[0], repo_dir) + + +def test_recovery_reclone_keeps_credentials_and_cache_key( tmp_path: Path, mock_run_git_command: Mock ) -> None: - """The refresh-path submodule update should use --depth=1.""" + """The recovery re-clone must not re-apply credentials to the already + rewritten URL (no doubled userinfo) and must land in the same cache + directory, or a credentialed private repo re-clones on every run.""" CORE.config_path = tmp_path / "test.yaml" url = "https://github.com/test/repo" @@ -924,24 +1468,820 @@ def test_refresh_submodule_update_is_shallow( repo_dir = _compute_repo_dir(url, None, domain) _setup_old_repo(repo_dir) - mock_run_git_command.return_value = "abc123" + (repo_dir / ".gitmodules").write_text("test") - git.clone_or_update( + calls = {"submodule": 0} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) == "clone": + _simulate_cloned_repo(repo_dir) + if _get_git_command_type(cmd) == "submodule": + calls["submodule"] += 1 + if calls["submodule"] == 1: + raise git.GitCommandError("git submodule update exited with code 1") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + recovered_dir, _ = git.clone_or_update( url=url, ref=None, refresh=TimePeriodSeconds(days=1), domain=domain, - submodules=["components/foo"], + username="user", + password="hunter2", + init_submodules=True, ) - submodule_calls = [ - c for c in mock_run_git_command.call_args_list if "submodule" in c[0][0] + assert recovered_dir == repo_dir + clone_cmds = [ + c[0][0] + for c in mock_run_git_command.call_args_list + if _get_git_command_type(c[0][0]) == "clone" ] - assert len(submodule_calls) == 1 - cmd = submodule_calls[0][0][0] - assert "--depth=1" in cmd - assert "components/foo" in cmd - assert cmd.index("--") < cmd.index("components/foo") + assert clone_cmds + clone_url = clone_cmds[0][-2] + assert clone_url == "https://user:hunter2@github.com/test/repo" + assert clone_url.count("@") == 1 + + +def test_refresh_submodule_failure_recovers_then_raises( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """A refresh-path submodule failure routes through the recovery re-clone. + + The broken repo is removed and re-cloned; when the submodule update fails + again on the fresh clone the cache entry is removed and the error + propagates, instead of leaving behind a repo the refresh window would + silently accept on the next run. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + + _setup_old_repo(repo_dir) + (repo_dir / ".gitmodules").write_text("test") + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) == "clone": + _simulate_cloned_repo(repo_dir) + (repo_dir / ".gitmodules").write_text("test") + if _get_git_command_type(cmd) == "submodule": + raise git.GitCommandError("git submodule update exited with code 1") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + with pytest.raises(git.GitCommandError, match="exited with code 1"): + git.clone_or_update( + url=url, + ref=None, + refresh=TimePeriodSeconds(days=1), + domain=domain, + init_submodules=True, + ) + + assert not repo_dir.is_dir() + # Recovery removed the repo and re-cloned before failing again. + assert any( + _get_git_command_type(c[0][0]) == "clone" + for c in mock_run_git_command.call_args_list + ) + + +def _lock_path(url: str, ref: str | None, domain: str) -> Path: + """The lock file the implementation uses for one cache entry.""" + return git._repo_lock_path(git._cache_key(url, ref), domain) + + +class _SetEventOnWaitLog(logging.Handler): + """Set an event when the 'Waiting for another process' record is emitted, + so tests can react to a caller observably blocking on the lock instead of + racing a wall-clock timer.""" + + def __init__(self, event: threading.Event) -> None: + super().__init__() + self._event = event + + def emit(self, record: logging.LogRecord) -> None: + if "Waiting for another process" in record.getMessage(): + self._event.set() + + +def test_clone_or_update_serializes_concurrent_clones( + tmp_path: Path, mock_run_git_command: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Two concurrent callers for the same uncached repo must not both clone. + + Without the per-entry lock both callers pass the is_dir() check before + either clone finishes, so the second one either clones on top of the + first or reads a half populated worktree (device-builder issue 2425). + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + + start_together = threading.Barrier(2) + other_caller_waiting = threading.Event() + + def on_clone() -> None: + # Hold the lock until the other caller is observably blocked on it, + # so the interleaving is guaranteed rather than raced on a timer. + assert other_caller_waiting.wait(timeout=30) + + mock_run_git_command.side_effect = _make_clone_side_effect( + repo_dir, on_clone=on_clone + ) + + results: list[Path] = [] + errors: list[BaseException] = [] + + def call() -> None: + try: + start_together.wait() + result_dir, _ = git.clone_or_update( + url=url, ref=None, refresh=git.NEVER_REFRESH, domain=domain + ) + results.append(result_dir) + except BaseException as err: # noqa: BLE001 - re-raised via errors below + errors.append(err) + + handler = _SetEventOnWaitLog(other_caller_waiting) + git_logger = logging.getLogger("esphome.git") + git_logger.addHandler(handler) + try: + with caplog.at_level(logging.INFO): + threads = [threading.Thread(target=call) for _ in range(2)] + for t in threads: + t.start() + for t in threads: + t.join() + finally: + git_logger.removeHandler(handler) + + assert not errors + assert results == [repo_dir, repo_dir] + clone_calls = [ + c + for c in mock_run_git_command.call_args_list + if _get_git_command_type(c[0][0]) == "clone" + ] + # The second caller waited for the lock, then saw the completed clone. + assert len(clone_calls) == 1 + assert _marker_path(repo_dir).is_file() + + +def test_clone_or_update_creates_lock_file_next_to_hash_dir( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """The lock file lives beside the hash dir, never inside it. + + Checked while the clone runs (the lock is held): filelock's Windows + backend deletes the lock file on release, so probing after the call + would only work on Unix. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + lock_path = _lock_path(url, None, domain) + lock_held_during_clone: list[bool] = [] + + mock_run_git_command.side_effect = _make_clone_side_effect( + repo_dir, on_clone=lambda: lock_held_during_clone.append(lock_path.is_file()) + ) + + result_dir, _ = git.clone_or_update( + url=url, ref=None, refresh=git.NEVER_REFRESH, domain=domain + ) + + assert result_dir == repo_dir + assert lock_path.parent == repo_dir.parent + assert lock_held_during_clone == [True] + + +def test_clone_or_update_subpath_locks_at_hash_dir_level( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """With a subpath the lock still guards the whole hash dir cache entry.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + hash_dir = _compute_repo_dir(url, None, domain) + repo_dir = hash_dir / "lib" + lock_path = _lock_path(url, None, domain) + lock_held_during_clone: list[bool] = [] + + mock_run_git_command.side_effect = _make_clone_side_effect( + repo_dir, on_clone=lambda: lock_held_during_clone.append(lock_path.is_file()) + ) + + result_dir, _ = git.clone_or_update( + url=url, + ref=None, + refresh=git.NEVER_REFRESH, + domain=domain, + subpath=Path("lib"), + ) + + assert result_dir == repo_dir + assert lock_path == hash_dir.parent / f"{hash_dir.name}.lock" + assert lock_held_during_clone == [True] + + +def test_clone_or_update_recovery_holds_lock_without_deadlock( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Recovery re-clones while holding the lock and must not re-acquire it. + + A naive re-acquisition would deadlock here, since OS file locks taken on + separate descriptors conflict even within one process. The lock file + itself must survive the recovery rmtree of the broken repo dir: the + re-clone runs after the rmtree, so probing the lock file there proves + it. Probing after the call would only work on Unix, since filelock's + Windows backend deletes the lock file on release. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + lock_path = _lock_path(url, None, domain) + lock_held_during_reclone: list[bool] = [] + + _setup_old_repo(repo_dir) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) == "stash": + raise git.GitCommandError("broken repository") + if _get_git_command_type(cmd) == "clone": + lock_held_during_reclone.append(lock_path.is_file()) + _simulate_cloned_repo(repo_dir) + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + recovered_dir, _ = git.clone_or_update( + url=url, ref=None, refresh=TimePeriodSeconds(days=1), domain=domain + ) + + assert recovered_dir == repo_dir + assert lock_held_during_reclone == [True] + + +def _hold_lock_in_thread(lock_path: Path) -> tuple[threading.Thread, threading.Event]: + """Hold the lock from another thread; returns the thread and its release event. + + OS file locks taken on separate descriptors conflict even within one + process, so a second FileLock instance in a thread contends the same + way another process would. + """ + held = threading.Event() + release = threading.Event() + + def hold() -> None: + with FileLock(str(lock_path)): + held.set() + release.wait(timeout=30) + + holder = threading.Thread(target=hold) + holder.start() + assert held.wait(timeout=30) + return holder, release + + +def test_clone_or_update_logs_wait_on_contended_lock( + tmp_path: Path, mock_run_git_command: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """A contended acquire logs a redacted waiting message instead of + silently blocking.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://user:hunter2@github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + mock_run_git_command.side_effect = _make_clone_side_effect(repo_dir) + + holder, release = _hold_lock_in_thread(_lock_path(url, None, domain)) + + # Free the lock only once the waiting message has been emitted, so the + # release is caused by the thing being asserted instead of racing it. + handler = _SetEventOnWaitLog(release) + git_logger = logging.getLogger("esphome.git") + git_logger.addHandler(handler) + try: + with caplog.at_level(logging.INFO): + result_dir, _ = git.clone_or_update( + url=url, ref=None, refresh=git.NEVER_REFRESH, domain=domain + ) + finally: + git_logger.removeHandler(handler) + release.set() + holder.join() + + assert result_dir == repo_dir + waiting = [ + r.getMessage() + for r in caplog.records + if "Waiting for another process" in r.getMessage() + ] + assert len(waiting) == 1 + assert "hunter2" not in waiting[0] + assert "://***@" in waiting[0] + + +def test_revert_skips_on_contended_lock( + tmp_path: Path, + mock_run_git_command: Mock, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """revert() only runs on an already-failing path; when it cannot get the + lock within the bounded timeout it warns and skips instead of hanging.""" + CORE.config_path = tmp_path / "test.yaml" + monkeypatch.setattr(git, "_REVERT_LOCK_TIMEOUT_SECONDS", 0.05) + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + _setup_old_repo(repo_dir) + + # A bare return value satisfies the rev-parse; the other commands' + # outputs are unused. + mock_run_git_command.return_value = "old_sha" + + _, revert = git.clone_or_update( + url=url, ref=None, refresh=TimePeriodSeconds(days=1), domain=domain + ) + assert revert is not None + + holder, release = _hold_lock_in_thread(_lock_path(url, None, domain)) + calls_before = len(mock_run_git_command.call_args_list) + with caplog.at_level(logging.INFO): + assert revert() is False + release.set() + holder.join() + + # No git reset was issued; the wait and the skip were both logged. + assert len(mock_run_git_command.call_args_list) == calls_before + assert any("Waiting for another process" in r.getMessage() for r in caplog.records) + assert any("skipping revert" in r.getMessage() for r in caplog.records) + + +def test_update_clears_marker_while_rewriting( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """The completion marker is absent while a refresh rewrites the entry + and restored once the rewrite finishes, so a timed-out peer's fallback + never trusts a mid-rewrite tree and a crashed update re-clones.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + _setup_old_repo(repo_dir) + + marker_during_rewrite: list[bool] = [] + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) in ("stash", "fetch", "reset"): + marker_during_rewrite.append(_marker_path(repo_dir).is_file()) + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + git.clone_or_update( + url=url, ref=None, refresh=TimePeriodSeconds(days=1), domain=domain + ) + + assert marker_during_rewrite == [False, False, False] + assert _marker_path(repo_dir).is_file() + + +def test_revert_skips_when_checkout_moved( + tmp_path: Path, mock_run_git_command: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """revert() only undoes this process's own update; when another process + refreshed the entry in the meantime it skips instead of rolling the + peer's newer checkout backwards.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + _setup_old_repo(repo_dir) + + # Pre-update SHA, post-update SHA, then a peer's SHA at revert time. + shas = iter(["old_sha", "new_sha", "peer_sha"]) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) == "rev-parse": + return next(shas) + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + _, revert = git.clone_or_update( + url=url, ref=None, refresh=TimePeriodSeconds(days=1), domain=domain + ) + assert revert is not None + + with caplog.at_level(logging.WARNING): + assert revert() is False + + resets = [ + c[0][0] + for c in mock_run_git_command.call_args_list + if _get_git_command_type(c[0][0]) == "reset" and c[0][0][-1] == "old_sha" + ] + assert resets == [] + assert any("checkout moved" in r.getMessage() for r in caplog.records) + + +def test_revert_returns_false_when_reset_fails( + tmp_path: Path, mock_run_git_command: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """A failed git reset inside revert() is reported through the bool + contract instead of raising a cv.Invalid that would replace the + caller's original error.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + _setup_old_repo(repo_dir) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) == "rev-parse": + return "old_sha" + # Only revert's reset targets the recorded SHA; the update path's + # reset targets FETCH_HEAD and must succeed. + if cmd[-1] == "old_sha": + raise git.GitCommandError("object not found") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + _, revert = git.clone_or_update( + url=url, ref=None, refresh=TimePeriodSeconds(days=1), domain=domain + ) + assert revert is not None + + with caplog.at_level(logging.WARNING): + assert revert() is False + + assert any("Could not revert" in r.getMessage() for r in caplog.records) + # The entry cannot be trusted after a failed reset; the dropped marker + # forces a re-clone on the next use. + assert not _marker_path(repo_dir).is_file() + + +def _raise_oserror_on_acquire( + monkeypatch: pytest.MonkeyPatch, code: int = errno.ENOLCK +) -> None: + """Make every FileLock acquire fail with the given errno.""" + + def broken_acquire(self: FileLock, *args: Any, **kwargs: Any) -> None: + raise OSError(code, os.strerror(code)) + + monkeypatch.setattr(FileLock, "acquire", broken_acquire) + + +@pytest.mark.parametrize( + ("code", "expected_fragment"), + [ + # Genuinely missing lock support is reported as such. + (errno.ENOLCK, "does not support locking"), + # A cache directory problem is not blamed on lock support; git + # reports the real error when it actually matters. + (errno.EROFS, "Could not take the cache entry lock"), + ], +) +def test_clone_or_update_continues_unlocked_when_filesystem_cannot_lock( + tmp_path: Path, + mock_run_git_command: Mock, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, + code: int, + expected_fragment: str, +) -> None: + """A filesystem where taking the lock fails (e.g. NFS without a lock + daemon) degrades to the old unlocked behavior instead of failing.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + mock_run_git_command.side_effect = _make_clone_side_effect(repo_dir) + _raise_oserror_on_acquire(monkeypatch, code) + + with caplog.at_level(logging.WARNING): + result_dir, _ = git.clone_or_update( + url=url, ref=None, refresh=git.NEVER_REFRESH, domain=domain + ) + + assert result_dir == repo_dir + assert _marker_path(repo_dir).is_file() + warnings = [ + r.getMessage() + for r in caplog.records + if "continuing without a lock" in r.getMessage() + ] + assert warnings + assert expected_fragment in warnings[0] + + +@pytest.mark.parametrize("broken_from_start", [True, False]) +def test_revert_continues_unlocked_when_filesystem_cannot_lock( + tmp_path: Path, + mock_run_git_command: Mock, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, + broken_from_start: bool, +) -> None: + """A revert still resets when locking is unavailable, whether the wrapper + already fell back to unlocked (revert sees no lock at all) or the + filesystem stops locking between the update and the revert.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + _setup_old_repo(repo_dir) + + # A bare return value satisfies the rev-parse; the other commands' + # outputs are unused. + mock_run_git_command.return_value = "old_sha" + + if broken_from_start: + _raise_oserror_on_acquire(monkeypatch) + + with caplog.at_level(logging.WARNING): + _, revert = git.clone_or_update( + url=url, ref=None, refresh=TimePeriodSeconds(days=1), domain=domain + ) + assert revert is not None + if not broken_from_start: + _raise_oserror_on_acquire(monkeypatch) + # The reset ran (unlocked), so the revert reports success. + assert revert() is True + + assert mock_run_git_command.call_args_list[-1][0][0] == [ + "git", + "reset", + "--hard", + "old_sha", + ] + assert any("continuing without a lock" in r.getMessage() for r in caplog.records) + + +def _script_acquire_statuses( + monkeypatch: pytest.MonkeyPatch, statuses: list["git._LockStatus"] +) -> list[float]: + """Replace _acquire_repo_lock with a scripted sequence; returns the + timeouts it was called with.""" + timeouts: list[float] = [] + status_iter = iter(statuses) + + def fake_acquire( + lock: FileLock, safe_key: str, timeout: float, **kwargs: Any + ) -> "git._LockStatus": + timeouts.append(timeout) + return next(status_iter) + + monkeypatch.setattr(git, "_acquire_repo_lock", fake_acquire) + return timeouts + + +@pytest.mark.parametrize("subpath", [None, Path("lib")]) +def test_clone_or_update_uses_complete_entry_when_lock_wait_times_out( + tmp_path: Path, + mock_run_git_command: Mock, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, + subpath: Path | None, +) -> None: + """A bounded wait behind a stalled holder falls back to an existing + complete cache entry instead of hanging every peer.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + if subpath is not None: + repo_dir = repo_dir / subpath + _simulate_cloned_repo(repo_dir) + _mark_clone_complete(repo_dir) + + timeouts = _script_acquire_statuses(monkeypatch, [git._LockStatus.TIMEOUT]) + + with caplog.at_level(logging.WARNING): + result_dir, revert = git.clone_or_update( + url=url, + ref=None, + refresh=TimePeriodSeconds(days=1), + domain=domain, + subpath=subpath, + ) + + assert result_dir == repo_dir + assert revert is None + # Nothing was cloned or refreshed; the existing entry was used as-is. + assert mock_run_git_command.call_args_list == [] + assert timeouts == [git._COMPLETE_ENTRY_LOCK_TIMEOUT_SECONDS] + assert any( + "proceeding with the existing clone" in r.getMessage() for r in caplog.records + ) + + +def test_clone_or_update_waits_unbounded_without_complete_entry( + tmp_path: Path, + mock_run_git_command: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """With no complete entry there is nothing to fall back to, so after the + bounded wait expires the caller keeps waiting for the holder's clone.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + mock_run_git_command.side_effect = _make_clone_side_effect(repo_dir) + + timeouts = _script_acquire_statuses( + monkeypatch, [git._LockStatus.TIMEOUT, git._LockStatus.ACQUIRED] + ) + + result_dir, _ = git.clone_or_update( + url=url, ref=None, refresh=git.NEVER_REFRESH, domain=domain + ) + + assert result_dir == repo_dir + assert timeouts == [git._COMPLETE_ENTRY_LOCK_TIMEOUT_SECONDS, -1] + # The clone proceeded normally once the lock was finally acquired. + assert _marker_path(repo_dir).is_file() + + +def _real_git(*args: str, cwd: Path) -> None: + """Run real git to build a test fixture repository.""" + subprocess.run( + [ + "git", + "-c", + "user.email=test@test.invalid", + "-c", + "user.name=test", + "-c", + "commit.gpgsign=false", + "-c", + "protocol.file.allow=always", + *args, + ], + cwd=cwd, + check=True, + capture_output=True, + ) + + +# Git blocks file-protocol submodules by default (CVE-2022-39253); the e2e +# tests allow them via GIT_CONFIG_* environment variables, which reach the +# child git processes through run_git_command's filtered environment. +_ALLOW_FILE_PROTOCOL_ENV = { + "GIT_CONFIG_COUNT": "1", + "GIT_CONFIG_KEY_0": "protocol.file.allow", + "GIT_CONFIG_VALUE_0": "always", +} + + +def _make_real_repo(path: Path, filename: str) -> None: + """Create a real git repository containing one committed file.""" + path.mkdir() + _real_git("init", "-q", cwd=path) + (path / filename).write_text("content") + _real_git("add", filename, cwd=path) + _real_git("commit", "-q", "-m", "init", cwd=path) + + +def _add_submodule( + repo: Path, url: Path, path: str, *, update_none: bool = False +) -> None: + """Add ``url`` as a submodule of ``repo`` at ``path`` and commit it.""" + _real_git("submodule", "add", str(url), path, cwd=repo) + if update_none: + _real_git( + "config", "-f", ".gitmodules", f"submodule.{path}.update", "none", cwd=repo + ) + _real_git("add", ".gitmodules", cwd=repo) + _real_git("commit", "-q", "-m", f"add submodule {path}", cwd=repo) + + +def test_clone_or_update_real_git_without_submodules(tmp_path: Path) -> None: + """End-to-end with real git: a repo with no .gitmodules clones cleanly. + + This is the issue #17860 scenario: requesting "all submodules" on a + submodule-less repository must not invoke the git submodule porcelain + and must produce a usable checkout. + """ + CORE.config_path = tmp_path / "test.yaml" + + upstream = tmp_path / "upstream" + _make_real_repo(upstream, "README.md") + + repo_dir, _ = git.clone_or_update( + url=str(upstream), + ref=None, + refresh=None, + domain="test_e2e", + init_submodules=True, + ) + + assert (repo_dir / "README.md").is_file() + + +def test_clone_or_update_real_git_initializes_submodules(tmp_path: Path) -> None: + """End-to-end with real git: submodules are actually checked out. + + Exercises the real `git submodule update` invocation, including the + env handling in run_git_command that the mocked tests cannot cover. + """ + CORE.config_path = tmp_path / "test.yaml" + + sub_repo = tmp_path / "sub" + _make_real_repo(sub_repo, "sub_file.txt") + + upstream = tmp_path / "upstream" + _make_real_repo(upstream, "README.md") + _add_submodule(upstream, sub_repo, "vendor/sub") + + with patch.dict(os.environ, _ALLOW_FILE_PROTOCOL_ENV): + repo_dir, _ = git.clone_or_update( + url=str(upstream), + ref=None, + refresh=None, + domain="test_e2e", + init_submodules=True, + ) + + assert (repo_dir / "vendor" / "sub" / "sub_file.txt").is_file() + + +def test_clone_or_update_real_git_honors_update_none_submodule( + tmp_path: Path, +) -> None: + """End-to-end with real git: submodules declared `update = none` stay skipped. + + Shows git itself skipping the declared paths at both nesting levels + (and exiting 0) while the regular submodules check out. + """ + CORE.config_path = tmp_path / "test.yaml" + + sub_repo = tmp_path / "sub" + _make_real_repo(sub_repo, "sub_file.txt") + + # Intermediate submodule that itself declares a skipped nested submodule. + mid_repo = tmp_path / "mid" + _make_real_repo(mid_repo, "mid_file.txt") + _add_submodule(mid_repo, sub_repo, "vendor/leaf", update_none=True) + + upstream = tmp_path / "upstream" + _make_real_repo(upstream, "README.md") + _add_submodule(upstream, sub_repo, "vendor/sub") + _add_submodule(upstream, sub_repo, "vendor/skipped", update_none=True) + _add_submodule(upstream, mid_repo, "vendor/mid") + + with patch.dict(os.environ, _ALLOW_FILE_PROTOCOL_ENV): + repo_dir, _ = git.clone_or_update( + url=str(upstream), + ref=None, + refresh=None, + domain="test_e2e", + init_submodules=True, + ) + + assert (repo_dir / "vendor" / "sub" / "sub_file.txt").is_file() + assert not (repo_dir / "vendor" / "skipped" / "sub_file.txt").exists() + assert (repo_dir / "vendor" / "mid" / "mid_file.txt").is_file() + assert not ( + repo_dir / "vendor" / "mid" / "vendor" / "leaf" / "sub_file.txt" + ).exists() def test_refresh_picks_up_new_remote_commits( @@ -983,7 +2323,8 @@ def test_refresh_picks_up_new_remote_commits( # Verify the refresh sequence: rev-parse -> stash -> fetch (depth=1) -> reset call_list = mock_run_git_command.call_args_list cmd_sequence = [_get_git_command_type(c[0][0]) for c in call_list] - assert cmd_sequence == ["rev-parse", "stash", "fetch", "reset"] + # The trailing rev-parse records the post-update SHA for revert(). + assert cmd_sequence == ["rev-parse", "stash", "fetch", "reset", "rev-parse"] fetch_cmd = call_list[2][0][0] assert "--depth=1" in fetch_cmd @@ -994,7 +2335,7 @@ def test_refresh_picks_up_new_remote_commits( # revert callback should reset back to the recorded pre-update SHA. assert revert is not None - revert() + assert revert() is True assert mock_run_git_command.call_args_list[-1][0][0] == [ "git", "reset", diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index fad249b0bb..211fbf5112 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1074,3 +1074,21 @@ def test_progressbar_enabled_on_pipe_with_dashboard(monkeypatch) -> None: bar = ProgressBar("Uploading", stream=stream) assert bar.enabled is True + + +@pytest.mark.parametrize( + ("seconds", "expected"), + [ + (0, "0s"), + (42, "42s"), + (60, "1min"), + (3661, "1h 1min"), + (86400, "1d"), + (90000, "1d 1h"), + (86700, "1d 5min"), + (-5, "0s"), + ], +) +def test_format_duration(seconds: float, expected: str) -> None: + """Test that durations are rendered as short human-readable strings.""" + assert helpers.format_duration(seconds) == expected diff --git a/tests/unit_tests/test_lazy_imports.py b/tests/unit_tests/test_lazy_imports.py new file mode 100644 index 0000000000..ee570a84f6 --- /dev/null +++ b/tests/unit_tests/test_lazy_imports.py @@ -0,0 +1,53 @@ +"""Guard the lazy-import contract of ``esphome.__main__``. + +Every ``esphome`` invocation pays for whatever ``esphome.__main__`` +imports at module level before the requested command runs. The +dashboard and device-builder spawn one ``esphome upload`` subprocess +per device, so keeping validation/codegen machinery out of the +top-level import directly lowers the RAM cost of each concurrent +upload (the upload/logs fast path in ``esphome.compiled_config`` +never needs them). + +``script/check_import_time.py`` budgets import *time* in CI; this +test pins down *which* heavy modules must stay out entirely. +""" + +from __future__ import annotations + +import subprocess +import sys + +# Modules that must only load for the commands that actually use them +# (compile/config validation, shell completion), never from a bare +# ``import esphome.__main__``. +HEAVY_MODULES = ( + "argcomplete", + "esphome.codegen", + "esphome.config", + "esphome.config_validation", + "esphome.cpp_generator", + "esphome.loader", + "voluptuous", +) + + +def test_main_module_does_not_import_heavy_modules() -> None: + """A bare ``import esphome.__main__`` must not drag in validation/codegen.""" + check = ( + "import sys; import esphome.__main__; " + f"leaked = [m for m in {HEAVY_MODULES!r} if m in sys.modules]; " + "print(','.join(leaked))" + ) + result = subprocess.run( + [sys.executable, "-c", check], + capture_output=True, + text=True, + check=True, + ) + leaked = result.stdout.strip() + assert not leaked, ( + f"esphome.__main__ imports heavy modules at top level: {leaked}. " + "Import them lazily inside the command that needs them instead; " + "every esphome invocation (including each parallel dashboard " + "upload subprocess) pays for top-level imports." + ) diff --git a/tests/unit_tests/test_loader.py b/tests/unit_tests/test_loader.py index 42e5203a73..41dd462678 100644 --- a/tests/unit_tests/test_loader.py +++ b/tests/unit_tests/test_loader.py @@ -1,19 +1,13 @@ """Unit tests for esphome.loader module.""" import ast -import logging from pathlib import Path import sys import textwrap -from types import ModuleType -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, patch import pytest -import voluptuous as vol -from esphome import config as esphome_config, config_validation as cv -from esphome.core import CORE -import esphome.loader as loader_mod from esphome.loader import ( AliasMeta, ComponentManifest, @@ -21,6 +15,7 @@ from esphome.loader import ( _build_alias_map, _read_aliases, _replace_component_manifest, + get_alias_metadata, get_component, ) from tests.testing_helpers import ComponentManifestOverride @@ -348,17 +343,12 @@ def test_component_manifest_resources_recursive_filter_source_files_supports_sub # Component aliases (renamed-platform back-compat) # --------------------------------------------------------------------------- # -# These tests pin down the substrate behind `ALIASES = [...]` on component -# `__init__.py` files: the AST scanner, the resulting global alias map, the -# Python-import `sys.meta_path` finder, the `get_component` integration, and -# the YAML pre-pass that rewrites legacy top-level keys. -# -# The framework is component-agnostic, so the integration tests inject a -# synthetic alias map (pointing a fake legacy name at the real `esp32` -# component) rather than depending on any specific renamed component. - -# A legacy name that is NOT a real component, used as a synthetic alias. -_FAKE_ALIAS = "esp32_legacy_alias" +# The framework here is the substrate behind `ALIASES = [...]` on component +# `__init__.py` files. These tests pin down the AST scanner, the resulting +# global alias map, the Python-import `sys.meta_path` finder, and the +# integration with `get_component`. The rp2 → rp2040 actual mapping in this +# repo is used as a real-world fixture; other cases use temp dirs / mocks so +# the framework's behavior is testable in isolation. def _write_component(root: Path, name: str, body: str) -> None: @@ -383,12 +373,12 @@ def test_read_aliases_extracts_removal_version(tmp_path: Path) -> None: init.write_text( textwrap.dedent("""\ ALIASES = ['old'] - ALIAS_REMOVAL_VERSION = "2027.6.0" + ALIAS_REMOVAL_VERSION = "2027.7.0" """) ) aliases, removal = _read_aliases(init, ast) assert aliases == ["old"] - assert removal == "2027.6.0" + assert removal == "2027.7.0" def test_read_aliases_skips_dynamic_forms(tmp_path: Path) -> None: @@ -409,28 +399,19 @@ def test_read_aliases_returns_empty_for_missing_declaration(tmp_path: Path) -> N assert removal is None -def test_read_aliases_handles_syntax_error( - tmp_path: Path, caplog: pytest.LogCaptureFixture -) -> None: +def test_read_aliases_handles_syntax_error(tmp_path: Path) -> None: """A broken __init__.py shouldn't crash the alias scanner — it'll - surface as an ImportError elsewhere, but the scanner logs a warning and - yields nothing so other components keep working. The substring pre-filter - only skips files with no ``ALIASES`` token, so this file (which has one) - still reaches the parse.""" + surface as an ImportError elsewhere, but the scanner just yields + nothing so other components keep working. + + The source must contain the substring ``ALIASES`` so the scanner + actually attempts to parse the file; otherwise the early-return + optimization would short-circuit before reaching the parser and + this test would not exercise the syntax-error branch. + """ init = tmp_path / "__init__.py" - init.write_text("ALIASES = ['x']\ndef broken( :\n") + init.write_text("ALIASES = ['oops'\ndef broken( :\n") assert _read_aliases(init, ast) == ([], None) - assert "Could not parse" in caplog.text - - -def test_read_aliases_handles_read_error( - tmp_path: Path, caplog: pytest.LogCaptureFixture -) -> None: - """An unreadable __init__.py logs a warning and yields nothing rather - than aborting the whole component scan.""" - missing = tmp_path / "nope" / "__init__.py" - assert _read_aliases(missing, ast) == ([], None) - assert "Could not read" in caplog.text def test_build_alias_map_aggregates_components(tmp_path: Path) -> None: @@ -480,96 +461,64 @@ def test_build_alias_map_handles_missing_dir(tmp_path: Path) -> None: but possible in some test contexts), we want an empty map rather than a crash — the rest of the loader can still function.""" fake = tmp_path / "does-not-exist" + assert not fake.exists() with patch("esphome.loader.CORE_COMPONENTS_PATH", fake): alias_map, meta_map = _build_alias_map() assert alias_map == {} assert meta_map == {} -def test_build_alias_map_rejects_alias_shadowing_component(tmp_path: Path) -> None: - """An alias that names an existing component package is refused: it would - hijack a live domain, and a self-alias (alias == canonical) would send - ``_lookup_module`` into infinite recursion.""" - # `newcomp` declares itself as an alias — its own package already exists. - _write_component(tmp_path, "newcomp", "ALIASES = ['newcomp']\n") - - from esphome.core import EsphomeError - - with ( - patch("esphome.loader.CORE_COMPONENTS_PATH", tmp_path), - pytest.raises(EsphomeError, match="shadows an existing component"), - ): - _build_alias_map() +# ---- Live integration against the real rp2/rp2040 mapping in this repo ---- -# ---- Integration against a synthetic alias map (fake legacy -> esp32) ---- +def test_real_alias_map_includes_rp2040() -> None: + """The rp2 component declares ``ALIASES = ['rp2040']`` in this repo; + the live alias map should surface it. This guards against future + refactors silently dropping the declaration.""" + meta = get_alias_metadata() + assert "rp2040" in meta + assert meta["rp2040"].canonical == "rp2" + assert meta["rp2040"].removal_version == "2027.7.0" -def _patch_alias_map(monkeypatch: pytest.MonkeyPatch, mapping: dict[str, str]) -> None: - """Force the loader's alias map (used by the finder and get_component). - - Patches the lazily-built caches so both ``_get_alias_map`` and the - installed meta-path finder resolve against ``mapping`` regardless of - what the real on-disk scan would produce. - """ - monkeypatch.setattr("esphome.loader._get_alias_map", lambda: mapping) - - -def test_get_component_resolves_alias(monkeypatch: pytest.MonkeyPatch) -> None: - """``get_component()`` should return the canonical manifest — every +def test_get_component_resolves_alias() -> None: + """``get_component('rp2040')`` should return the rp2 manifest — every caller of the loader (dep checker, schema validator, codegen) hits the canonical component without knowing about the alias.""" - import esphome.loader as loader_mod - - _patch_alias_map(monkeypatch, {_FAKE_ALIAS: "esp32"}) - loader_mod._COMPONENT_CACHE.pop(_FAKE_ALIAS, None) - - canonical = get_component("esp32") - aliased = get_component(_FAKE_ALIAS) - assert canonical is not None - assert aliased is canonical + rp2 = get_component("rp2") + rp2040 = get_component("rp2040") + assert rp2 is not None + assert rp2040 is rp2 -def test_alias_finder_resolves_top_level_import( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """``import esphome.components.`` resolves to the canonical - module via the meta-path finder. ``_FAKE_ALIAS`` == ``esp32_legacy_alias``.""" - _patch_alias_map(monkeypatch, {_FAKE_ALIAS: "esp32"}) - sys.modules.pop(f"esphome.components.{_FAKE_ALIAS}", None) - +def test_alias_finder_resolves_top_level_import() -> None: + """``import esphome.components.rp2040`` resolves to the canonical + module via the meta-path finder.""" + # Remove any cached entry so we exercise the finder, not sys.modules cache. + sys.modules.pop("esphome.components.rp2040", None) finder = _AliasFinder() - spec = finder.find_spec(f"esphome.components.{_FAKE_ALIAS}", None) + spec = finder.find_spec("esphome.components.rp2040", None) assert spec is not None - import esphome.components.esp32 - import esphome.components.esp32_legacy_alias + import esphome.components.rp2 + import esphome.components.rp2040 - assert esphome.components.esp32_legacy_alias is esphome.components.esp32 + assert esphome.components.rp2040 is esphome.components.rp2 -def test_alias_finder_resolves_submodule_import( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """``from esphome.components. import boards`` routes through to - ``esphome.components.esp32.boards`` — same submodule object on both paths. - - The canonical submodule is imported first so its parent module carries - the ``boards`` attribute; ``from import boards`` then resolves - the aliased parent (via the finder) and reads that same attribute, - rather than triggering a fresh file load under the alias name. - ``_FAKE_ALIAS`` == ``esp32_legacy_alias``.""" - _patch_alias_map(monkeypatch, {_FAKE_ALIAS: "esp32"}) - sys.modules.pop(f"esphome.components.{_FAKE_ALIAS}", None) - +def test_alias_finder_resolves_submodule_import() -> None: + """``from esphome.components.rp2040 import boards`` routes through to + ``esphome.components.rp2.boards`` — same submodule object on both + paths.""" + sys.modules.pop("esphome.components.rp2040.boards", None) finder = _AliasFinder() - spec = finder.find_spec(f"esphome.components.{_FAKE_ALIAS}.boards", None) + spec = finder.find_spec("esphome.components.rp2040.boards", None) assert spec is not None - from esphome.components.esp32 import boards as canonical_boards - from esphome.components.esp32_legacy_alias import boards as aliased_boards + from esphome.components.rp2 import boards as rp2_boards + from esphome.components.rp2040 import boards as rp2040_boards - assert aliased_boards is canonical_boards + assert rp2040_boards is rp2_boards def test_alias_finder_ignores_non_components_path() -> None: @@ -581,9 +530,6 @@ def test_alias_finder_ignores_non_components_path() -> None: assert finder.find_spec("os.path", None) is None # `esphome.components` itself (no domain segment) is not a candidate. assert finder.find_spec("esphome.components", None) is None - # A real, non-aliased component domain defers to normal import machinery - # (no component declares an alias in this repo, so the live map is empty). - assert finder.find_spec("esphome.components.logger", None) is None # --------------------------------------------------------------------------- @@ -593,391 +539,121 @@ def test_alias_finder_ignores_non_components_path() -> None: # The companion to the loader-side alias map: ``esphome.config`` runs a # pre-pass over the user's parsed YAML that rewrites legacy top-level keys # to their canonical names, surfacing a one-shot deprecation warning. These -# tests inject a synthetic alias-metadata map so the rewrite behavior, the -# warning text, and the both-keys-present conflict can be tested in isolation. - - -def _patch_alias_metadata( - monkeypatch: pytest.MonkeyPatch, mapping: dict[str, AliasMeta] -) -> None: - monkeypatch.setattr("esphome.loader.get_alias_metadata", lambda: mapping) +# tests pin down the rewrite behavior, the warning text, and the +# both-keys-present conflict. def test_resolve_component_aliases_renames_legacy_key( - monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + caplog: pytest.LogCaptureFixture, ) -> None: - """A legacy alias key should be renamed to the canonical key and a - deprecation warning citing the removal version logged.""" + """A legacy alias key ``rp2040:`` should be renamed to the canonical + ``rp2:`` and a deprecation warning citing the removal version logged.""" + import logging + from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases from esphome.core import CORE - _patch_alias_metadata( - monkeypatch, - {"oldcomp": AliasMeta(canonical="newcomp", removal_version="2027.6.0")}, - ) CORE.data.pop(_ALIAS_WARNED_KEY, None) # ensure the warning fires - config = {"esphome": {"name": "test"}, "oldcomp": {"board": "x"}} + config = {"esphome": {"name": "test"}, "rp2040": {"board": "rpipicow"}} with caplog.at_level(logging.WARNING, logger="esphome.config"): _resolve_component_aliases(config) - assert "oldcomp" not in config - assert config["newcomp"] == {"board": "x"} + assert "rp2040" not in config + assert config["rp2"] == {"board": "rpipicow"} assert any( - "'oldcomp:' top-level key is deprecated" in record.message - and "rename it to 'newcomp:'" in record.message - and "2027.6.0" in record.message + "'rp2040:' top-level key is deprecated" in record.message + and "rename it to 'rp2:'" in record.message + and "2027.7.0" in record.message for record in caplog.records ) def test_resolve_component_aliases_dedupes_warning_within_a_run( - monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + caplog: pytest.LogCaptureFixture, ) -> None: """Schema validators can run twice (auto-load discovery + final pass) so the rename pass must emit the warning only once per alias per run. Deduped via ``CORE.data``; cleared between runs.""" + import logging + from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases from esphome.core import CORE - _patch_alias_metadata( - monkeypatch, - {"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)}, - ) CORE.data.pop(_ALIAS_WARNED_KEY, None) with caplog.at_level(logging.WARNING, logger="esphome.config"): - _resolve_component_aliases({"oldcomp": {"board": "a"}}) - _resolve_component_aliases({"oldcomp": {"board": "b"}}) + _resolve_component_aliases({"rp2040": {"board": "rpipicow"}}) + _resolve_component_aliases({"rp2040": {"board": "rpipico2w"}}) matches = [ r for r in caplog.records - if "'oldcomp:' top-level key is deprecated" in r.message + if "'rp2040:' top-level key is deprecated" in r.message ] assert len(matches) == 1 -def test_resolve_component_aliases_rejects_both_keys_present( - monkeypatch: pytest.MonkeyPatch, -) -> None: +def test_resolve_component_aliases_rejects_both_keys_present() -> None: """If the user has BOTH legacy and canonical keys, silently dropping one would hide a real misconfiguration. Raise instead.""" + import voluptuous as vol + from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases from esphome.core import CORE - _patch_alias_metadata( - monkeypatch, - {"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)}, - ) CORE.data.pop(_ALIAS_WARNED_KEY, None) - config = {"newcomp": {"board": "x"}, "oldcomp": {"board": "x"}} - with pytest.raises(vol.Invalid, match="Both 'oldcomp:'"): + config = { + "rp2": {"board": "rpipicow"}, + "rp2040": {"board": "rpipicow"}, + } + with pytest.raises(vol.Invalid, match="Both 'rp2040:'"): _resolve_component_aliases(config) -def test_resolve_component_aliases_rejects_canonical_key_after_legacy( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """The both-keys conflict must be detected even when the canonical key - appears *after* the legacy key in the config (the up-front conflict - scan, not a position-dependent check).""" - from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases - from esphome.core import CORE - - _patch_alias_metadata( - monkeypatch, - {"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)}, - ) - CORE.data.pop(_ALIAS_WARNED_KEY, None) - config = {"oldcomp": {"board": "x"}, "newcomp": {"board": "x"}} - with pytest.raises(vol.Invalid, match="Both 'oldcomp:'"): - _resolve_component_aliases(config) - - -def test_resolve_component_aliases_rejects_multiple_aliases_of_one_component( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Two different deprecated aliases of the same canonical component is - ambiguous — silently keeping one would hide a misconfiguration.""" - from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases - from esphome.core import CORE - - _patch_alias_metadata( - monkeypatch, - { - "oldcomp": AliasMeta(canonical="newcomp", removal_version=None), - "legacycomp": AliasMeta(canonical="newcomp", removal_version=None), - }, - ) - CORE.data.pop(_ALIAS_WARNED_KEY, None) - config = {"oldcomp": {"board": "x"}, "legacycomp": {"board": "y"}} - with pytest.raises(vol.Invalid, match=r"Multiple deprecated aliases of 'newcomp:'"): - _resolve_component_aliases(config) - - -def test_resolve_component_aliases_preserves_key_position( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """The renamed canonical key keeps the legacy key's original position - rather than being moved to the end of the config.""" - from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases - from esphome.core import CORE - - _patch_alias_metadata( - monkeypatch, - {"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)}, - ) - CORE.data.pop(_ALIAS_WARNED_KEY, None) - config = {"esphome": {"name": "t"}, "oldcomp": {"board": "x"}, "logger": {}} - - _resolve_component_aliases(config) - - assert list(config) == ["esphome", "newcomp", "logger"] - - -def test_resolve_component_aliases_no_op_when_no_legacy_keys( - monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture -) -> None: +def test_resolve_component_aliases_no_op_when_no_legacy_keys() -> None: """The pre-pass must be a no-op (no warning, no mutation) for configs that already use canonical keys.""" + import logging + from esphome.config import _ALIAS_WARNED_KEY, _resolve_component_aliases from esphome.core import CORE - _patch_alias_metadata( - monkeypatch, - {"oldcomp": AliasMeta(canonical="newcomp", removal_version=None)}, - ) CORE.data.pop(_ALIAS_WARNED_KEY, None) - config = {"esphome": {"name": "test"}, "newcomp": {"board": "x"}} + config = {"esphome": {"name": "test"}, "rp2": {"board": "rpipicow"}} original = dict(config) - with caplog.at_level(logging.WARNING, logger="esphome.config"): + with caplog_at_warning() as records: _resolve_component_aliases(config) assert config == original - assert not any("deprecated" in r.message for r in caplog.records) + assert not any("deprecated" in r.message for r in records) + _ = logging # silence unused-import in branches that don't read records -# --------------------------------------------------------------------------- -# ComponentManifest alias properties -# --------------------------------------------------------------------------- +# Helper context manager — small enough to inline rather than pull in +# caplog for the simple "did anything warn?" case above. +import contextlib # noqa: E402 -def test_component_manifest_alias_properties_default_empty() -> None: - """``aliases`` / ``alias_removal_version`` fall back to ``[]`` / ``None`` - when the component module declares neither. +@contextlib.contextmanager +def caplog_at_warning(): + """Minimal in-test caplog substitute: collect WARNING records on a + dedicated handler attached to ``esphome.config``.""" + import logging - Uses a real ``ModuleType`` rather than a ``MagicMock`` so that the - ``getattr(..., default)`` fallback is actually exercised — a bare mock - auto-creates any attribute on access and would never hit the default.""" - mod = ModuleType("fake_component") - manifest = ComponentManifest(mod) - assert manifest.aliases == [] - assert manifest.alias_removal_version is None + logger = logging.getLogger("esphome.config") + records: list[logging.LogRecord] = [] + class _Handler(logging.Handler): + def emit(self, record): # noqa: D401 + records.append(record) -def test_component_manifest_alias_properties_read_module_values() -> None: - """The properties surface the module's declared values verbatim.""" - mod = MagicMock() - mod.ALIASES = ["legacy"] - mod.ALIAS_REMOVAL_VERSION = "2027.6.0" - manifest = ComponentManifest(mod) - assert manifest.aliases == ["legacy"] - assert manifest.alias_removal_version == "2027.6.0" - - -# --------------------------------------------------------------------------- -# Real (unpatched) lazy build + cache and remaining scanner branches -# --------------------------------------------------------------------------- - - -def test_get_alias_map_real_build_and_caches(monkeypatch: pytest.MonkeyPatch) -> None: - """Exercise the real lazy build over the actual components dir (no patch): - the first call scans and caches, the second returns the cached object.""" - monkeypatch.setattr(loader_mod, "_ALIAS_MAP_CACHE", None) - first = loader_mod._get_alias_map() - second = loader_mod._get_alias_map() - assert isinstance(first, dict) - assert first is second # cached, not rebuilt on the second call - - -def test_get_alias_metadata_real_build_and_caches( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr(loader_mod, "_ALIAS_META_CACHE", None) - first = loader_mod.get_alias_metadata() - second = loader_mod.get_alias_metadata() - assert isinstance(first, dict) - assert first is second - - -def test_build_alias_map_skips_files_and_initless_dirs(tmp_path: Path) -> None: - """Loose files and directories without an ``__init__.py`` are ignored; - only real component packages contribute to the map.""" - (tmp_path / "loose_file.py").write_text("ALIASES = ['ignored']\n") - (tmp_path / "initless").mkdir() # a dir, but no __init__.py - _write_component(tmp_path, "realcomp", "ALIASES = ['legacy']\n") - - with patch("esphome.loader.CORE_COMPONENTS_PATH", tmp_path): - alias_map, _ = _build_alias_map() - - assert alias_map == {"legacy": "realcomp"} - - -def test_read_aliases_ignores_non_assignment_and_complex_targets( - tmp_path: Path, -) -> None: - """Non-assignment statements and assignments to non-Name targets are - skipped; only simple ``NAME = ...`` assignments are read.""" - init = tmp_path / "__init__.py" - init.write_text( - "import os\n" # non-Assign (Import) node -> skipped - "obj.attr = 'v'\n" # Assign with an Attribute target -> skipped - "ALIASES = ['legacy']\n" - ) - aliases, _ = _read_aliases(init, ast) - assert aliases == ["legacy"] - - -# --------------------------------------------------------------------------- -# Finder / loader edge branches -# --------------------------------------------------------------------------- - - -def test_alias_finder_returns_none_when_canonical_missing( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """If an alias points at a canonical *target* that doesn't exist, the - finder declines (returns None) and lets normal import machinery report - the missing module.""" - _patch_alias_map(monkeypatch, {"broken_alias": "definitely_not_a_real_component"}) - finder = _AliasFinder() - assert finder.find_spec("esphome.components.broken_alias", None) is None - - -def test_alias_finder_reraises_when_canonical_dependency_missing( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """If the canonical module exists but fails to import one of its own - dependencies, the finder surfaces that real error instead of masking it - as an unresolved alias (which would silently fall through to a confusing - 'no module named ').""" - _patch_alias_map(monkeypatch, {"some_alias": "real_canonical"}) - - def boom(name: str) -> None: - raise ModuleNotFoundError("No module named 'missing_dep'", name="missing_dep") - - monkeypatch.setattr("esphome.loader.importlib.import_module", boom) - finder = _AliasFinder() - with pytest.raises(ModuleNotFoundError, match="missing_dep"): - finder.find_spec("esphome.components.some_alias", None) - - -def test_install_alias_finder_is_idempotent() -> None: - """The finder is installed once at import; calling the installer again is - a no-op (no duplicate ``_AliasFinder`` on ``sys.meta_path``).""" - before = [e for e in sys.meta_path if isinstance(e, _AliasFinder)] - assert len(before) == 1 # installed at module import time - loader_mod._install_alias_finder() - after = [e for e in sys.meta_path if isinstance(e, _AliasFinder)] - assert len(after) == 1 - - -def test_get_component_alias_to_missing_canonical_returns_none( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """If an alias resolves to a canonical component that can't be loaded, - ``get_component`` returns None and caches no bogus manifest.""" - _patch_alias_map(monkeypatch, {"ghost_alias": "definitely_not_a_real_component"}) - loader_mod._COMPONENT_CACHE.pop("ghost_alias", None) - - assert get_component("ghost_alias") is None - assert "ghost_alias" not in loader_mod._COMPONENT_CACHE - - -# --------------------------------------------------------------------------- -# YAML pre-pass: empty-map fast path + validate_config integration -# --------------------------------------------------------------------------- - - -def test_resolve_component_aliases_noop_when_no_aliases_declared( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """When no component declares an alias, the pre-pass returns immediately - without inspecting or mutating the config.""" - from esphome.config import _resolve_component_aliases - - monkeypatch.setattr("esphome.loader.get_alias_metadata", dict) # empty map - config = {"esphome": {"name": "t"}, "rp2040": {"board": "x"}} - original = dict(config) - _resolve_component_aliases(config) - assert config == original - - -def _default_component_mock() -> Mock: - """A permissive component mock that validates any config (ALLOW_EXTRA).""" - return Mock( - auto_load=[], - is_platform_component=False, - is_platform=False, - multi_conf=False, - multi_conf_no_default=False, - dependencies=[], - conflicts_with=[], - config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA), - ) - - -@pytest.mark.usefixtures("setup_core") -def test_validate_config_renames_alias_key( - mock_get_component: Mock, monkeypatch: pytest.MonkeyPatch -) -> None: - """End-to-end: a legacy top-level key is renamed to its canonical name - before the rest of ``validate_config`` runs, and validation succeeds. - - A real ``esp32`` target platform is included so ``preload_core_config`` - is satisfied and validation runs to completion (the renamed canonical - key is loaded via the mocked, permissive component).""" - mock_get_component.side_effect = lambda name: _default_component_mock() - monkeypatch.setattr( - "esphome.loader.get_alias_metadata", - lambda: { - "legacyfoo": AliasMeta(canonical="newcomp", removal_version="2027.6.0") - }, - ) - CORE.data.pop("_component_aliases_warned", None) - - raw_config = { - "esphome": {"name": "test"}, - "esp32": {"board": "esp32dev"}, - "legacyfoo": {"opt": 1}, - } - result = esphome_config.validate_config(raw_config, {}) - - assert not result.errors, f"unexpected errors: {result.errors}" - assert "newcomp" in result - assert "legacyfoo" not in result - - -@pytest.mark.usefixtures("setup_core") -def test_validate_config_reports_alias_conflict_as_error( - mock_get_component: Mock, monkeypatch: pytest.MonkeyPatch -) -> None: - """If both the legacy and canonical keys are present, ``validate_config`` - surfaces the conflict as a config error (the ``vol.Invalid`` path).""" - mock_get_component.return_value = _default_component_mock() - monkeypatch.setattr( - "esphome.loader.get_alias_metadata", - lambda: {"legacyfoo": AliasMeta(canonical="newcomp", removal_version=None)}, - ) - CORE.data.pop("_component_aliases_warned", None) - - raw_config = { - "esphome": {"name": "test"}, - "newcomp": {"opt": 1}, - "legacyfoo": {"opt": 2}, - } - result = esphome_config.validate_config(raw_config, {}) - - assert result.errors - assert "Both 'legacyfoo:'" in str(result.errors) + handler = _Handler(level=logging.WARNING) + logger.addHandler(handler) + prev_level = logger.level + logger.setLevel(logging.WARNING) + try: + yield records + finally: + logger.removeHandler(handler) + logger.setLevel(prev_level) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 65bf4a583e..e575934870 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -94,7 +94,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_RP2040, + PLATFORM_RP2, Toolchain, ) from esphome.core import CORE, EsphomeError @@ -167,9 +167,9 @@ def setup_core( CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform} if tmp_path is not None: - CORE.config_path = str(tmp_path / f"{name}.yaml") + CORE.config_path = tmp_path / f"{name}.yaml" CORE.name = name - CORE.build_path = str(tmp_path / ".esphome" / "build" / name) + CORE.build_path = tmp_path / ".esphome" / "build" / name @pytest.fixture @@ -442,6 +442,46 @@ def test_redact_with_legacy_fallback__does_not_match_fragment_as_suffix( assert not any("legacy substring" in rec.message for rec in caplog.records) +@pytest.mark.parametrize("field", ["public_key", "peer_public_key"]) +def test_redact_with_legacy_fallback__skips_public_key_fields( + field: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Public keys are not secret; fields with a ``public`` name segment + must pass through unredacted and without the migration warning + (see issue #17718).""" + text = f"{field}: c29tZXB1YmxpY2tleQ==\n" + with caplog.at_level(logging.WARNING, logger="esphome.__main__"): + out = _redact_with_legacy_fallback(text) + assert out == text + assert not any("legacy substring" in rec.message for rec in caplog.records) + + +def test_redact_with_legacy_fallback__public_substitution_still_redacted( + caplog: pytest.LogCaptureFixture, +) -> None: + """Substitution keys are user-named with no schema behind them, so the + public-key exemption does not apply there; a ``public``-named substitution + keeps the conservative silent redaction.""" + text = "substitutions:\n public_key: something\nesphome:\n name: x\n" + with caplog.at_level(logging.WARNING, logger="esphome.__main__"): + out = _redact_with_legacy_fallback(text) + assert "public_key: \\033[8msomething\\033[28m" in out + assert not any("legacy substring" in rec.message for rec in caplog.records) + + +def test_redact_with_legacy_fallback__public_must_be_a_whole_segment( + caplog: pytest.LogCaptureFixture, +) -> None: + """The exemption matches ``public`` as an underscore-separated segment, + not a substring; an unrelated name like ``republic_key`` keeps the + conservative redaction.""" + with caplog.at_level(logging.WARNING, logger="esphome.__main__"): + out = _redact_with_legacy_fallback("republic_key: abc\n") + assert "republic_key: \\033[8mabc\\033[28m" in out + assert any("'republic_key'" in rec.message for rec in caplog.records) + + def test_redact_with_legacy_fallback__substitutions_redacted_without_warning( caplog: pytest.LogCaptureFixture, ) -> None: @@ -578,7 +618,7 @@ def test_command_config__no_defaults_skips_strip_default_ids( validated.user_config = {"sensor": [{"name": "x"}]} with patch( - "esphome.__main__.strip_default_ids", side_effect=AssertionError + "esphome.config.strip_default_ids", side_effect=AssertionError ) as mock_strip: result = command_config(args, validated) @@ -1226,7 +1266,7 @@ def test_choose_upload_log_host_no_defaults_with_rp2040_bootsel( mock_choose_prompt: Mock, ) -> None: """Test interactive mode shows RP2040 BOOTSEL option via picotool.""" - setup_core(platform=PLATFORM_RP2040) + setup_core(platform=PLATFORM_RP2) with ( patch( @@ -1249,7 +1289,7 @@ def test_choose_upload_log_host_no_defaults_with_rp2040_bootsel( @pytest.mark.usefixtures("mock_no_serial_ports") def test_choose_upload_log_host_rp2040_no_device_shows_bootsel_help() -> None: """Test BOOTSEL instructions shown when no RP2040 device found.""" - setup_core(platform=PLATFORM_RP2040) + setup_core(platform=PLATFORM_RP2) with ( patch( @@ -1271,7 +1311,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_ota( ) -> None: """Test BOOTSEL tip shown when only OTA options exist for RP2040.""" setup_core( - platform=PLATFORM_RP2040, + platform=PLATFORM_RP2, config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100", ) @@ -1300,7 +1340,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports( mock_choose_prompt: Mock, ) -> None: """Test BOOTSEL tip shown when serial ports exist but no BOOTSEL device.""" - setup_core(platform=PLATFORM_RP2040) + setup_core(platform=PLATFORM_RP2) mock_ports = [MockSerialPort("/dev/ttyACM0", "RP2040 Serial")] with ( @@ -1325,7 +1365,7 @@ def test_choose_upload_log_host_rp2040_permission_error_no_options( caplog: pytest.LogCaptureFixture, ) -> None: """Test permission warning shown when BOOTSEL device found but not accessible.""" - setup_core(platform=PLATFORM_RP2040) + setup_core(platform=PLATFORM_RP2) with ( patch( @@ -1355,7 +1395,7 @@ def test_choose_upload_log_host_rp2040_permission_error_with_ota( ) -> None: """Test permission warning shown with OTA fallback available.""" setup_core( - platform=PLATFORM_RP2040, + platform=PLATFORM_RP2, config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100", ) @@ -1412,7 +1452,7 @@ def test_choose_upload_log_host_rp2040_serial_and_bootsel( mock_choose_prompt: Mock, ) -> None: """Test both serial ports and BOOTSEL option shown for RP2040.""" - setup_core(platform=PLATFORM_RP2040) + setup_core(platform=PLATFORM_RP2) mock_ports = [MockSerialPort("/dev/ttyACM0", "RP2040 Serial")] with ( @@ -1665,7 +1705,7 @@ def test_upload_using_esptool_with_file_path( @pytest.mark.parametrize( "platform,device", [ - (PLATFORM_RP2040, "/dev/ttyACM0"), + (PLATFORM_RP2, "/dev/ttyACM0"), (PLATFORM_BK72XX, "/dev/ttyUSB0"), # LibreTiny platform ], ) @@ -1720,7 +1760,7 @@ def test_upload_using_platformio_creates_signed_bin_for_rp2040( tmp_path: Path, ) -> None: """Test that upload_using_platformio creates firmware.bin.signed for RP2040.""" - setup_core(platform=PLATFORM_RP2040) + setup_core(platform=PLATFORM_RP2) build_dir = tmp_path / "build" build_dir.mkdir() @@ -1756,6 +1796,53 @@ def test_upload_using_platformio_skips_signed_bin_for_non_rp2040( assert result == 0 +def test_upload_using_platformio_skips_signed_bin_when_already_present( + tmp_path: Path, +) -> None: + """The signed-bin copy is idempotent: if ``firmware.bin.signed`` already + exists on the RP2 build path, the upload step must not overwrite it + (and must not fail when the unsigned ``firmware.bin`` is absent).""" + setup_core(platform=PLATFORM_RP2) + + build_dir = tmp_path / "build" + build_dir.mkdir() + # Pre-existing signed bin with distinct content — must be preserved. + signed_bin = build_dir / "firmware.bin.signed" + signed_bin.write_bytes(b"already signed") + # No unsigned firmware.bin on disk — the `is_file()` guard must hold. + firmware_elf = build_dir / "firmware.elf" + firmware_elf.write_bytes(b"elf") + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(firmware_elf) + + with ( + patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata), + patch("esphome.platformio.toolchain.run_platformio_cli_run", return_value=0), + ): + result = upload_using_platformio({}, "/dev/ttyACM0") + + assert result == 0 + # Pre-existing signed bin is untouched. + assert signed_bin.read_bytes() == b"already signed" + + +def test_upload_using_platformio_handles_port_none(tmp_path: Path) -> None: + """The upload step must work without a serial port (PlatformIO picks the + target itself); the ``--upload-port`` flag is only appended when a port + is provided.""" + setup_core(platform=PLATFORM_ESP32) + + with patch( + "esphome.platformio.toolchain.run_platformio_cli_run", return_value=0 + ) as mock_run: + result = upload_using_platformio({}, None) + + assert result == 0 + args = mock_run.call_args.args + assert "--upload-port" not in args + + def test_upload_program_serial_upload_failed( mock_upload_using_esptool: Mock, mock_get_port_type: Mock, @@ -1783,7 +1870,7 @@ def test_upload_program_bootsel( mock_get_port_type: Mock, ) -> None: """Test upload_program with BOOTSEL for RP2040.""" - setup_core(platform=PLATFORM_RP2040) + setup_core(platform=PLATFORM_RP2) mock_get_port_type.return_value = "BOOTSEL" mock_upload_using_picotool.return_value = 0 @@ -1804,7 +1891,7 @@ def test_upload_program_bootsel_failed( mock_get_port_type: Mock, ) -> None: """Test upload_program when BOOTSEL upload fails.""" - setup_core(platform=PLATFORM_RP2040) + setup_core(platform=PLATFORM_RP2) mock_get_port_type.return_value = "BOOTSEL" mock_upload_using_picotool.return_value = 1 @@ -1821,7 +1908,7 @@ def test_upload_program_bootsel_failed( def test_upload_using_picotool_success(tmp_path: Path) -> None: """Test upload_using_picotool succeeds.""" - setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + setup_core(platform=PLATFORM_RP2, tmp_path=tmp_path) build_dir = tmp_path / "build" build_dir.mkdir() @@ -1858,7 +1945,7 @@ def test_upload_using_picotool_success(tmp_path: Path) -> None: def test_upload_using_picotool_no_elf(tmp_path: Path) -> None: """Test upload_using_picotool when ELF file is missing.""" - setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + setup_core(platform=PLATFORM_RP2, tmp_path=tmp_path) build_dir = tmp_path / "build" build_dir.mkdir() @@ -1876,7 +1963,7 @@ def test_upload_using_picotool_no_elf(tmp_path: Path) -> None: def test_upload_using_picotool_not_found(tmp_path: Path) -> None: """Test upload_using_picotool when picotool binary not found.""" - setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + setup_core(platform=PLATFORM_RP2, tmp_path=tmp_path) build_dir = tmp_path / "build" build_dir.mkdir() @@ -1896,7 +1983,7 @@ def test_upload_using_picotool_not_found(tmp_path: Path) -> None: def test_upload_using_picotool_permission_error(tmp_path: Path) -> None: """Test upload_using_picotool shows helpful message on permission error.""" - setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + setup_core(platform=PLATFORM_RP2, tmp_path=tmp_path) build_dir = tmp_path / "build" build_dir.mkdir() @@ -5363,6 +5450,43 @@ def _setup_build_info_test( return build_info_path, firmware_path +def test_compile_program_esp8266_runs_rosetta_check(tmp_path: Path) -> None: + """Test that compile_program runs the Rosetta preflight for ESP8266 targets.""" + setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test_device") + + config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + with ( + patch( + "esphome.components.esp8266.check_rosetta", + side_effect=EsphomeError("Rosetta 2 is not installed"), + ) as mock_check, + pytest.raises(EsphomeError, match="Rosetta 2 is not installed"), + ): + compile_program(args, config) + + mock_check.assert_called_once() + + +def test_compile_program_skips_rosetta_check_on_other_platforms( + tmp_path: Path, + mock_compile_build_info_run_compile: Mock, + mock_compile_build_info_get_idedata: Mock, +) -> None: + """Test that the Rosetta preflight does not run for non-ESP8266 targets.""" + _setup_build_info_test(tmp_path, firmware_first=True) + + config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + with patch("esphome.components.esp8266.check_rosetta") as mock_check: + result = compile_program(args, config) + + assert result == 0 + mock_check.assert_not_called() + + def test_compile_program_emits_build_info_when_firmware_rebuilt( tmp_path: Path, caplog: pytest.LogCaptureFixture, @@ -6077,7 +6201,7 @@ def test_run_esphome_bundle_detection(tmp_path: Path) -> None: "esphome.bundle.prepare_bundle_for_compile", return_value=extracted_yaml, ) as mock_prepare, - patch("esphome.__main__.read_config", return_value=None), + patch("esphome.config.read_config", return_value=None), ): result = run_esphome(["esphome", "compile", str(bundle_path)]) @@ -6095,7 +6219,7 @@ def test_run_esphome_non_bundle_skips_extraction(tmp_path: Path) -> None: with ( patch("esphome.bundle.is_bundle_path", return_value=False) as mock_is_bundle, patch("esphome.bundle.prepare_bundle_for_compile") as mock_prepare, - patch("esphome.__main__.read_config", return_value=None), + patch("esphome.config.read_config", return_value=None), ): result = run_esphome(["esphome", "compile", str(yaml_file)]) @@ -6123,7 +6247,7 @@ def test_run_esphome_skip_external_update_per_command( yaml_file = tmp_path / "device.yaml" yaml_file.write_text("esphome:\n name: test\n") - with patch("esphome.__main__.read_config", return_value=None) as mock_read: + with patch("esphome.config.read_config", return_value=None) as mock_read: run_esphome(["esphome", command, str(yaml_file)]) mock_read.assert_called_once() @@ -6281,6 +6405,23 @@ def test_parse_args_logs_states() -> None: assert args.states is True +def test_parse_args_argcomplete_only_runs_when_completing() -> None: + """Only import and invoke argcomplete when _ARGCOMPLETE is set. + + The shell-completion machinery sets _ARGCOMPLETE when it invokes the + CLI; a normal invocation must skip the import entirely so every + esphome subprocess (e.g. parallel dashboard uploads) avoids paying + for it. + """ + fake_argcomplete = MagicMock() + with ( + patch.dict(os.environ, {"_ARGCOMPLETE": "1"}), + patch.dict(sys.modules, {"argcomplete": fake_argcomplete}), + ): + parse_args(["esphome", "version"]) + fake_argcomplete.autocomplete.assert_called_once() + + def test_should_subscribe_states_default() -> None: """Test that states are shown by default when nothing is set.""" from esphome.__main__ import _should_subscribe_states @@ -6411,7 +6552,7 @@ def test_command_run_rp2040_bootsel_redetects_serial_port() -> None: picks up the newly enumerated serial port before showing logs.""" setup_core( config={"logger": {}, CONF_API: {}, CONF_MDNS: {CONF_DISABLED: False}}, - platform=PLATFORM_RP2040, + platform=PLATFORM_RP2, ) args = MockArgs() diff --git a/tests/unit_tests/test_nrf52_framework.py b/tests/unit_tests/test_nrf52_framework.py index 2b3d1f6db8..0a6bddc280 100644 --- a/tests/unit_tests/test_nrf52_framework.py +++ b/tests/unit_tests/test_nrf52_framework.py @@ -1,20 +1,30 @@ """Tests for esphome.components.nrf52.framework helpers.""" +import hashlib +import os from pathlib import Path +import sys from types import SimpleNamespace from unittest.mock import patch import pytest from esphome.components.nrf52.framework import ( - _TOOLCHAIN_VERSION, + _PLATFORMIO_PENV_REQUIREMENTS, + _REQUIREMENTS, + TOOLCHAIN_VERSION, + _get_penv_site_packages, + _get_platformio_penv_path, _get_toolchain_platform_info, check_and_install, + get_build_env, get_sdk_nrf_tools_path, + setup_platformio_python_env, ) from esphome.config_validation import Version from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION from esphome.core import CORE, EsphomeError +from esphome.framework_helpers import get_python_env_executable_path @pytest.fixture(autouse=True) @@ -71,7 +81,7 @@ def nrf52_dirs(setup_core: Path) -> SimpleNamespace: tools = get_sdk_nrf_tools_path() python_env = tools / "penvs" / f"v{_TEST_SDK_VERSION}" framework = tools / "frameworks" / f"v{_TEST_SDK_VERSION}" - toolchain_dir = tools / "toolchains" / _TOOLCHAIN_VERSION + toolchain_dir = tools / "toolchains" / TOOLCHAIN_VERSION for d in (python_env, framework, toolchain_dir): d.mkdir(parents=True, exist_ok=True) zephyr_scripts = framework / "zephyr" / "scripts" @@ -113,6 +123,12 @@ def mock_nrf52_ops(): # --------------------------------------------------------------------------- +def _mark_venv_ready(python_env: Path) -> None: + """Write the venv sentinel with the current requirements hash.""" + requirements_hash = hashlib.sha256(_REQUIREMENTS.read_bytes()).hexdigest() + (python_env / ".ready").write_text(requirements_hash, encoding="utf-8") + + class TestCheckAndInstall: def test_all_installed_skips_all_steps( self, @@ -120,7 +136,7 @@ class TestCheckAndInstall: mock_nrf52_ops: SimpleNamespace, ) -> None: """All three sentinels present → nothing downloaded or compiled.""" - (nrf52_dirs.python_env / ".ready").touch() + _mark_venv_ready(nrf52_dirs.python_env) (nrf52_dirs.python_env / ".zephyr_reqs_ready").touch() (nrf52_dirs.framework / ".ready").touch() (nrf52_dirs.toolchain / ".ready").touch() @@ -157,7 +173,7 @@ class TestCheckAndInstall: mock_nrf52_ops: SimpleNamespace, ) -> None: """Venv ready but framework missing → skip venv creation, run SDK init+update.""" - (nrf52_dirs.python_env / ".ready").touch() + _mark_venv_ready(nrf52_dirs.python_env) check_and_install() @@ -173,7 +189,7 @@ class TestCheckAndInstall: mock_nrf52_ops: SimpleNamespace, ) -> None: """Venv and framework ready → only toolchain downloaded and extracted.""" - (nrf52_dirs.python_env / ".ready").touch() + _mark_venv_ready(nrf52_dirs.python_env) (nrf52_dirs.python_env / ".zephyr_reqs_ready").touch() (nrf52_dirs.framework / ".ready").touch() @@ -185,6 +201,24 @@ class TestCheckAndInstall: assert mock_nrf52_ops.download_from_mirrors.call_count == 2 assert mock_nrf52_ops.archive_extract_all.call_count == 2 + def test_framework_clone_is_shallow( + self, + nrf52_dirs: SimpleNamespace, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """Both the manifest repository and every project are fetched at depth 1.""" + _mark_venv_ready(nrf52_dirs.python_env) + + check_and_install() + + init_cmd, update_cmd = ( + call.args[0] for call in mock_nrf52_ops.run_command_ok.call_args_list[:2] + ) + assert "init" in init_cmd + assert "-o=--depth=1" in init_cmd + assert "update" in update_cmd + assert "--fetch-opt=--depth=1" in update_cmd + def test_requirements_install_failure_raises( self, nrf52_dirs: SimpleNamespace, @@ -202,7 +236,7 @@ class TestCheckAndInstall: mock_nrf52_ops: SimpleNamespace, ) -> None: """Failing west init raises EsphomeError.""" - (nrf52_dirs.python_env / ".ready").touch() + _mark_venv_ready(nrf52_dirs.python_env) mock_nrf52_ops.run_command_ok.return_value = False with pytest.raises(EsphomeError, match="Can't initialize"): @@ -214,7 +248,7 @@ class TestCheckAndInstall: mock_nrf52_ops: SimpleNamespace, ) -> None: """Failing west update raises EsphomeError.""" - (nrf52_dirs.python_env / ".ready").touch() + _mark_venv_ready(nrf52_dirs.python_env) # init succeeds, update fails mock_nrf52_ops.run_command_ok.side_effect = [True, False] @@ -227,7 +261,7 @@ class TestCheckAndInstall: mock_nrf52_ops: SimpleNamespace, ) -> None: """download_from_mirrors receives VERSION + platform triple from _get_toolchain_platform_info.""" - (nrf52_dirs.python_env / ".ready").touch() + _mark_venv_ready(nrf52_dirs.python_env) (nrf52_dirs.framework / ".ready").touch() with patch( @@ -238,12 +272,224 @@ class TestCheckAndInstall: args, _ = mock_nrf52_ops.download_from_mirrors.call_args substitutions = args[1] - assert substitutions["VERSION"] == _TOOLCHAIN_VERSION + assert substitutions["VERSION"] == TOOLCHAIN_VERSION assert substitutions["sysname"] == "linux" assert substitutions["machine"] == "x86_64" assert substitutions["extension"] == "tar.xz" +# --------------------------------------------------------------------------- +# setup_platformio_python_env tests +# --------------------------------------------------------------------------- + + +def _platformio_requirements_hash() -> str: + return hashlib.sha256( + _REQUIREMENTS.read_bytes() + + "\n".join(_PLATFORMIO_PENV_REQUIREMENTS).encode() + + f"python{sys.version_info.major}.{sys.version_info.minor}".encode() + ).hexdigest() + + +@pytest.fixture +def platformio_penv_dir() -> Path: + """Pre-create the PlatformIO penv dir so sentinel writes succeed. + + create_venv is mocked in these tests, so the directory it would have + created must exist for ``sentinel.write_text`` to work. + """ + penv_path = _get_platformio_penv_path() + penv_path.mkdir(parents=True, exist_ok=True) + return penv_path + + +class TestSetupPlatformioPythonEnv: + def test_fresh_install_creates_venv_and_sets_env( + self, + platformio_penv_dir: Path, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """No sentinel → venv created, requirements installed, env exported.""" + with patch.dict(os.environ): + os.environ.pop("PYTHONPATH", None) + + setup_platformio_python_env() + + mock_nrf52_ops.rmdir.assert_called_once() + mock_nrf52_ops.create_venv.assert_called_once_with( + platformio_penv_dir, msg="PlatformIO toolchain" + ) + mock_nrf52_ops.run_command_ok.assert_called_once() + cmd = mock_nrf52_ops.run_command_ok.call_args[0][0] + assert cmd[1:4] == ["-m", "pip", "install"] + assert "-r" in cmd + assert str(_REQUIREMENTS) in cmd + for requirement in _PLATFORMIO_PENV_REQUIREMENTS: + assert requirement in cmd + sentinel = platformio_penv_dir / ".ready" + assert sentinel.read_text(encoding="utf-8") == ( + _platformio_requirements_hash() + ) + + assert os.environ["VIRTUAL_ENV"] == str(platformio_penv_dir) + site_packages = str(_get_penv_site_packages(platformio_penv_dir)) + assert os.environ["PYTHONPATH"] == site_packages + bin_dir = str( + get_python_env_executable_path(platformio_penv_dir, "python").parent + ) + assert os.environ["PATH"].split(os.pathsep)[0] == bin_dir + + def test_ready_sentinel_skips_install_but_sets_env( + self, + platformio_penv_dir: Path, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """Current sentinel → no install work, env vars still exported.""" + (platformio_penv_dir / ".ready").write_text( + _platformio_requirements_hash(), encoding="utf-8" + ) + + with patch.dict(os.environ): + setup_platformio_python_env() + + mock_nrf52_ops.rmdir.assert_not_called() + mock_nrf52_ops.create_venv.assert_not_called() + mock_nrf52_ops.run_command_ok.assert_not_called() + assert os.environ["VIRTUAL_ENV"] == str(platformio_penv_dir) + + def test_stale_sentinel_reinstalls( + self, + platformio_penv_dir: Path, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """A sentinel from different requirements → venv rebuilt from scratch.""" + sentinel = platformio_penv_dir / ".ready" + sentinel.write_text("stale-hash", encoding="utf-8") + + with patch.dict(os.environ): + setup_platformio_python_env() + + mock_nrf52_ops.rmdir.assert_called_once() + mock_nrf52_ops.create_venv.assert_called_once() + mock_nrf52_ops.run_command_ok.assert_called_once() + assert sentinel.read_text(encoding="utf-8") == _platformio_requirements_hash() + + def test_install_failure_raises( + self, + platformio_penv_dir: Path, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """Failing pip install raises EsphomeError and writes no sentinel.""" + mock_nrf52_ops.run_command_ok.return_value = False + + with ( + patch.dict(os.environ), + pytest.raises( + EsphomeError, match="Install requirements for PlatformIO toolchain" + ), + ): + setup_platformio_python_env() + + assert not (platformio_penv_dir / ".ready").exists() + + def test_repeated_calls_do_not_duplicate_env_entries( + self, + platformio_penv_dir: Path, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """Compile then upload in one process must not grow PYTHONPATH/PATH.""" + (platformio_penv_dir / ".ready").write_text( + _platformio_requirements_hash(), encoding="utf-8" + ) + site_packages = str(_get_penv_site_packages(platformio_penv_dir)) + bin_dir = str( + get_python_env_executable_path(platformio_penv_dir, "python").parent + ) + + with patch.dict(os.environ): + setup_platformio_python_env() + setup_platformio_python_env() + + assert os.environ["PYTHONPATH"].split(os.pathsep).count(site_packages) == 1 + assert os.environ["PATH"].split(os.pathsep).count(bin_dir) == 1 + + def test_existing_pythonpath_preserved( + self, + platformio_penv_dir: Path, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """A pre-existing PYTHONPATH keeps its entries after the venv entry.""" + (platformio_penv_dir / ".ready").write_text( + _platformio_requirements_hash(), encoding="utf-8" + ) + site_packages = str(_get_penv_site_packages(platformio_penv_dir)) + + with patch.dict(os.environ, {"PYTHONPATH": "/existing/path"}): + setup_platformio_python_env() + + assert os.environ["PYTHONPATH"] == os.pathsep.join( + [site_packages, "/existing/path"] + ) + + +@pytest.mark.parametrize( + ("os_name", "expected_parts"), + [ + ( + "posix", + ( + "lib", + f"python{sys.version_info.major}.{sys.version_info.minor}", + "site-packages", + ), + ), + ("nt", ("Lib", "site-packages")), + ], +) +def test_get_penv_site_packages( + tmp_path: Path, os_name: str, expected_parts: tuple[str, ...] +) -> None: + penv_path = tmp_path / "penv" + with patch("os.name", os_name): + assert _get_penv_site_packages(penv_path) == penv_path.joinpath(*expected_parts) + + +# --------------------------------------------------------------------------- +# get_build_env tests +# --------------------------------------------------------------------------- + + +def test_get_build_env( + nrf52_dirs: SimpleNamespace, monkeypatch: pytest.MonkeyPatch +) -> None: + """get_build_env exposes ZEPHYR_SDK_INSTALL_DIR pointing at the toolchain root. + + ZEPHYR_SDK_INSTALL_DIR is the variable Zephyr's FindZephyr-sdk.cmake + explicitly consumes (from the environment) and uses as a find_package + HINT. The old Zephyr-sdk_DIR environment hint proved unreliable in + containerized non-root builds and was removed. + """ + monkeypatch.setenv("SOME_PREEXISTING_VAR", "kept") + + env = get_build_env() + + tools = get_sdk_nrf_tools_path() + venv_bin_dir = get_python_env_executable_path( + tools / "penvs" / f"v{_TEST_SDK_VERSION}", "python" + ).parent + assert env["PATH"].startswith(str(venv_bin_dir) + os.pathsep) + assert env["ZEPHYR_BASE"] == str( + tools / "frameworks" / f"v{_TEST_SDK_VERSION}" / "zephyr" + ) + # Toolchain root, not the cmake/ subdir + assert env["ZEPHYR_SDK_INSTALL_DIR"] == str( + tools / "toolchains" / TOOLCHAIN_VERSION + ) + assert "Zephyr-sdk_DIR" not in env + # The rest of the process environment is inherited + assert env["SOME_PREEXISTING_VAR"] == "kept" + + # --------------------------------------------------------------------------- # get_sdk_nrf_tools_path tests # --------------------------------------------------------------------------- diff --git a/tests/unit_tests/test_nrf52_upload.py b/tests/unit_tests/test_nrf52_upload.py index 4945e4c705..9091b429f6 100644 --- a/tests/unit_tests/test_nrf52_upload.py +++ b/tests/unit_tests/test_nrf52_upload.py @@ -146,6 +146,72 @@ class TestUploadProgramPyocd: upload_program(config={}, args=None, host="PYOCD") +# --------------------------------------------------------------------------- +# PlatformIO toolchain paths +# --------------------------------------------------------------------------- + + +class TestRunCompilePlatformio: + def test_prepares_python_env_and_delegates_to_platformio( + self, setup_core: Path, tmp_path: Path + ) -> None: + """The PlatformIO toolchain prepares the env, then returns False so PlatformIO builds.""" + from esphome.components.nrf52 import run_compile + + _setup_nrf52_core(toolchain=Toolchain.PLATFORMIO, build_path=tmp_path / "build") + + with patch( + "esphome.components.nrf52.setup_platformio_python_env" + ) as mock_setup: + assert run_compile(args=None, config={}) is False + + mock_setup.assert_called_once_with() + + +class TestUploadProgramSerialPlatformio: + def _upload(self, host: str, tmp_path: Path, run_result: int) -> tuple: + from esphome.components.nrf52 import upload_program + from esphome.upload_targets import PortType + + _setup_nrf52_core(toolchain=Toolchain.PLATFORMIO, build_path=tmp_path / "build") + CORE.config_path = tmp_path / "test.yaml" + + with ( + patch("esphome.upload_targets.get_port_type", return_value=PortType.SERIAL), + patch("esphome.__main__.check_permissions"), + patch("esphome.components.nrf52.setup_platformio_python_env") as mock_setup, + patch( + "esphome.platformio.toolchain.run_platformio_cli_run", + return_value=run_result, + ) as mock_run, + ): + result = upload_program(config={}, args=None, host=host) + return result, mock_setup, mock_run + + def test_serial_upload_prepares_env_and_runs_platformio( + self, setup_core: Path, tmp_path: Path + ) -> None: + """Serial upload with the PlatformIO toolchain runs pio with -t upload.""" + host = "/dev/ttyACM0" + result, mock_setup, mock_run = self._upload(host, tmp_path, run_result=0) + + assert result is True + mock_setup.assert_called_once_with() + mock_run.assert_called_once() + run_args = mock_run.call_args[0] + assert "-t" in run_args + assert "upload" in run_args + assert "--upload-port" in run_args + assert host in run_args + + def test_serial_upload_failure_raises( + self, setup_core: Path, tmp_path: Path + ) -> None: + """A non-zero PlatformIO result must raise EsphomeError.""" + with pytest.raises(EsphomeError, match="Upload failed"): + self._upload("/dev/ttyACM0", tmp_path, run_result=1) + + # --------------------------------------------------------------------------- # Serial DFU upload path # --------------------------------------------------------------------------- diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index 03360eab37..c0a0c678db 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -133,18 +133,8 @@ def test_resolve_registry_version_raises_without_pkg_file(monkeypatch): _resolve_registry_version("owner", "pkg", set()) -def _patch_download_with_manifests(monkeypatch, tmp_path, manifests, *, properties=()): - """Fake ConvertedLibrary.download to materialize canned manifests on disk.""" - - def fake_download(self, force=False, salt="", namespace=""): - self.path = tmp_path / self.get_sanitized_name().replace("/", "__") - self.path.mkdir(parents=True, exist_ok=True) - if self.name in properties: - (self.path / "library.properties").write_text(manifests[self.name]) - else: - (self.path / "library.json").write_text(json.dumps(manifests[self.name])) - - monkeypatch.setattr(ConvertedLibrary, "download", fake_download) +def _patch_registry_resolve(monkeypatch: pytest.MonkeyPatch) -> None: + """Stub the registry lookup so tests never touch the network.""" monkeypatch.setattr( lib, "_resolve_registry_version", @@ -157,6 +147,21 @@ def _patch_download_with_manifests(monkeypatch, tmp_path, manifests, *, properti ) +def _patch_download_with_manifests(monkeypatch, tmp_path, manifests, *, properties=()): + """Fake ConvertedLibrary.download to materialize canned manifests on disk.""" + + def fake_download(self, force=False, salt="", namespace=""): + self.path = tmp_path / self.get_require_name() + self.path.mkdir(parents=True, exist_ok=True) + if self.name in properties: + (self.path / "library.properties").write_text(manifests[self.name]) + else: + (self.path / "library.json").write_text(json.dumps(manifests[self.name])) + + monkeypatch.setattr(ConvertedLibrary, "download", fake_download) + _patch_registry_resolve(monkeypatch) + + def test_convert_libraries_parses_library_properties(tmp_path, monkeypatch): # A manifest provided as library.properties (Arduino style) instead of # library.json must still be parsed and converted. @@ -212,6 +217,106 @@ def test_convert_libraries_handles_unparsable_dependency_version(tmp_path, monke assert [d.name for d in top[0].dependencies] == ["C"] +def _patch_download_without_manifest( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, *, manifest_on_force: bool +) -> list[bool]: + """Fake ConvertedLibrary.download that leaves the manifest missing. + + When ``manifest_on_force`` is set, a forced re-download writes a valid + library.json, simulating a broken cache entry that heals on retry. + Returns the list of ``force`` values download was called with. + """ + calls: list[bool] = [] + + def fake_download( + self: ConvertedLibrary, force: bool = False, salt: str = "", namespace: str = "" + ) -> None: + calls.append(force) + self.path = tmp_path / self.get_require_name() + self.path.mkdir(parents=True, exist_ok=True) + if force and manifest_on_force: + (self.path / "library.json").write_text(json.dumps({"name": "A"})) + + monkeypatch.setattr(ConvertedLibrary, "download", fake_download) + _patch_registry_resolve(monkeypatch) + return calls + + +def test_convert_libraries_redownloads_when_manifest_missing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + # A cached copy without any manifest (e.g. an interrupted clone or + # extraction) triggers exactly one forced re-download and then succeeds. + calls = _patch_download_without_manifest( + monkeypatch, tmp_path, manifest_on_force=True + ) + + top = convert_libraries([Library("esphome/A", "1.0.0", None)], _backend()) + + assert calls == [False, True] + assert top[0].data["name"] == "A" + + +def test_convert_libraries_raises_when_manifest_missing_after_retry( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + # If the forced re-download still yields no manifest, the error is raised + # after exactly one retry (no retry loop). The error must name the cache + # directory so users can find the broken entry instead of guessing where + # the library was unpacked. + calls = _patch_download_without_manifest( + monkeypatch, tmp_path, manifest_on_force=False + ) + + with pytest.raises(RuntimeError, match="Invalid PIO library") as excinfo: + convert_libraries([Library("esphome/A", "1.0.0", None)], _backend()) + + assert calls == [False, True] + assert str(tmp_path / "esphome__A") in str(excinfo.value) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (None, None), + ("", None), + ("http://[::1", None), # malformed IPv6 makes urlsplit raise ValueError + ("foo/bar", None), + ("file:///no/host", None), + ("https://github.com/x/y", "https://github.com/x/y"), + ], +) +def test_url_or_none(value: str | None, expected: str | None) -> None: + assert lib._url_or_none(value) == expected + + +def test_convert_libraries_url_in_name_resolves_as_git( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + # add_library("https://github.com/x/y", None) puts a git URL in the name + # position; it must resolve as a git source and never hit the registry. + _patch_download_with_manifests( + monkeypatch, tmp_path, {"pstolarz/OneWireNg": {"name": "OneWireNg"}} + ) + + def fail_registry(owner: str, pkgname: str, requirements: set[str]) -> None: + raise AssertionError(f"registry consulted for {owner}/{pkgname}") + + # After the helper so this stub wins over the helper's benign one + monkeypatch.setattr(lib, "_resolve_registry_version", fail_registry) + + top = convert_libraries( + [Library("https://github.com/pstolarz/OneWireNg", None, None)], _backend() + ) + + assert [c.name for c in top] == ["pstolarz/OneWireNg"] + assert top[0].data["name"] == "OneWireNg" + source = top[0].source + assert isinstance(source, GitSource) + assert source.url == "https://github.com/pstolarz/OneWireNg" + assert source.ref is None + + def test_convert_libraries_skips_incompatible_dependency(tmp_path, monkeypatch): # A dependency that declares an incompatible platform is skipped (the # top-level library still builds). diff --git a/tests/unit_tests/test_platformio_toolchain.py b/tests/unit_tests/test_platformio_toolchain.py index 568b43a259..723646fbd6 100644 --- a/tests/unit_tests/test_platformio_toolchain.py +++ b/tests/unit_tests/test_platformio_toolchain.py @@ -2,12 +2,14 @@ # pylint: disable=protected-access +from collections.abc import Generator from contextlib import contextmanager from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import json import os from pathlib import Path import shutil +import sys import threading from types import SimpleNamespace from unittest.mock import MagicMock, Mock, call, patch @@ -320,6 +322,149 @@ def test_run_platformio_cli_sets_environment_variables( assert "arg" in args +def test_ccache_env_enabled_by_default(setup_core: Path) -> None: + """Ccache is enabled when the binary is on PATH and no override is set.""" + CORE.build_path = setup_core / "build" / "test" + + with ( + patch.dict(os.environ, {}, clear=True), + patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"), + ): + env = toolchain._ccache_env() + + assert env["ESPHOME_CCACHE_ENABLE"] == "1" + assert env["CCACHE_BASEDIR"] == str((setup_core / "build" / "test").resolve()) + assert env["CCACHE_DIR"].endswith("platformio-ccache") + assert env["CCACHE_NOHASHDIR"] == "true" + # Nothing may leak into os.environ: a later ESP-IDF build in the same + # process would otherwise skip its own ccache defaults. + assert "CCACHE_BASEDIR" not in os.environ + assert "ESPHOME_CCACHE_ENABLE" not in os.environ + + +def test_ccache_env_disabled_without_binary(setup_core: Path) -> None: + """Ccache stays off when the binary is not on PATH.""" + CORE.build_path = setup_core / "build" / "test" + + with ( + patch.dict(os.environ, {}, clear=True), + patch.object(toolchain.shutil, "which", return_value=None), + ): + env = toolchain._ccache_env() + + assert env == {"ESPHOME_CCACHE_ENABLE": "0"} + + +def test_ccache_env_opt_out(setup_core: Path) -> None: + """ESPHOME_CCACHE_ENABLE=0 disables ccache even with the binary present.""" + CORE.build_path = setup_core / "build" / "test" + + with ( + patch.dict(os.environ, {"ESPHOME_CCACHE_ENABLE": "0"}, clear=True), + patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"), + ): + env = toolchain._ccache_env() + + assert env == {"ESPHOME_CCACHE_ENABLE": "0"} + + +def test_ccache_env_normalizes_enable_value(setup_core: Path) -> None: + """A truthy override value is normalized to "1" for the build scripts.""" + CORE.build_path = setup_core / "build" / "test" + + with ( + patch.dict(os.environ, {"ESPHOME_CCACHE_ENABLE": "yes"}, clear=True), + patch.object(toolchain.shutil, "which", return_value=None), + ): + env = toolchain._ccache_env() + + assert env["ESPHOME_CCACHE_ENABLE"] == "1" + + +def test_ccache_env_respects_user_values_and_refreshes_basedir( + setup_core: Path, +) -> None: + """User CCACHE_* values win, but CCACHE_BASEDIR follows the build dir.""" + user_env = { + "CCACHE_DIR": "/custom/cache", + "CCACHE_BASEDIR": "/stale/other-device", + } + CORE.build_path = setup_core / "build" / "test" + + with ( + patch.dict(os.environ, user_env, clear=True), + patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"), + ): + env = toolchain._ccache_env() + + # CCACHE_DIR is not returned, so the user's os.environ value applies in + # the subprocess; CCACHE_BASEDIR is always refreshed to the build dir. + assert "CCACHE_DIR" not in env + assert env["CCACHE_BASEDIR"] == str((setup_core / "build" / "test").resolve()) + + +def test_run_platformio_cli_passes_ccache_env_to_subprocess_only( + setup_core: Path, mock_run_external_process: Mock +) -> None: + """The ccache settings reach the subprocess env without touching os.environ.""" + CORE.build_path = str(setup_core / "build" / "test") + + with ( + patch.dict(os.environ, {}, clear=False), + patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"), + ): + os.environ.pop("ESPHOME_CCACHE_ENABLE", None) + mock_run_external_process.return_value = 0 + toolchain.run_platformio_cli("test", "arg") + + env = mock_run_external_process.call_args[1]["env"] + assert env["ESPHOME_CCACHE_ENABLE"] == "1" + assert env["CCACHE_BASEDIR"] == str((setup_core / "build" / "test").resolve()) + assert "ESPHOME_CCACHE_ENABLE" not in os.environ + assert "CCACHE_BASEDIR" not in os.environ + + +def test_ccache_env_requires_build_path(setup_core: Path) -> None: + """Enabling ccache without a build path fails loudly.""" + CORE.build_path = None + + with ( + patch.dict(os.environ, {}, clear=True), + patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"), + pytest.raises(ValueError, match="CORE.build_path must be set"), + ): + toolchain._ccache_env() + + +def test_run_platformio_cli_merges_caller_env( + setup_core: Path, mock_run_external_process: Mock +) -> None: + """A caller-supplied env is the base and gains the ccache settings.""" + CORE.build_path = str(setup_core / "build" / "test") + + with patch.object(toolchain.shutil, "which", return_value="/usr/bin/ccache"): + mock_run_external_process.return_value = 0 + toolchain.run_platformio_cli( + "test", env={"CUSTOM_VAR": "1", "ESPHOME_CCACHE_ENABLE": "0"} + ) + + env = mock_run_external_process.call_args[1]["env"] + assert env["CUSTOM_VAR"] == "1" + # The normalized enable flag still lands in the subprocess env. + assert "ESPHOME_CCACHE_ENABLE" in env + + +def test_copy_ccache_script(setup_core: Path) -> None: + """The shared ccache pre-script is copied into the build dir.""" + CORE.build_path = setup_core / "build" / "test" + + toolchain.copy_ccache_script() + + dest = setup_core / "build" / "test" / "ccache.py" + source = Path(toolchain.__file__).parent / "ccache.py.script" + assert dest.read_text() == source.read_text() + + @pytest.mark.parametrize( ("platform", "input_path", "expected"), [ @@ -373,7 +518,10 @@ def test_run_platformio_cli_strips_win_long_path_prefix( ) with ( - patch.dict(os.environ, {}, clear=False), + # Pin ccache off: patching sys.platform to win32 (sys is a singleton, + # so the stdlib sees it too) would send shutil.which down the Windows + # code path, which crashes on a POSIX host. + patch.dict(os.environ, {"ESPHOME_CCACHE_ENABLE": "0"}, clear=False), patch("esphome.platformio.toolchain.sys.platform", "win32"), patch("esphome.platformio.toolchain.sys.executable", prefixed_exe), ): @@ -1093,3 +1241,361 @@ def test_filter_platformio_lines_blocks_noisy_messages(msg: str) -> None: def test_filter_platformio_lines_allows_other_messages(msg: str) -> None: """Test that non-noisy platformio output lines pass through RedirectText.""" assert _filter_through_redirect(msg) == msg + "\n" + + +# --------------------------------------------------------------------------- +# PlatformIO python-version cache heal +# --------------------------------------------------------------------------- + +_CURRENT_MINOR = f"{sys.version_info.major}.{sys.version_info.minor}" +# Captured before the autouse guard patches the name, so tests can exercise the +# real implementation. +_REAL_GET_PLATFORMIO_CONFIG = toolchain.get_platformio_config + + +@pytest.fixture(autouse=True) +def _guard_real_platformio() -> Generator[None, None, None]: + """Default the PlatformIO config lookup to None so no test in this module + touches a real ~/.platformio; the heal tests re-patch it at a temp dir.""" + with patch.object(toolchain, "get_platformio_config", return_value=None): + yield + + +def _pio_layout(core_dir: Path) -> dict[str, Path]: + """Return the PlatformIO dir layout with cache/packages/platforms under core.""" + return { + "core_dir": core_dir, + "packages_dir": core_dir / "packages", + "platforms_dir": core_dir / "platforms", + "cache_dir": core_dir / ".cache", + } + + +def _split_pio_layout(tmp_path: Path) -> dict[str, Path]: + """Container-shape layout: caches on a persistent root, core_dir ephemeral.""" + persistent = tmp_path / "data" / "platformio" + return { + "core_dir": tmp_path / "root" / ".platformio", + "platforms_dir": persistent / "platforms", + "packages_dir": persistent / "packages", + "cache_dir": persistent / "cache", + } + + +def _seed_layout(layout: dict[str, Path]) -> None: + """Populate each cache dir (and the core penv) with a marker file.""" + for key in ("platforms_dir", "packages_dir", "cache_dir"): + layout[key].mkdir(parents=True, exist_ok=True) + (layout[key] / "marker").write_text("x", encoding="utf-8") + penv = layout["core_dir"] / "penv" + penv.mkdir(parents=True, exist_ok=True) + (penv / "marker").write_text("x", encoding="utf-8") + + +def _make_pio_config(layout: dict[str, Path] | Path) -> MagicMock: + """A ProjectConfig stand-in resolving platformio dir options from *layout*.""" + resolved = _pio_layout(layout) if isinstance(layout, Path) else layout + config = MagicMock() + config.get.side_effect = lambda section, option: ( + str(resolved[option]) if section == "platformio" else "" + ) + return config + + +@contextmanager +def _use_pio_config(layout: dict[str, Path] | Path) -> Generator[MagicMock, None, None]: + """Point ``get_platformio_config`` at a temp layout for the block.""" + config = _make_pio_config(layout) + with patch.object(toolchain, "get_platformio_config", return_value=config): + yield config + + +def _stamp_version(core_dir: Path) -> str | None: + """Read the python version recorded in the heal stamp under *core_dir*.""" + return toolchain._read_pio_stamp_python(core_dir / toolchain._PIO_PYTHON_STAMP_FILE) + + +def _cache_wiped(core_dir: Path) -> bool: + """True when the seeded cache subdir markers are gone.""" + return not any( + (core_dir / sub / "marker").exists() + for sub in ("packages", "platforms", ".cache") + ) + + +@pytest.fixture +def pio_core_dir(tmp_path: Path) -> Path: + """A populated PlatformIO core dir (packages/platforms/.cache/penv seeded).""" + core = tmp_path / "dot-platformio" + for sub in ("packages", "platforms", ".cache", "penv"): + seeded = core / sub + seeded.mkdir(parents=True) + (seeded / "marker").write_text("x", encoding="utf-8") + return core + + +def test_current_python_minor_matches_running_interpreter() -> None: + """_current_python_minor returns major.minor of the running interpreter.""" + assert toolchain._current_python_minor() == _CURRENT_MINOR + + +def test_pio_stamp_round_trip(tmp_path: Path) -> None: + """The stamp writer/reader round-trips and records the schema version.""" + stamp = tmp_path / toolchain._PIO_PYTHON_STAMP_FILE + toolchain._write_pio_stamp_python(stamp, "3.13") + assert toolchain._read_pio_stamp_python(stamp) == "3.13" + assert json.loads(stamp.read_text()) == { + "schema_version": toolchain._PIO_PYTHON_STAMP_SCHEMA, + "python_version": "3.13", + } + + +def test_read_pio_stamp_missing(tmp_path: Path) -> None: + """A missing stamp file yields None.""" + assert toolchain._read_pio_stamp_python(tmp_path / "nope.json") is None + + +def test_read_pio_stamp_malformed(tmp_path: Path) -> None: + """A corrupt stamp file yields None instead of raising.""" + stamp = tmp_path / "bad.json" + stamp.write_text("{not json", encoding="utf-8") + assert toolchain._read_pio_stamp_python(stamp) is None + + +def test_read_pio_stamp_unreadable_logs_warning( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A present-but-unreadable stamp yields None and warns.""" + stamp = tmp_path / "stamp.json" + stamp.mkdir() + with caplog.at_level("WARNING"): + assert toolchain._read_pio_stamp_python(stamp) is None + assert "Could not read" in caplog.text + + +def test_read_pio_stamp_without_python_version(tmp_path: Path) -> None: + """A stamp missing python_version yields None.""" + stamp = tmp_path / "s.json" + stamp.write_text(json.dumps({"schema_version": "0"}), encoding="utf-8") + assert toolchain._read_pio_stamp_python(stamp) is None + + +@pytest.mark.parametrize("payload", ["42", '"x"', "[1, 2]", "null"]) +def test_read_pio_stamp_non_object_json(tmp_path: Path, payload: str) -> None: + """Valid-but-non-object JSON in the stamp yields None, not a crash.""" + stamp = tmp_path / "s.json" + stamp.write_text(payload, encoding="utf-8") + assert toolchain._read_pio_stamp_python(stamp) is None + + +def test_clean_platformio_cache_none_config_is_noop() -> None: + """clean_platformio_cache is a no-op when PlatformIO is unavailable.""" + with patch.object(toolchain, "get_platformio_config", return_value=None): + toolchain.clean_platformio_cache() + + +def test_clean_platformio_cache_wipes_everything(pio_core_dir: Path) -> None: + """clean_platformio_cache removes cache/packages/platforms and core_dir.""" + with _use_pio_config(pio_core_dir): + toolchain.clean_platformio_cache() + assert not pio_core_dir.exists() + + +def test_heal_none_config_is_noop() -> None: + """Heal is a no-op (no error) when PlatformIO is unavailable.""" + with patch.object(toolchain, "get_platformio_config", return_value=None): + toolchain.heal_platformio_python_env() + + +def test_heal_fresh_cache_stamps_without_wipe(tmp_path: Path) -> None: + """A fresh core dir (no stamp, no penv) is stamped, not wiped.""" + core = tmp_path / "pio" + with _use_pio_config(core): + toolchain.heal_platformio_python_env() + assert _stamp_version(core) == _CURRENT_MINOR + + +def test_heal_stamp_matches_current_no_wipe(pio_core_dir: Path) -> None: + """A stamp matching the running interpreter leaves the cache untouched.""" + toolchain._write_pio_stamp_python( + pio_core_dir / toolchain._PIO_PYTHON_STAMP_FILE, _CURRENT_MINOR + ) + with _use_pio_config(pio_core_dir): + toolchain.heal_platformio_python_env() + assert not _cache_wiped(pio_core_dir) + assert (pio_core_dir / "penv" / "marker").exists() + + +def test_heal_stale_stamp_wipes_and_restamps(pio_core_dir: Path) -> None: + """A stamp from an older interpreter triggers a wipe + restamp; core_dir stays.""" + toolchain._write_pio_stamp_python( + pio_core_dir / toolchain._PIO_PYTHON_STAMP_FILE, "2.7" + ) + with _use_pio_config(pio_core_dir): + toolchain.heal_platformio_python_env() + assert _cache_wiped(pio_core_dir) + assert not (pio_core_dir / "penv").exists() + assert pio_core_dir.is_dir() + assert _stamp_version(pio_core_dir) == _CURRENT_MINOR + + +def test_heal_no_stamp_existing_cache_wipes_once( + pio_core_dir: Path, caplog: pytest.LogCaptureFixture +) -> None: + """An existing cache with no stamp is cleaned once and stamped.""" + with _use_pio_config(pio_core_dir), caplog.at_level("INFO"): + toolchain.heal_platformio_python_env() + assert _cache_wiped(pio_core_dir) + assert not (pio_core_dir / "penv").exists() + assert _stamp_version(pio_core_dir) == _CURRENT_MINOR + assert "once" in caplog.text + + +def test_heal_no_stamp_penv_only_counts_as_cache(tmp_path: Path) -> None: + """A core dir holding only a penv still triggers the one-time clean.""" + core = tmp_path / "pio" + penv = core / "penv" + penv.mkdir(parents=True) + (penv / "marker").write_text("x", encoding="utf-8") + with _use_pio_config(core): + toolchain.heal_platformio_python_env() + assert not penv.exists() + assert _stamp_version(core) == _CURRENT_MINOR + + +def test_heal_oserror_is_nonfatal( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A filesystem failure during the check warns instead of aborting the build.""" + blocker = tmp_path / "pio" + blocker.write_text("not a directory", encoding="utf-8") + with _use_pio_config(blocker), caplog.at_level("WARNING"): + toolchain.heal_platformio_python_env() + assert "build environment check failed" in caplog.text + + +def test_heal_stamp_write_failure_is_nonfatal( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A failed stamp write (EsphomeError from write_file) warns, not aborts.""" + with ( + _use_pio_config(tmp_path / "pio"), + patch.object( + toolchain, + "_write_pio_stamp_python", + side_effect=EsphomeError("disk full"), + ), + caplog.at_level("WARNING"), + ): + toolchain.heal_platformio_python_env() + assert "build environment check failed" in caplog.text + + +def test_heal_is_idempotent_across_runs(pio_core_dir: Path) -> None: + """After a heal writes the stamp, a re-provisioned cache is not wiped again.""" + with _use_pio_config(pio_core_dir): + toolchain.heal_platformio_python_env() + repop = pio_core_dir / "packages" + repop.mkdir(exist_ok=True) + (repop / "marker").write_text("x", encoding="utf-8") + toolchain.heal_platformio_python_env() + assert (pio_core_dir / "packages" / "marker").exists() + + +def test_pio_stamp_dir_is_platforms_parent(tmp_path: Path) -> None: + """The stamp home is the parent of platforms_dir, not core_dir.""" + layout = _split_pio_layout(tmp_path) + config = _make_pio_config(layout) + assert toolchain._pio_stamp_dir(config) == layout["platforms_dir"].parent + nested = _make_pio_config(tmp_path / "pio") + assert toolchain._pio_stamp_dir(nested) == tmp_path / "pio" + + +def test_heal_container_layout_stamps_persistent_root(tmp_path: Path) -> None: + """Container shape: the stamp lands on the persistent cache root.""" + layout = _split_pio_layout(tmp_path) + with _use_pio_config(layout): + toolchain.heal_platformio_python_env() + persistent = layout["platforms_dir"].parent + assert _stamp_version(persistent) == _CURRENT_MINOR + assert not (layout["core_dir"] / toolchain._PIO_PYTHON_STAMP_FILE).exists() + + +def test_heal_container_layout_stale_stamp_wipes_persistent_cache( + tmp_path: Path, +) -> None: + """Container shape: a stale stamp wipes the relocated persistent caches.""" + layout = _split_pio_layout(tmp_path) + _seed_layout(layout) + persistent = layout["platforms_dir"].parent + toolchain._write_pio_stamp_python( + persistent / toolchain._PIO_PYTHON_STAMP_FILE, "2.7" + ) + with _use_pio_config(layout): + toolchain.heal_platformio_python_env() + for key in ("platforms_dir", "packages_dir", "cache_dir"): + assert not layout[key].exists() + assert not (layout["core_dir"] / "penv").exists() + assert _stamp_version(persistent) == _CURRENT_MINOR + + +def test_heal_container_layout_survives_core_dir_wipe(tmp_path: Path) -> None: + """A python change is still detected after an image update wiped core_dir.""" + layout = _split_pio_layout(tmp_path) + _seed_layout(layout) + shutil.rmtree(layout["core_dir"]) + persistent = layout["platforms_dir"].parent + toolchain._write_pio_stamp_python( + persistent / toolchain._PIO_PYTHON_STAMP_FILE, "2.7" + ) + with _use_pio_config(layout): + toolchain.heal_platformio_python_env() + for key in ("platforms_dir", "packages_dir", "cache_dir"): + assert not layout[key].exists() + assert _stamp_version(persistent) == _CURRENT_MINOR + + +def test_get_platformio_config_returns_project_config() -> None: + """The real lookup returns a usable ProjectConfig when PlatformIO is present.""" + config = _REAL_GET_PLATFORMIO_CONFIG() + assert config is not None + assert hasattr(config, "get") + + +def test_get_platformio_config_none_when_platformio_absent() -> None: + """The lookup returns None when PlatformIO cannot be imported.""" + with patch.dict(sys.modules, {"platformio.project.config": None}): + assert _REAL_GET_PLATFORMIO_CONFIG() is None + + +def test_delete_platformio_dirs_skips_missing(tmp_path: Path) -> None: + """A named dir that does not exist is skipped without error.""" + (tmp_path / "packages").mkdir() + (tmp_path / "packages" / "marker").write_text("x", encoding="utf-8") + config = _make_pio_config(tmp_path) + # platforms_dir does not exist; packages_dir does. + toolchain._delete_platformio_dirs(config, ["packages_dir", "platforms_dir"]) + assert not (tmp_path / "packages").exists() + + +def test_heal_stale_stamp_wipes_when_penv_absent(pio_core_dir: Path) -> None: + """The penv wipe is skipped cleanly when no penv exists.""" + shutil.rmtree(pio_core_dir / "penv") + toolchain._write_pio_stamp_python( + pio_core_dir / toolchain._PIO_PYTHON_STAMP_FILE, "2.7" + ) + with _use_pio_config(pio_core_dir): + toolchain.heal_platformio_python_env() + assert _cache_wiped(pio_core_dir) + assert _stamp_version(pio_core_dir) == _CURRENT_MINOR + + +def test_run_platformio_cli_invokes_heal( + setup_core: Path, mock_run_external_process: Mock +) -> None: + """run_platformio_cli runs the heal before spawning PlatformIO.""" + CORE.build_path = str(setup_core / "build" / "test") + mock_run_external_process.return_value = 0 + with patch.object(toolchain, "heal_platformio_python_env") as mock_heal: + toolchain.run_platformio_cli("test") + mock_heal.assert_called_once() diff --git a/tests/unit_tests/test_preferences.py b/tests/unit_tests/test_preferences.py new file mode 100644 index 0000000000..677eeee7f2 --- /dev/null +++ b/tests/unit_tests/test_preferences.py @@ -0,0 +1,149 @@ +"""Tests for esphome.preferences storage backend selection.""" + +import pytest + +from esphome import preferences +from esphome.components.esp32 import KEY_ESP32 +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C61, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_STORAGE, + KEY_CORE, + KEY_TARGET_PLATFORM, + KEY_VARIANT, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, +) +from esphome.core import CORE + + +def _set_platform(platform: str) -> None: + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform} + + +def _set_esp32(variant: str) -> None: + _set_platform(PLATFORM_ESP32) + CORE.data[KEY_ESP32] = {KEY_VARIANT: variant} + + +def _validate(value: dict): + return cv.Schema(preferences.storage_schema())(value) + + +def _define_names() -> set[str]: + return {define.name for define in CORE.defines} + + +def test_is_in_flash() -> None: + _set_platform(PLATFORM_ESP8266) + assert preferences.is_in_flash(preferences.STORAGE_FLASH) is True + assert preferences.is_in_flash(preferences.STORAGE_RTC) is False + # The RTC storage define is ESP32-specific. + assert "USE_ESP32_RTC_PREFERENCES" not in _define_names() + + +def test_is_in_flash_esp32_rtc_emits_define() -> None: + _set_esp32(VARIANT_ESP32) + assert preferences.is_in_flash(preferences.STORAGE_FLASH) is True + assert "USE_ESP32_RTC_PREFERENCES" not in _define_names() + assert preferences.is_in_flash(preferences.STORAGE_RTC) is False + assert "USE_ESP32_RTC_PREFERENCES" in _define_names() + + +def test_request_rtc_storage_esp32_only() -> None: + _set_platform(PLATFORM_ESP8266) + preferences.request_rtc_storage() + # ESP8266 always has its RTC backend; no define is needed or emitted. + assert "USE_ESP32_RTC_PREFERENCES" not in _define_names() + + +def test_request_rtc_storage_esp32_emits_define() -> None: + _set_esp32(VARIANT_ESP32) + preferences.request_rtc_storage() + assert "USE_ESP32_RTC_PREFERENCES" in _define_names() + + +@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C3]) +def test_validate_rtc_storage_accepted(variant: str) -> None: + _set_esp32(variant) + assert preferences.validate_rtc_storage(True) is True + assert preferences.validate_rtc_storage(False) is False + + +def test_validate_rtc_storage_esp8266() -> None: + _set_platform(PLATFORM_ESP8266) + # Tolerated no-op: the ESP8266 backend always has RTC storage. + assert preferences.validate_rtc_storage(True) is True + # But it cannot be disabled, so an explicit false is an error. + with pytest.raises(cv.Invalid, match="always enabled on ESP8266"): + preferences.validate_rtc_storage(False) + + +@pytest.mark.parametrize("variant", [VARIANT_ESP32C2, VARIANT_ESP32C61]) +def test_validate_rtc_storage_rejected_without_rtc_memory(variant: str) -> None: + _set_esp32(variant) + with pytest.raises(cv.Invalid, match="not supported on this platform"): + preferences.validate_rtc_storage(True) + # Disabling it is always fine. + assert preferences.validate_rtc_storage(False) is False + + +def test_validate_rtc_storage_rejected_on_unsupported_platform() -> None: + _set_platform(PLATFORM_RP2040) + with pytest.raises(cv.Invalid, match="not supported on this platform"): + preferences.validate_rtc_storage(True) + + +@pytest.mark.parametrize( + ("platform", "expected"), + [ + # Defaults preserve each platform's historic behavior. + (PLATFORM_ESP8266, preferences.STORAGE_RTC), + (PLATFORM_RP2040, preferences.STORAGE_FLASH), + ], +) +def test_default_storage_per_platform(platform: str, expected: str) -> None: + _set_platform(platform) + assert _validate({})[CONF_STORAGE] == expected + + +@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C2]) +def test_default_storage_esp32_is_flash(variant: str) -> None: + # ESP32 defaults to flash on every variant, including those without RTC memory. + _set_esp32(variant) + assert _validate({})[CONF_STORAGE] == preferences.STORAGE_FLASH + + +def test_rtc_allowed_on_esp8266() -> None: + _set_platform(PLATFORM_ESP8266) + assert _validate({CONF_STORAGE: "rtc"})[CONF_STORAGE] == preferences.STORAGE_RTC + + +@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C3]) +def test_rtc_allowed_on_esp32_with_rtc_memory(variant: str) -> None: + _set_esp32(variant) + assert _validate({CONF_STORAGE: "rtc"})[CONF_STORAGE] == preferences.STORAGE_RTC + + +@pytest.mark.parametrize("variant", [VARIANT_ESP32C2, VARIANT_ESP32C61]) +def test_rtc_rejected_on_esp32_without_rtc_memory(variant: str) -> None: + _set_esp32(variant) + with pytest.raises(cv.Invalid, match="not supported on this platform"): + _validate({CONF_STORAGE: "rtc"}) + + +def test_rtc_rejected_on_unsupported_platform() -> None: + _set_platform(PLATFORM_RP2040) + with pytest.raises(cv.Invalid, match="not supported on this platform"): + _validate({CONF_STORAGE: "rtc"}) + + +def test_flash_allowed_on_unsupported_platform() -> None: + _set_platform(PLATFORM_RP2040) + assert _validate({CONF_STORAGE: "flash"})[CONF_STORAGE] == preferences.STORAGE_FLASH diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 0ce89230d8..244e4eb5a1 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -11,6 +11,7 @@ from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS from esphome.components.esp32.boards import ESP32_BOARD_PINS from esphome.components.esp8266.boards import ESP8266_BOARD_PINS from esphome.components.ln882x.boards import LN882X_BOARD_PINS +from esphome.components.rp2.boards import RP2_BOARD_PINS from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS from esphome.core import CORE import esphome.wizard as wz @@ -300,6 +301,31 @@ def test_wizard_write_defaults_platform_from_board_rtl87xx( assert "rtl87xx:" in generated_config +def test_wizard_write_defaults_platform_from_board_rp2( + default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch +): + """ + If the platform is not explicitly set, use "RP2" when the board is in + the RP2 boards list. The generated config must use the canonical + ``rp2:`` top-level key (not the deprecated ``rp2040:`` alias). + """ + # Given + del default_config["platform"] + default_config["board"] = [*RP2_BOARD_PINS][0] + + monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "rp2:" in generated_config + # Guard against regressing to the legacy alias key. + assert "rp2040:" not in generated_config + + def test_safe_print_step_prints_step_number_and_description(monkeypatch: MonkeyPatch): """ The safe_print_step function prints the step number and the passed description @@ -450,6 +476,34 @@ def test_wizard_accepts_default_answers_esp32( assert retval == 0 +def test_wizard_accepts_default_answers_bk72xx( + tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] +): + """ + The wizard should accept the given default answers for bk72xx. The + libretiny branch also exercises the False side of the + ``elif platform == "RP2":`` checks in the platform / board-link + elif chain (without this, those branches show as partial coverage + because only the rpipico interactive test reaches them with platform + == "RP2"). + """ + # Given + wizard_answers[1] = "BK72XX" + wizard_answers[2] = next(iter(BK72XX_BOARD_PINS)) + config_file = tmp_path / "test.yaml" + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(config_file) + + # Then + assert retval == 0 + + def test_wizard_offers_better_node_name( tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] ): @@ -612,7 +666,7 @@ def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch): # Given wizard_answers_rp2040 = [ "test-node", # Name of the node - "RP2040", # platform + "RP2", # platform (canonical name; ``RP2040`` was the legacy alias) "rpipico", # board (no WiFi support) ] config_file = tmp_path / "test.yaml" diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 07f334d350..46e60ebd8e 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -18,7 +18,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_RP2040, + PLATFORM_RP2, PLATFORM_RTL87XX, ) from esphome.core import EsphomeError @@ -338,7 +338,7 @@ def test_storage_should_not_update_cmake_cache_when_nothing_changes( @pytest.mark.parametrize( "core_platform", - [PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_RTL87XX], + [PLATFORM_ESP8266, PLATFORM_RP2, PLATFORM_BK72XX, PLATFORM_RTL87XX], ) def test_storage_should_not_update_cmake_cache_for_non_esp32( create_storage: Callable[..., StorageJSON], diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index fa1c0fcce2..5c38fce105 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -1349,6 +1349,57 @@ def test_sensitive_str__is_a_str_subclass() -> None: assert value == "hunter2" +def test_dump_path_without_relative_to_is_unchanged() -> None: + """Test that Path values dump as str(path) when relative_to is not given.""" + path = Path("some") / "dir" / "file.ttf" + output = yaml_util.dump({"file": path}) + assert output.strip() == f"file: {path}" + + +def test_dump_path_relative_to_anchor_dir() -> None: + """Test that Path values under relative_to dump as relative POSIX paths.""" + anchor = Path("/config/esphome").absolute() + data = {"file": anchor / "fonts" / "arial.ttf"} + output = yaml_util.dump(data, relative_to=anchor) + assert output.strip() == "file: fonts/arial.ttf" + + +def test_dump_path_outside_anchor_dir_walks_up() -> None: + """Test that Path values outside relative_to walk up with ".." segments.""" + anchor = Path("/config/esphome").absolute() + outside = Path("/config/fonts/file.ttf").absolute() + output = yaml_util.dump({"file": outside}, relative_to=anchor) + assert output.strip() == "file: ../fonts/file.ttf" + + +def test_dump_path_with_dotdot_segments_is_normalized() -> None: + """Test that ".." segments do not defeat relativization. + + A path like /config/other/../esphome/fonts/x.ttf is under the anchor + once normalized, so it must dump as a plain relative path. + """ + anchor = Path("/config/esphome").absolute() + path = Path("/config/other/../esphome/fonts/x.ttf").absolute() + output = yaml_util.dump({"file": path}, relative_to=anchor) + assert output.strip() == "file: fonts/x.ttf" + + +def test_dump_path_dotdot_reference_outside_anchor() -> None: + """Test the relative_config_path("../...") shape stays relative.""" + anchor = Path("/config/esphome").absolute() + path = anchor / ".." / "shared" / "font.ttf" + output = yaml_util.dump({"file": path}, relative_to=anchor) + assert output.strip() == "file: ../shared/font.ttf" + + +def test_dump_relative_to_does_not_leak_between_calls() -> None: + """Test that the relative_to flag is scoped to a single dump call.""" + anchor = Path("/config/esphome").absolute() + path = anchor / "fonts" / "arial.ttf" + assert "fonts/arial.ttf" in yaml_util.dump({"file": path}, relative_to=anchor) + assert yaml_util.dump({"file": path}).strip() == f"file: {path}" + + def test_dump__redacts_sensitive_str_by_default() -> None: out = yaml_util.dump({"password": SensitiveStr("hunter2")}) assert "\\033[8mhunter2\\033[28m" in out