mirror of
https://github.com/esphome/esphome.git
synced 2026-07-05 06:29:41 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c37ce25fb7 | |||
| 8921a9dd60 | |||
| 14b804f3e1 | |||
| d15a9597d7 | |||
| 5460ee7edd | |||
| 40081e5ae7 | |||
| a7c5b0ab46 | |||
| e1a813e11f | |||
| 1dfeef0265 | |||
| 395610c117 | |||
| ae96f82b82 | |||
| 2c610abcd0 | |||
| d3591c8d9e | |||
| ec420d5792 | |||
| 17209df7b5 | |||
| 9cf9b02ba2 | |||
| c90fa2378a | |||
| c04dfa922e | |||
| 668007707d | |||
| ab71f5276f | |||
| d062f62656 | |||
| 03db32d045 | |||
| 8f6d489a9a | |||
| dd07fba943 | |||
| 6f5d642a31 | |||
| 2721f08bcc | |||
| eafc5df3f2 | |||
| 46d0c29be5 | |||
| abdbbf4dd2 | |||
| 4dc0599a7d | |||
| 52c35ec09c | |||
| 76490e45bc | |||
| 0a8130858c |
@@ -47,7 +47,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
|
||||
TOO_BIG_MARKER: '<!-- too-big-request -->',
|
||||
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
|
||||
ORG_FORK_MARKER: '<!-- maintainer-access-warning -->',
|
||||
|
||||
MANAGED_LABELS: [
|
||||
'new-component',
|
||||
|
||||
@@ -281,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
return { labels, deprecatedInfo };
|
||||
}
|
||||
|
||||
// Strategy: Detect when maintainers cannot modify the PR branch
|
||||
function detectMaintainerAccess(context) {
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Only relevant for cross-repo PRs (forks)
|
||||
if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pr.maintainer_can_modify) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOrgFork = pr.head.repo.owner.type === 'Organization';
|
||||
console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`);
|
||||
return { isOrgFork, orgName: pr.head.repo.owner.login };
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels, prFiles, context) {
|
||||
const labels = new Set();
|
||||
@@ -329,5 +347,6 @@ module.exports = {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
};
|
||||
|
||||
@@ -12,9 +12,10 @@ const {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
} = require('./detectors');
|
||||
const { handleReviews } = require('./reviews');
|
||||
const { handleReviews, handleMaintainerAccessComment } = require('./reviews');
|
||||
const { applyLabels, removeOldLabels } = require('./labels');
|
||||
|
||||
// Fetch API data
|
||||
@@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => {
|
||||
codeOwnerLabels,
|
||||
testLabels,
|
||||
checkboxLabels,
|
||||
deprecatedResult
|
||||
deprecatedResult,
|
||||
maintainerAccess
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(context),
|
||||
detectComponentPlatforms(changedFiles, apiData),
|
||||
@@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => {
|
||||
detectCodeOwner(github, context, changedFiles),
|
||||
detectTests(changedFiles),
|
||||
detectPRTemplateCheckboxes(context),
|
||||
detectDeprecatedComponents(github, context, changedFiles)
|
||||
detectDeprecatedComponents(github, context, changedFiles),
|
||||
detectMaintainerAccess(context)
|
||||
]);
|
||||
|
||||
// Extract deprecated component info
|
||||
@@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => {
|
||||
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Handle reviews
|
||||
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||
// Handle reviews and org fork comment
|
||||
await Promise.all([
|
||||
handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD),
|
||||
handleMaintainerAccessComment(github, context, maintainerAccess)
|
||||
]);
|
||||
|
||||
// Apply labels
|
||||
await applyLabels(github, context, finalLabels);
|
||||
|
||||
@@ -2,7 +2,8 @@ const {
|
||||
BOT_COMMENT_MARKER,
|
||||
CODEOWNERS_MARKER,
|
||||
TOO_BIG_MARKER,
|
||||
DEPRECATED_COMPONENT_MARKER
|
||||
DEPRECATED_COMPONENT_MARKER,
|
||||
ORG_FORK_MARKER
|
||||
} = require('./constants');
|
||||
|
||||
// Generate review messages
|
||||
@@ -136,6 +137,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d
|
||||
}
|
||||
}
|
||||
|
||||
// Handle maintainer access warning comment
|
||||
async function handleMaintainerAccessComment(github, context, maintainerAccess) {
|
||||
if (!maintainerAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
// Check if we already posted the warning (iterate pages to exit early)
|
||||
let existingComment;
|
||||
for await (const { data: comments } of github.paginate.iterator(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number: pr_number }
|
||||
)) {
|
||||
existingComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body && comment.body.includes(ORG_FORK_MARKER)
|
||||
);
|
||||
if (existingComment) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingComment) {
|
||||
console.log('Maintainer access warning comment already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
let body;
|
||||
if (maintainerAccess.isOrgFork) {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` +
|
||||
`GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` +
|
||||
`See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`;
|
||||
} else {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`Please enable this option in the PR sidebar to allow maintainer collaboration.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body
|
||||
});
|
||||
console.log('Created maintainer access warning comment');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleReviews
|
||||
handleReviews,
|
||||
handleMaintainerAccessComment
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto Label PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
fi
|
||||
- if: failure()
|
||||
name: Review PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
esphome/components/api/api_pb2_service.*
|
||||
- if: success()
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Request changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
@@ -868,7 +868,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
@@ -903,7 +904,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -954,7 +955,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
@@ -967,7 +969,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
CODEOWNERS
|
||||
|
||||
- name: Check codeowner approval and update label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Request reviews from component codeowners
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add external component comment
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify codeowners for component issues
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const {
|
||||
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
repositories: home-assistant-addon
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
repositories: esphome-schema
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
repositories: version-notifier
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -2,30 +2,29 @@ name: Status check labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled]
|
||||
types: [opened, reopened, labeled, unlabeled, synchronize]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check ${{ matrix.label }}
|
||||
name: Check blocking labels
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
label:
|
||||
- needs-docs
|
||||
- merge-after-release
|
||||
- chained-pr
|
||||
steps:
|
||||
- name: Check for ${{ matrix.label }} label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- name: Check for blocking labels
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr'];
|
||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
|
||||
if (hasLabel) {
|
||||
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
|
||||
const labelNames = labels.map(l => l.name);
|
||||
const found = blockingLabels.filter(bl => labelNames.includes(bl));
|
||||
if (found.length > 0) {
|
||||
core.setFailed(`Pull request cannot be merged, it has blocking label(s): ${found.join(', ')}`);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.9
|
||||
rev: v0.15.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -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.4.0b1
|
||||
PROJECT_NUMBER = 2026.5.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
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ade7953_base {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
constexpr uint16_t CONFIG_DEFAULT = 0x8004u;
|
||||
constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u;
|
||||
|
||||
static const float ADE_POWER_FACTOR = 154.0f;
|
||||
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
|
||||
|
||||
@@ -18,7 +21,12 @@ void ADE7953::setup() {
|
||||
|
||||
// The chip might take up to 100ms to initialise
|
||||
this->set_timeout(100, [this]() {
|
||||
// this->ade_write_8(0x0010, 0x04);
|
||||
// Lock communication interface (SPI or I2C)
|
||||
uint16_t config_v = CONFIG_DEFAULT;
|
||||
this->ade_read_16(CONFIG_16, &config_v);
|
||||
config_v &= static_cast<uint16_t>(~CONFIG_LOCK_BIT); // Clear the lock bit
|
||||
this->ade_write_16(CONFIG_16, config_v);
|
||||
// Configure optimum settings according to datasheet
|
||||
this->ade_write_8(0x00FE, 0xAD);
|
||||
this->ade_write_16(0x0120, 0x0030);
|
||||
// Set gains
|
||||
|
||||
@@ -9,31 +9,35 @@
|
||||
namespace esphome {
|
||||
namespace ade7953_base {
|
||||
|
||||
static const uint8_t PGA_V_8 =
|
||||
static constexpr uint8_t PGA_V_8 =
|
||||
0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IA_8 =
|
||||
static constexpr uint8_t PGA_IA_8 =
|
||||
0x008; // PGA_IA, (R/W) Default: 0x00, Unsigned, Current Channel A gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IB_8 =
|
||||
static constexpr uint8_t PGA_IB_8 =
|
||||
0x009; // PGA_IB, (R/W) Default: 0x00, Unsigned, Current Channel B gain configuration (Bits[2:0])
|
||||
|
||||
static const uint32_t AIGAIN_32 =
|
||||
static constexpr uint16_t CONFIG_16 = 0x102; // CONFIG, (R/W) Default: 0x8004, Unsigned, Configuration register
|
||||
|
||||
static constexpr uint16_t AIGAIN_32 =
|
||||
0x380; // AIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVGAIN_32 = 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t AWGAIN_32 =
|
||||
static constexpr uint16_t AVGAIN_32 =
|
||||
0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t AWGAIN_32 =
|
||||
0x382; // AWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVARGAIN_32 =
|
||||
static constexpr uint16_t AVARGAIN_32 =
|
||||
0x383; // AVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVAGAIN_32 =
|
||||
static constexpr uint16_t AVAGAIN_32 =
|
||||
0x384; // AVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel A)(32 bit)
|
||||
|
||||
static const uint32_t BIGAIN_32 =
|
||||
static constexpr uint16_t BIGAIN_32 =
|
||||
0x38C; // BIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVGAIN_32 = 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t BWGAIN_32 =
|
||||
static constexpr uint16_t BVGAIN_32 =
|
||||
0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t BWGAIN_32 =
|
||||
0x38E; // BWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVARGAIN_32 =
|
||||
static constexpr uint16_t BVARGAIN_32 =
|
||||
0x38F; // BVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVAGAIN_32 =
|
||||
static constexpr uint16_t BVAGAIN_32 =
|
||||
0x390; // BVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel B)(32 bit)
|
||||
|
||||
class ADE7953 : public PollingComponent, public sensor::Sensor {
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ade7953_spi {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
// Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS
|
||||
constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2;
|
||||
|
||||
void AdE7953Spi::setup() {
|
||||
this->spi_setup();
|
||||
ade7953_base::ADE7953::setup();
|
||||
@@ -32,6 +35,9 @@ bool AdE7953Spi::ade_write_16(uint16_t reg, uint16_t value) {
|
||||
this->write_byte16(reg);
|
||||
this->transfer_byte(0);
|
||||
this->write_byte16(value);
|
||||
if (reg == ade7953_base::CONFIG_16) {
|
||||
delayMicroseconds(CONFIG_LOCK_SETTLE_US);
|
||||
}
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace esphome {
|
||||
namespace ade7953_spi {
|
||||
|
||||
class AdE7953Spi : public ade7953_base::ADE7953,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_LEADING,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
@@ -1625,6 +1625,7 @@ message BluetoothLEAdvertisementResponse {
|
||||
}
|
||||
|
||||
message BluetoothLERawAdvertisement {
|
||||
option (inline_encode) = true;
|
||||
uint64 address = 1 [(force) = true];
|
||||
sint32 rssi = 2 [(force) = true];
|
||||
uint32 address_type = 3 [(max_value) = 4];
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Read a maximum of 5 messages per loop iteration to prevent starving other components.
|
||||
// Maximum messages to read per loop iteration to prevent starving other components.
|
||||
// This is a balance between API responsiveness and allowing other components to run.
|
||||
// Since each message could contain multiple protobuf messages when using packet batching,
|
||||
// this limits the number of messages processed, not the number of TCP packets.
|
||||
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
|
||||
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 10;
|
||||
static constexpr uint8_t MAX_PING_RETRIES = 60;
|
||||
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
|
||||
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
|
||||
@@ -220,10 +220,17 @@ void APIConnection::loop() {
|
||||
}
|
||||
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
// Check if socket has data ready before attempting to read
|
||||
if (this->helper_->is_socket_ready()) {
|
||||
// Check if socket has data ready before attempting to read.
|
||||
// Also try reading if we hit the message limit last time — LWIP's rcvevent
|
||||
// (used by is_socket_ready) tracks pbuf dequeues, not bytes. When multiple
|
||||
// messages share a TCP segment, the last message's data stays in LWIP's
|
||||
// lastdata cache after rcvevent hits 0, making is_socket_ready() return false
|
||||
// even though data remains.
|
||||
if (this->helper_->is_socket_ready() || this->flags_.may_have_remaining_data) {
|
||||
this->flags_.may_have_remaining_data = false;
|
||||
// Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
|
||||
for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
|
||||
uint8_t message_count = 0;
|
||||
for (; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
|
||||
ReadPacketBuffer buffer;
|
||||
err = this->helper_->read_packet(&buffer);
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
@@ -245,6 +252,11 @@ void APIConnection::loop() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If we hit the limit, there may be more data remaining in LWIP's
|
||||
// lastdata cache that rcvevent doesn't account for.
|
||||
if (message_count == MAX_MESSAGES_PER_LOOP) {
|
||||
this->flags_.may_have_remaining_data = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Process deferred batch if scheduled and timer has expired
|
||||
@@ -2086,6 +2098,13 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure TCP_NODELAY is on before draining overflow and writing batch data.
|
||||
// Log messages enable Nagle (NODELAY off) to coalesce small packets.
|
||||
// If Nagle is still on when we try to drain, LWIP holds data in the
|
||||
// Nagle buffer, the TCP send buffer stays full, and the overflow
|
||||
// buffer can never drain — blocking the batch write indefinitely.
|
||||
this->helper_->set_nodelay_for_message(false);
|
||||
|
||||
// Try to clear buffer first
|
||||
if (!this->try_to_clear_buffer(true)) {
|
||||
// Can't write now, we'll try again later
|
||||
@@ -2193,13 +2212,6 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Ensure TCP_NODELAY is on before writing batch data.
|
||||
// Log messages enable Nagle (NODELAY off) to coalesce small packets.
|
||||
// Without this, batch data written to the socket sits in LWIP's Nagle
|
||||
// buffer — the remote won't ACK until it sends its own data (e.g. a
|
||||
// ping), which can take 20+ seconds.
|
||||
this->helper_->set_nodelay_for_message(false);
|
||||
|
||||
// Send all collected messages
|
||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const MessageInfo>(message_info, items_processed));
|
||||
|
||||
@@ -771,6 +771,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
uint8_t batch_scheduled : 1;
|
||||
uint8_t batch_first_message : 1; // For batch buffer allocation
|
||||
uint8_t should_try_send_immediately : 1; // True after initial states are sent
|
||||
uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
uint8_t log_only_mode : 1;
|
||||
#endif
|
||||
|
||||
@@ -195,7 +195,10 @@ class APIFrameHelper {
|
||||
}
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() const { return frame_footer_size_; }
|
||||
// Check if socket has data ready to read
|
||||
// Check if socket has buffered data ready to read.
|
||||
// Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK)
|
||||
// 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(); }
|
||||
// Release excess memory from internal buffers after initial sync
|
||||
void release_buffers() {
|
||||
|
||||
@@ -22,6 +22,7 @@ extend google.protobuf.MessageOptions {
|
||||
optional bool log = 1039 [default=true];
|
||||
optional bool no_delay = 1040 [default=false];
|
||||
optional string base_class = 1041;
|
||||
optional bool inline_encode = 1042 [default=false];
|
||||
}
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
|
||||
@@ -2328,40 +2328,37 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
|
||||
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, this->address);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
|
||||
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(this->rssi));
|
||||
if (this->address_type) {
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24);
|
||||
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->address_type);
|
||||
}
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->data_len));
|
||||
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->data, this->data_len);
|
||||
return pos;
|
||||
}
|
||||
uint32_t BluetoothLERawAdvertisement::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint64_force(1, this->address);
|
||||
size += ProtoSize::calc_sint32_force(1, this->rssi);
|
||||
size += this->address_type ? 2 : 0;
|
||||
size += 2 + this->data_len;
|
||||
return size;
|
||||
}
|
||||
uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, this->advertisements[i]);
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 10);
|
||||
uint8_t *len_pos = pos;
|
||||
ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
|
||||
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
|
||||
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi));
|
||||
if (sub_msg.address_type) {
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24);
|
||||
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address_type);
|
||||
}
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(sub_msg.data_len));
|
||||
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.data, sub_msg.data_len);
|
||||
*len_pos = static_cast<uint8_t>(pos - len_pos - 1);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size());
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
size += 2;
|
||||
size += ProtoSize::calc_uint64_force(1, sub_msg.address);
|
||||
size += ProtoSize::calc_sint32_force(1, sub_msg.rssi);
|
||||
size += sub_msg.address_type ? 2 : 0;
|
||||
size += 2 + sub_msg.data_len;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@@ -1888,8 +1888,6 @@ class BluetoothLERawAdvertisement final : public ProtoMessage {
|
||||
uint32_t address_type{0};
|
||||
uint8_t data[62]{};
|
||||
uint8_t data_len{0};
|
||||
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
|
||||
|
||||
@@ -352,6 +352,12 @@ class ProtoEncode {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = b;
|
||||
}
|
||||
/// Reserve one byte for later backpatch (e.g., sub-message length).
|
||||
/// Advances pos past the reserved byte without writing a value.
|
||||
static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
pos++;
|
||||
}
|
||||
/// Write raw bytes to the buffer (no tag, no length prefix).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
const void *data, size_t len) {
|
||||
|
||||
@@ -102,6 +102,8 @@ CC1101Component::CC1101Component() {
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
}
|
||||
|
||||
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void CC1101Component::setup() {
|
||||
this->spi_setup();
|
||||
this->cs_->digital_write(true);
|
||||
@@ -148,7 +150,12 @@ void CC1101Component::setup() {
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
|
||||
this->defer([this]() {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +167,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
this->disable_loop();
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
||||
!this->gdo0_pin_->digital_read()) {
|
||||
return;
|
||||
@@ -240,6 +248,7 @@ void CC1101Component::begin_tx() {
|
||||
this->write_(Register::PKTCTRL0, 0x32);
|
||||
ESP_LOGV(TAG, "Beginning TX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
|
||||
@@ -669,6 +678,13 @@ void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
if (value) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
} else {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
}
|
||||
}
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTCTRL1);
|
||||
this->write_(Register::IOCFG0);
|
||||
|
||||
@@ -93,6 +93,7 @@ class CC1101Component : public Component,
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace gdk101 {
|
||||
|
||||
static const char *const TAG = "gdk101";
|
||||
static constexpr uint8_t NUMBER_OF_READ_RETRIES = 5;
|
||||
static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 10;
|
||||
static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 30;
|
||||
static constexpr uint32_t RESET_INTERVAL_ID = 0;
|
||||
static constexpr uint32_t RESET_INTERVAL_MS = 1000;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from .. import hbridge_ns
|
||||
CODEOWNERS = ["@DotNetDann"]
|
||||
|
||||
HBridgeLightOutput = hbridge_ns.class_(
|
||||
"HBridgeLightOutput", cg.PollingComponent, light.LightOutput
|
||||
"HBridgeLightOutput", cg.Component, light.LightOutput
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include "esphome/components/light/light_output.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hbridge {
|
||||
|
||||
// Using PollingComponent as the updates are more consistent and reduces flickering
|
||||
class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
|
||||
class HBridgeLightOutput : public Component, public light::LightOutput {
|
||||
public:
|
||||
HBridgeLightOutput() : PollingComponent(1) {}
|
||||
|
||||
void set_pina_pin(output::FloatOutput *pina_pin) { pina_pin_ = pina_pin; }
|
||||
void set_pinb_pin(output::FloatOutput *pinb_pin) { pinb_pin_ = pinb_pin; }
|
||||
void set_pina_pin(output::FloatOutput *pina_pin) { this->pina_pin_ = pina_pin; }
|
||||
void set_pinb_pin(output::FloatOutput *pinb_pin) { this->pinb_pin_ = pinb_pin; }
|
||||
|
||||
light::LightTraits get_traits() override {
|
||||
auto traits = light::LightTraits();
|
||||
@@ -24,16 +21,16 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
|
||||
return traits;
|
||||
}
|
||||
|
||||
void setup() override { this->forward_direction_ = false; }
|
||||
void setup() override { this->disable_loop(); }
|
||||
|
||||
void update() override {
|
||||
// This method runs around 60 times per second
|
||||
// We cannot do the PWM ourselves so we are reliant on the hardware PWM
|
||||
if (!this->forward_direction_) { // First LED Direction
|
||||
void loop() override {
|
||||
// Only called when both channels are active — alternate H-bridge direction
|
||||
// each iteration to multiplex cold and warm white.
|
||||
if (!this->forward_direction_) {
|
||||
this->pina_pin_->set_level(this->pina_duty_);
|
||||
this->pinb_pin_->set_level(0);
|
||||
this->forward_direction_ = true;
|
||||
} else { // Second LED Direction
|
||||
} else {
|
||||
this->pina_pin_->set_level(0);
|
||||
this->pinb_pin_->set_level(this->pinb_duty_);
|
||||
this->forward_direction_ = false;
|
||||
@@ -43,15 +40,32 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void write_state(light::LightState *state) override {
|
||||
state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false);
|
||||
float new_pina, new_pinb;
|
||||
state->current_values_as_cwww(&new_pina, &new_pinb, false);
|
||||
|
||||
this->pina_duty_ = new_pina;
|
||||
this->pinb_duty_ = new_pinb;
|
||||
|
||||
if (new_pina != 0.0f && new_pinb != 0.0f) {
|
||||
// Both channels active — need loop to alternate H-bridge direction
|
||||
this->high_freq_.start();
|
||||
this->enable_loop();
|
||||
} else {
|
||||
// Zero or one channel active — drive pins directly, no multiplexing needed
|
||||
this->high_freq_.stop();
|
||||
this->disable_loop();
|
||||
this->pina_pin_->set_level(new_pina);
|
||||
this->pinb_pin_->set_level(new_pinb);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
output::FloatOutput *pina_pin_;
|
||||
output::FloatOutput *pinb_pin_;
|
||||
float pina_duty_ = 0;
|
||||
float pinb_duty_ = 0;
|
||||
bool forward_direction_ = false;
|
||||
float pina_duty_{0};
|
||||
float pinb_duty_{0};
|
||||
bool forward_direction_{false};
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
};
|
||||
|
||||
} // namespace hbridge
|
||||
|
||||
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
@@ -24,6 +25,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(MCP23016),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -35,6 +37,8 @@ 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)
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
def validate_mode(value):
|
||||
|
||||
@@ -24,11 +24,22 @@ void MCP23016::setup() {
|
||||
|
||||
// all pins input
|
||||
this->write_reg_(MCP23016_IODIR1, 0xFFFF);
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&MCP23016::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void MCP23016::loop() {
|
||||
// Invalidate cache at the start of each loop
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); }
|
||||
|
||||
@@ -37,6 +48,9 @@ void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin
|
||||
void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
if (flags == gpio::FLAG_INPUT) {
|
||||
this->update_reg_(pin, true, MCP23016_IODIR1);
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
this->update_reg_(pin, false, MCP23016_IODIR1);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(MCP23016 *arg);
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
@@ -51,6 +54,7 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
|
||||
uint16_t olat_{0x0000};
|
||||
// Cache for input values (16-bit combined for both banks)
|
||||
uint16_t input_mask_{0x0000};
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
class MCP23016GPIOPin : public GPIOPin {
|
||||
|
||||
@@ -451,6 +451,8 @@ async def to_code(config):
|
||||
ota.request_ota_state_listeners()
|
||||
|
||||
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
|
||||
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
|
||||
esp32.add_idf_component(name="espressif/esp-nn", ref="1.2.1")
|
||||
|
||||
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
|
||||
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
|
||||
|
||||
@@ -29,14 +29,6 @@ void VADModel::log_model_config() {
|
||||
bool StreamingModel::load_model_() {
|
||||
RAMAllocator<uint8_t> arena_allocator;
|
||||
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->var_arena_ == nullptr) {
|
||||
this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
if (this->var_arena_ == nullptr) {
|
||||
@@ -53,6 +45,26 @@ bool StreamingModel::load_model_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Probe for the actual required tensor arena size if not yet determined
|
||||
if (!this->tensor_arena_size_probed_) {
|
||||
size_t probed_size = this->probe_arena_size_();
|
||||
if (probed_size > 0) {
|
||||
ESP_LOGD(TAG, "Probed tensor arena size: %zu bytes", probed_size);
|
||||
this->tensor_arena_size_ = probed_size;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Arena size probe failed, using manifest size: %zu bytes", this->tensor_arena_size_);
|
||||
}
|
||||
this->tensor_arena_size_probed_ = true;
|
||||
}
|
||||
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->interpreter_ == nullptr) {
|
||||
this->interpreter_ =
|
||||
make_unique<tflite::MicroInterpreter>(tflite::GetModel(this->model_start_), this->streaming_op_resolver_,
|
||||
@@ -94,6 +106,70 @@ bool StreamingModel::load_model_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t StreamingModel::probe_arena_size_() {
|
||||
RAMAllocator<uint8_t> arena_allocator;
|
||||
|
||||
// Try with the manifest size first, then escalates to 1.5, then 2x if it fails. Different platforms and different
|
||||
// versions of the esp-nn library require different amounts of memory, so the manifest size may not always be correct,
|
||||
// and probing allows us to find the actual required size for the current build and platform. Aligns test sizes to 16
|
||||
// bytes.
|
||||
size_t attempt_sizes[] = {(this->tensor_arena_size_ + 15) & ~15, (this->tensor_arena_size_ * 3 / 2 + 15) & ~15,
|
||||
(this->tensor_arena_size_ * 2 + 15) & ~15};
|
||||
|
||||
for (size_t attempt_size : attempt_sizes) {
|
||||
uint8_t *probe_arena = arena_allocator.allocate(attempt_size);
|
||||
if (probe_arena == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify the model works at all with this arena size
|
||||
auto probe_interpreter = make_unique<tflite::MicroInterpreter>(
|
||||
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, attempt_size, this->mrv_);
|
||||
|
||||
if (probe_interpreter->AllocateTensors() != kTfLiteOk) {
|
||||
probe_interpreter.reset();
|
||||
arena_allocator.deallocate(probe_arena, attempt_size);
|
||||
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to shrink the arena. Start with arena_used_bytes() + 16 (rounded to 16-byte alignment).
|
||||
// If that works, use it. Otherwise, try midpoints between that and the full size until one succeeds.
|
||||
size_t lower = (probe_interpreter->arena_used_bytes() + 16 + 15) & ~15;
|
||||
probe_interpreter.reset();
|
||||
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
|
||||
|
||||
size_t upper = attempt_size;
|
||||
|
||||
while (lower < upper) {
|
||||
auto test_interpreter = make_unique<tflite::MicroInterpreter>(
|
||||
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, lower, this->mrv_);
|
||||
|
||||
bool ok = test_interpreter->AllocateTensors() == kTfLiteOk;
|
||||
|
||||
test_interpreter.reset();
|
||||
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
|
||||
|
||||
if (ok) {
|
||||
// Found a working size smaller than the full arena
|
||||
upper = lower + 16; // Pad by 16 bytes to be safe for future allocations
|
||||
break;
|
||||
}
|
||||
|
||||
// Try the midpoint between current attempt and full size
|
||||
lower = ((lower + upper) / 2 + 15) & ~15;
|
||||
}
|
||||
|
||||
arena_allocator.deallocate(probe_arena, attempt_size);
|
||||
return upper;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void StreamingModel::unload_model() {
|
||||
this->interpreter_.reset();
|
||||
|
||||
|
||||
@@ -63,6 +63,10 @@ class StreamingModel {
|
||||
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
|
||||
/// @return True if successful, false otherwise
|
||||
bool load_model_();
|
||||
/// @brief Probes the actual required tensor arena size by trial allocation.
|
||||
/// Tries the manifest size first, then 2x if that fails.
|
||||
/// @return The required arena size rounded up to 16-byte alignment, or 0 on failure.
|
||||
size_t probe_arena_size_();
|
||||
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
|
||||
bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver);
|
||||
|
||||
@@ -70,6 +74,7 @@ class StreamingModel {
|
||||
|
||||
bool loaded_{false};
|
||||
bool enabled_{true};
|
||||
bool tensor_arena_size_probed_{false};
|
||||
bool unprocessed_probability_status_{false};
|
||||
uint8_t current_stride_step_{0};
|
||||
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
|
||||
|
||||
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
@@ -25,7 +26,12 @@ PCA6416AGPIOPin = pca6416a_ns.class_(
|
||||
|
||||
CONF_PCA6416A = "pca6416a"
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent)})
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x21))
|
||||
)
|
||||
@@ -35,6 +41,8 @@ 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)
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
def validate_mode(value):
|
||||
|
||||
@@ -49,11 +49,22 @@ void PCA6416AComponent::setup() {
|
||||
|
||||
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
|
||||
this->status_has_error());
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCA6416AComponent::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void IRAM_ATTR PCA6416AComponent::gpio_intr(PCA6416AComponent *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void PCA6416AComponent::loop() {
|
||||
// Invalidate cache at the start of each loop
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void PCA6416AComponent::dump_config() {
|
||||
@@ -62,6 +73,7 @@ void PCA6416AComponent::dump_config() {
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "PCA6416A:");
|
||||
}
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
@@ -101,6 +113,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->update_register_(pin, true, pull_dir);
|
||||
this->update_register_(pin, false, pull_en);
|
||||
}
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) {
|
||||
this->update_register_(pin, true, io_dir);
|
||||
if (has_pullup_) {
|
||||
@@ -109,6 +124,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors");
|
||||
}
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
this->update_register_(pin, false, io_dir);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ class PCA6416AComponent : public Component,
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA6416AComponent *arg);
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
@@ -43,6 +46,7 @@ class PCA6416AComponent : public Component,
|
||||
esphome::i2c::ErrorCode last_error_;
|
||||
/// Only the PCAL6416A has pull-up resistors
|
||||
bool has_pullup_{false};
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
/// Helper class to expose a PCA6416A pin as an internal input GPIO pin.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <WiFi.h>
|
||||
#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock)
|
||||
#elif defined(USE_ETHERNET)
|
||||
#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock)
|
||||
#include <lwip_wrap.h> // For LWIPMutex — LwIPLock mirrors its semantics (see below)
|
||||
#include "esphome/components/ethernet/ethernet_component.h"
|
||||
#endif
|
||||
#include <hardware/structs/rosc.h>
|
||||
@@ -43,9 +43,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
||||
// main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat
|
||||
// assertion failures). See esphome#10681.
|
||||
//
|
||||
// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end.
|
||||
// Both acquire the async_context recursive mutex to prevent IRQ callbacks from
|
||||
// firing during critical sections.
|
||||
// WiFi uses cyw43_arch_lwip_begin/end.
|
||||
//
|
||||
// For wired Ethernet, taking only the async_context lock is NOT enough. The
|
||||
// W5500 GPIO IRQ path (LwipIntfDev::_irq) checks arduino-pico's `__inLWIP`
|
||||
// counter to decide whether to defer packet processing. If we hold the
|
||||
// async_context lock without bumping `__inLWIP`, an interrupt-driven packet
|
||||
// arrival re-enters lwIP from IRQ context and corrupts pbufs (the `pbuf_cat`
|
||||
// assertion crash on wiznet-w5500-evb-pico). We mirror arduino-pico's
|
||||
// LWIPMutex (cores/rp2040/lwip_wrap.h) exactly: bump `__inLWIP`, take the
|
||||
// lock, and on release re-unmask any GPIO IRQs that were deferred while we
|
||||
// held it. We can't `using LwIPLock = LWIPMutex;` in helpers.h because
|
||||
// pulling lwip_wrap.h there poisons many TUs with lwIP types.
|
||||
//
|
||||
// When neither WiFi nor Ethernet is configured, this is a no-op since
|
||||
// there's no network stack and no lwip callbacks to race with.
|
||||
@@ -53,8 +62,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
||||
LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); }
|
||||
LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); }
|
||||
#elif defined(USE_ETHERNET)
|
||||
LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); }
|
||||
LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); }
|
||||
LwIPLock::LwIPLock() {
|
||||
__inLWIP++;
|
||||
ethernet_arch_lwip_begin();
|
||||
}
|
||||
LwIPLock::~LwIPLock() {
|
||||
ethernet_arch_lwip_end();
|
||||
__inLWIP--;
|
||||
if (__needsIRQEN && !__inLWIP) {
|
||||
__needsIRQEN = false;
|
||||
ethernet_arch_lwip_gpio_unmask();
|
||||
}
|
||||
}
|
||||
#else
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
|
||||
@@ -55,11 +55,13 @@ void SafeModeComponent::dump_config() {
|
||||
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
|
||||
const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition();
|
||||
if (last_invalid != nullptr) {
|
||||
ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label);
|
||||
ESP_LOGW(TAG, "The device reset before the boot was marked successful");
|
||||
ESP_LOGW(TAG,
|
||||
"OTA rollback detected! Rolled back from partition '%s'\n"
|
||||
" The device reset before the boot was marked successful",
|
||||
last_invalid->label);
|
||||
if (esp_reset_reason() == ESP_RST_BROWNOUT) {
|
||||
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!");
|
||||
ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
|
||||
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n"
|
||||
" See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -86,7 +88,8 @@ void SafeModeComponent::mark_successful() {
|
||||
}
|
||||
|
||||
void SafeModeComponent::loop() {
|
||||
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
|
||||
if (!this->boot_successful_ &&
|
||||
(App.get_loop_component_start_time() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
|
||||
// successful boot, reset counter
|
||||
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
|
||||
this->mark_successful();
|
||||
|
||||
@@ -112,6 +112,8 @@ class BSDSocketImpl {
|
||||
int setblocking(bool blocking);
|
||||
int loop() { return 0; }
|
||||
|
||||
/// Check if the socket has buffered data ready to read.
|
||||
/// See the ready() contract in socket.h — callers must drain or track remaining data.
|
||||
bool ready() const;
|
||||
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
@@ -96,6 +96,8 @@ class LWIPRawImpl : public LWIPRawCommon {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
// Check if the socket has buffered data ready to read.
|
||||
// See the ready() contract in socket.h — callers must drain or track remaining data.
|
||||
// Intentionally unlocked — this is a polling check called every loop iteration.
|
||||
// A stale read at worst delays processing by one loop tick; the actual I/O in
|
||||
// read() holds the lwip lock and re-checks properly. See esphome#10681.
|
||||
|
||||
@@ -78,6 +78,8 @@ class LwIPSocketImpl {
|
||||
int setblocking(bool blocking);
|
||||
int loop() { return 0; }
|
||||
|
||||
/// Check if the socket has buffered data ready to read.
|
||||
/// See the ready() contract in socket.h — callers must drain or track remaining data.
|
||||
bool ready() const;
|
||||
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
@@ -53,6 +53,19 @@ bool socket_ready_fd(int fd, bool loop_monitored);
|
||||
|
||||
// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd
|
||||
// declared above, while the impl headers are included before those declarations.
|
||||
//
|
||||
// Contract (applies to ALL socket implementations — each platform implements
|
||||
// ready() differently, but this contract holds regardless of the mechanism):
|
||||
// ready() checks if the socket has buffered data ready to read. When it returns
|
||||
// true, the caller MUST read until it would block (EAGAIN/EWOULDBLOCK), or until
|
||||
// read() returns 0 to indicate EOF / connection closed, or track that it stopped
|
||||
// early and retry without calling ready(). The next call to ready() will only
|
||||
// report new data correctly if all callers fulfill this contract. Failing to
|
||||
// drain the socket may cause ready() to return false while data remains readable.
|
||||
//
|
||||
// In practice each socket is owned by a single component, so this contract is
|
||||
// straightforward to fulfill — but the owning component must be aware of it,
|
||||
// especially if it limits how many messages it processes per loop iteration.
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
inline bool Socket::ready() const {
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
|
||||
@@ -104,11 +104,17 @@ void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
|
||||
void IRAM_ATTR SX126x::gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void SX126x::setup() {
|
||||
// setup pins
|
||||
this->busy_pin_->setup();
|
||||
this->rst_pin_->setup();
|
||||
this->dio1_pin_->setup();
|
||||
if (this->dio1_pin_->is_internal()) {
|
||||
static_cast<InternalGPIOPin *>(this->dio1_pin_)
|
||||
->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
|
||||
// start spi
|
||||
this->spi_setup();
|
||||
@@ -348,6 +354,9 @@ void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo
|
||||
}
|
||||
|
||||
void SX126x::loop() {
|
||||
if (this->dio1_pin_->is_internal()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
if (!this->dio1_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "sx126x_reg.h"
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -100,6 +101,7 @@ class SX126x : public Component,
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(SX126x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_packet_params_(uint8_t payload_length);
|
||||
|
||||
@@ -53,6 +53,8 @@ void SX127x::write_fifo_(const std::vector<uint8_t> &packet) {
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void IRAM_ATTR SX127x::gpio_intr(SX127x *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void SX127x::setup() {
|
||||
// setup reset
|
||||
this->rst_pin_->setup();
|
||||
@@ -60,6 +62,7 @@ void SX127x::setup() {
|
||||
// setup dio0
|
||||
if (this->dio0_pin_) {
|
||||
this->dio0_pin_->setup();
|
||||
this->dio0_pin_->attach_interrupt(&SX127x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
|
||||
// start spi
|
||||
@@ -313,6 +316,7 @@ void SX127x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo
|
||||
}
|
||||
|
||||
void SX127x::loop() {
|
||||
this->disable_loop();
|
||||
if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
@@ -383,7 +387,7 @@ void SX127x::set_mode_(uint8_t modulation, uint8_t mode) {
|
||||
if (millis() - start > 20) {
|
||||
ESP_LOGE(TAG, "Set mode failure");
|
||||
this->mark_failed();
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
@@ -86,6 +87,7 @@ class SX127x : public Component,
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(SX127x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_mode_(uint8_t modulation, uint8_t mode);
|
||||
|
||||
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
@@ -27,6 +28,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(TCA9555Component),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -38,6 +40,8 @@ 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)
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
def validate_mode(value):
|
||||
|
||||
@@ -24,9 +24,18 @@ void TCA9555Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&TCA9555Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
this->disable_loop();
|
||||
}
|
||||
void IRAM_ATTR TCA9555Component::gpio_intr(TCA9555Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void TCA9555Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "TCA9555:");
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
@@ -36,6 +45,9 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
if (flags == gpio::FLAG_INPUT) {
|
||||
// Set mode mask bit
|
||||
this->mode_mask_ |= 1 << pin;
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
// Clear mode mask bit
|
||||
this->mode_mask_ &= ~(1 << pin);
|
||||
@@ -43,7 +55,12 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
// Write GPIO to enable input mode
|
||||
this->write_gpio_modes_();
|
||||
}
|
||||
void TCA9555Component::loop() { this->reset_pin_cache_(); }
|
||||
void TCA9555Component::loop() {
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
bool TCA9555Component::read_gpio_outputs_() {
|
||||
if (this->is_failed())
|
||||
|
||||
@@ -24,7 +24,10 @@ class TCA9555Component : public Component,
|
||||
|
||||
void loop() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(TCA9555Component *arg);
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
@@ -39,6 +42,8 @@ class TCA9555Component : public Component,
|
||||
bool read_gpio_modes_();
|
||||
bool write_gpio_modes_();
|
||||
bool read_gpio_outputs_();
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
/// Helper class to expose a TCA9555 pin as an internal input GPIO pin.
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.4.0b1"
|
||||
__version__ = "2026.5.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -450,6 +450,99 @@ void Application::enable_pending_loops_() {
|
||||
}
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
std::atomic<uint32_t> Application::fast_select_scan_total_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_found_data_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_load_bearing_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_load_bearing_race_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_load_bearing_micro_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_load_bearing_stall_{0};
|
||||
|
||||
void Application::log_fast_select_scan_stats_() {
|
||||
uint32_t total = fast_select_scan_total_.load(std::memory_order_relaxed);
|
||||
uint32_t found = fast_select_scan_found_data_.load(std::memory_order_relaxed);
|
||||
uint32_t load_bearing = fast_select_scan_load_bearing_.load(std::memory_order_relaxed);
|
||||
uint32_t lb_race = fast_select_scan_load_bearing_race_.load(std::memory_order_relaxed);
|
||||
uint32_t lb_micro = fast_select_scan_load_bearing_micro_.load(std::memory_order_relaxed);
|
||||
uint32_t lb_stall = fast_select_scan_load_bearing_stall_.load(std::memory_order_relaxed);
|
||||
ESP_LOGD(TAG,
|
||||
"fast_select scan: total=%" PRIu32 " found_data=%" PRIu32 " load_bearing=%" PRIu32 " (race<10us=%" PRIu32
|
||||
" micro<100us=%" PRIu32 " stall>100us=%" PRIu32 ")",
|
||||
total, found, load_bearing, lb_race, lb_micro, lb_stall);
|
||||
}
|
||||
|
||||
void Application::note_fast_select_load_bearing_(struct lwip_sock *sock, uint32_t delay_ms) {
|
||||
uint32_t load_bearing = fast_select_scan_load_bearing_.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
|
||||
// Spin-poll the task notification value for a short bounded window to measure how long
|
||||
// the counterfactual ulTaskNotifyTake would actually have blocked. This distinguishes
|
||||
// three cases:
|
||||
// race (<10µs) — notification arrived within ~10µs of scan start: callback-ordering
|
||||
// race between the lwip event_callback writing rcvevent and calling
|
||||
// xTaskNotifyGive a few instructions later. Scan is noise.
|
||||
// micro (<100µs) — notification arrived within 100µs: still noise at loop_interval scale.
|
||||
// stall (≥100µs) — notification did not arrive within our polling window. This is the
|
||||
// only case where the scan could be rescuing a real latency spike.
|
||||
// Cap the spin at 100µs so that if we're wrong and this IS a real stall, we only add
|
||||
// 100µs of extra work to that one unlucky loop iteration.
|
||||
uint32_t t_start = micros();
|
||||
uint32_t gap_us = UINT32_MAX;
|
||||
while (true) {
|
||||
if (ulTaskNotifyValueClear(nullptr, 0) != 0) {
|
||||
gap_us = micros() - t_start;
|
||||
break;
|
||||
}
|
||||
uint32_t elapsed = micros() - t_start;
|
||||
if (elapsed >= 100) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char *bucket;
|
||||
if (gap_us == UINT32_MAX) {
|
||||
fast_select_scan_load_bearing_stall_.fetch_add(1, std::memory_order_relaxed);
|
||||
bucket = "STALL";
|
||||
} else if (gap_us < 10) {
|
||||
fast_select_scan_load_bearing_race_.fetch_add(1, std::memory_order_relaxed);
|
||||
bucket = "race";
|
||||
} else {
|
||||
fast_select_scan_load_bearing_micro_.fetch_add(1, std::memory_order_relaxed);
|
||||
bucket = "micro";
|
||||
}
|
||||
|
||||
// Find the socket's index in monitored_sockets_ for easier correlation with registration order.
|
||||
int index = -1;
|
||||
for (size_t i = 0; i < this->monitored_sockets_.size(); i++) {
|
||||
if (this->monitored_sockets_[i] == sock) {
|
||||
index = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Read the rcvevent value directly. This is the same offset-based read used by
|
||||
// esphome_lwip_socket_has_data(); value > 0 means unread data is queued.
|
||||
int16_t rcvevent =
|
||||
*reinterpret_cast<volatile int16_t *>(reinterpret_cast<char *>(sock) + ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET);
|
||||
// Count how many other sockets also had data at this scan (could reveal whether it's always
|
||||
// the same socket or a burst across multiple).
|
||||
size_t sockets_with_data = 0;
|
||||
for (struct lwip_sock *s : this->monitored_sockets_) {
|
||||
if (esphome_lwip_socket_has_data(s))
|
||||
sockets_with_data++;
|
||||
}
|
||||
if (gap_us == UINT32_MAX) {
|
||||
ESP_LOGW(TAG,
|
||||
"fast_select LOAD-BEARING #%" PRIu32 " [%s]: sock=%p idx=%d/%u rcvevent=%d delay_ms=%" PRIu32
|
||||
" sockets_with_data=%u gap_us=>100",
|
||||
load_bearing, bucket, sock, index, static_cast<unsigned>(this->monitored_sockets_.size()), rcvevent,
|
||||
delay_ms, static_cast<unsigned>(sockets_with_data));
|
||||
} else {
|
||||
ESP_LOGW(TAG,
|
||||
"fast_select LOAD-BEARING #%" PRIu32 " [%s]: sock=%p idx=%d/%u rcvevent=%d delay_ms=%" PRIu32
|
||||
" sockets_with_data=%u gap_us=%" PRIu32,
|
||||
load_bearing, bucket, sock, index, static_cast<unsigned>(this->monitored_sockets_.size()), rcvevent,
|
||||
delay_ms, static_cast<unsigned>(sockets_with_data), gap_us);
|
||||
}
|
||||
}
|
||||
|
||||
bool Application::register_socket(struct lwip_sock *sock) {
|
||||
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
|
||||
if (sock == nullptr)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <ctime>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
@@ -655,6 +656,25 @@ class Application {
|
||||
FixedVector<Component *> looping_components_{};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read
|
||||
// Stats to verify whether the pre-sleep socket scan in yield_with_select_() is ever load-bearing.
|
||||
// If fast_select_scan_load_bearing_ stays 0 under real workloads, the scan can be removed.
|
||||
// These are static because yield_with_select_() is inlined at every call site.
|
||||
static std::atomic<uint32_t> fast_select_scan_total_;
|
||||
static std::atomic<uint32_t> fast_select_scan_found_data_;
|
||||
// Umbrella counter: pre-scan notify peek was 0 and scan found data.
|
||||
// Broken down into three buckets based on the post-scan spin-poll result:
|
||||
// _race_ — notify arrived in < 10µs (callback-ordering race, scan is noise)
|
||||
// _micro_ — notify arrived in 10..100µs (still noise at loop_interval scale)
|
||||
// _stall_ — notify did not arrive within 100µs (the only case that could be a real stall)
|
||||
// If _stall_ stays 0, the scan is provably irrelevant under this workload.
|
||||
static std::atomic<uint32_t> fast_select_scan_load_bearing_;
|
||||
static std::atomic<uint32_t> fast_select_scan_load_bearing_race_;
|
||||
static std::atomic<uint32_t> fast_select_scan_load_bearing_micro_;
|
||||
static std::atomic<uint32_t> fast_select_scan_load_bearing_stall_;
|
||||
uint32_t fast_select_scan_stats_last_log_{0};
|
||||
void log_fast_select_scan_stats_();
|
||||
// Non-inline, called only on the rare load-bearing event so the hot path stays unchanged.
|
||||
void note_fast_select_load_bearing_(struct lwip_sock *sock, uint32_t delay_ms);
|
||||
#elif defined(USE_HOST)
|
||||
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
|
||||
#endif
|
||||
@@ -889,6 +909,14 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
this->yield_with_select_(delay_time);
|
||||
this->last_loop_ = last_op_end_time;
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Periodic fast-select scan stats (debug). Remove once the scan is proven unneeded.
|
||||
if (last_op_end_time - this->fast_select_scan_stats_last_log_ >= 30000) {
|
||||
this->fast_select_scan_stats_last_log_ = last_op_end_time;
|
||||
this->log_fast_select_scan_stats_();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->dump_config_at_ < this->components_.size()) {
|
||||
this->process_dump_config_();
|
||||
}
|
||||
@@ -909,8 +937,30 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay
|
||||
// If a socket still has unread data (rcvevent > 0) but the task notification was already
|
||||
// consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency.
|
||||
// This scan preserves select() semantics: return immediately when any fd is ready.
|
||||
//
|
||||
// Debug stats: peek the task notification value BEFORE scanning. This answers the
|
||||
// counterfactual "if the scan did not exist and we called ulTaskNotifyTake right now,
|
||||
// would it stall?". ulTaskNotifyValueClear(nullptr, 0) is a pure read — it returns the
|
||||
// current value and clears zero bits, leaving the notification state untouched. Reading
|
||||
// before the loop (rather than after finding data) makes the answer TOCTOU-free: the
|
||||
// value we compare against is the value at the moment Take would have been called.
|
||||
// LibreTiny's FreeRTOS port predates ulTaskNotifyValueClear (added in FreeRTOS 10.4.0),
|
||||
// so we fall back to a pessimistic 0, which makes load_bearing an upper bound == found_data
|
||||
// on that platform. Zero there is still a valid proof that the scan is unused.
|
||||
#ifdef USE_ESP32
|
||||
uint32_t fast_select_notify_value_before_scan = ulTaskNotifyValueClear(nullptr, 0);
|
||||
#else
|
||||
uint32_t fast_select_notify_value_before_scan = 0;
|
||||
#endif
|
||||
fast_select_scan_total_.fetch_add(1, std::memory_order_relaxed);
|
||||
for (struct lwip_sock *sock : this->monitored_sockets_) {
|
||||
if (esphome_lwip_socket_has_data(sock)) {
|
||||
fast_select_scan_found_data_.fetch_add(1, std::memory_order_relaxed);
|
||||
if (fast_select_notify_value_before_scan == 0) {
|
||||
// Scan was load-bearing: no notification pending, so Take would have stalled.
|
||||
// Delegate to a non-inline helper so the hot path stays the same size.
|
||||
this->note_fast_select_load_bearing_(sock, delay_ms);
|
||||
}
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,14 +12,14 @@ platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.2
|
||||
esphome-dashboard==20260408.1
|
||||
aioesphomeapi==44.12.0
|
||||
aioesphomeapi==44.13.2
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==12.2.0
|
||||
resvg-py==0.2.6
|
||||
resvg-py==0.3.0
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==2.1.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.5
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.9 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.10 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ FILE_HEADER = """// This file was automatically generated with a tool.
|
||||
# Maps enum type name (e.g. ".BluetoothDeviceRequestType") to max enum value.
|
||||
_enum_max_values: dict[str, int] = {}
|
||||
|
||||
# Populated by main() before message generation.
|
||||
# Maps message name (e.g. "BluetoothLERawAdvertisement") to its descriptor.
|
||||
_message_desc_map: dict[str, Any] = {}
|
||||
|
||||
|
||||
def indent_list(text: str, padding: str = " ") -> list[str]:
|
||||
"""Indent each line of the given text with the specified padding."""
|
||||
@@ -427,6 +431,23 @@ class TypeInfo(ABC):
|
||||
Estimated size in bytes including field ID and typical data
|
||||
"""
|
||||
|
||||
def get_max_encoded_size(self) -> int | None:
|
||||
"""Get the maximum possible encoded size in bytes for this field.
|
||||
|
||||
Returns the worst-case encoded size including field ID and maximum
|
||||
possible value encoding. Returns None if the size is unbounded
|
||||
(e.g., variable-length strings without max_data_length).
|
||||
|
||||
Used by (inline_encode) validation to ensure sub-messages fit in a
|
||||
single-byte length varint (< 128 bytes).
|
||||
"""
|
||||
return None # Unbounded by default
|
||||
|
||||
|
||||
def _varint_max_size(bits: int) -> int:
|
||||
"""Return the maximum varint encoding size for a value with the given number of bits."""
|
||||
return (max(bits, 1) + 6) // 7 # ceil(bits / 7), min 1 byte for varint(0)
|
||||
|
||||
|
||||
TYPE_INFO: dict[int, TypeInfo] = {}
|
||||
|
||||
@@ -514,8 +535,30 @@ def register_type(name: int):
|
||||
return func
|
||||
|
||||
|
||||
class FixedSizeTypeMixin:
|
||||
"""Mixin for types with a known fixed encoded size (float, double, fixed32, fixed64)."""
|
||||
|
||||
def get_max_encoded_size(self) -> int:
|
||||
return self.calculate_field_id_size() + self.get_fixed_size_bytes()
|
||||
|
||||
|
||||
class VarintTypeMixin:
|
||||
"""Mixin for varint types. Subclasses set _varint_max_bits."""
|
||||
|
||||
_varint_max_bits: int = 64 # Default to worst case
|
||||
|
||||
def get_max_encoded_size(self) -> int:
|
||||
max_val = self.max_value
|
||||
if max_val is not None:
|
||||
return self.calculate_field_id_size() + _varint_max_size(
|
||||
max_val.bit_length() if max_val > 0 else 1
|
||||
)
|
||||
return self.calculate_field_id_size() + _varint_max_size(self._varint_max_bits)
|
||||
|
||||
|
||||
@register_type(1)
|
||||
class DoubleType(TypeInfo):
|
||||
class DoubleType(FixedSizeTypeMixin, TypeInfo):
|
||||
# Unsupported but defined for completeness
|
||||
cpp_type = "double"
|
||||
default_value = "0.0"
|
||||
decode_64bit = "value.as_double()"
|
||||
@@ -541,7 +584,7 @@ class DoubleType(TypeInfo):
|
||||
|
||||
|
||||
@register_type(2)
|
||||
class FloatType(TypeInfo):
|
||||
class FloatType(FixedSizeTypeMixin, TypeInfo):
|
||||
cpp_type = "float"
|
||||
default_value = "0.0f"
|
||||
decode_32bit = "value.as_float()"
|
||||
@@ -567,8 +610,9 @@ class FloatType(TypeInfo):
|
||||
|
||||
|
||||
@register_type(3)
|
||||
class Int64Type(TypeInfo):
|
||||
class Int64Type(VarintTypeMixin, TypeInfo):
|
||||
cpp_type = "int64_t"
|
||||
_varint_max_bits = 64
|
||||
default_value = "0"
|
||||
decode_varint = "static_cast<int64_t>(value)"
|
||||
encode_func = "encode_int64"
|
||||
@@ -587,8 +631,9 @@ class Int64Type(TypeInfo):
|
||||
|
||||
|
||||
@register_type(4)
|
||||
class UInt64Type(TypeInfo):
|
||||
class UInt64Type(VarintTypeMixin, TypeInfo):
|
||||
cpp_type = "uint64_t"
|
||||
_varint_max_bits = 64
|
||||
default_value = "0"
|
||||
decode_varint = "value"
|
||||
encode_func = "encode_uint64"
|
||||
@@ -607,8 +652,9 @@ class UInt64Type(TypeInfo):
|
||||
|
||||
|
||||
@register_type(5)
|
||||
class Int32Type(TypeInfo):
|
||||
class Int32Type(VarintTypeMixin, TypeInfo):
|
||||
cpp_type = "int32_t"
|
||||
_varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf
|
||||
default_value = "0"
|
||||
decode_varint = "static_cast<int32_t>(value)"
|
||||
encode_func = "encode_int32"
|
||||
@@ -627,7 +673,7 @@ class Int32Type(TypeInfo):
|
||||
|
||||
|
||||
@register_type(6)
|
||||
class Fixed64Type(TypeInfo):
|
||||
class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
|
||||
cpp_type = "uint64_t"
|
||||
default_value = "0"
|
||||
decode_64bit = "value.as_fixed64()"
|
||||
@@ -653,7 +699,7 @@ class Fixed64Type(TypeInfo):
|
||||
|
||||
|
||||
@register_type(7)
|
||||
class Fixed32Type(TypeInfo):
|
||||
class Fixed32Type(FixedSizeTypeMixin, TypeInfo):
|
||||
cpp_type = "uint32_t"
|
||||
default_value = "0"
|
||||
decode_32bit = "value.as_fixed32()"
|
||||
@@ -689,7 +735,8 @@ class Fixed32Type(TypeInfo):
|
||||
|
||||
|
||||
@register_type(8)
|
||||
class BoolType(TypeInfo):
|
||||
class BoolType(VarintTypeMixin, TypeInfo):
|
||||
_varint_max_bits = 1
|
||||
cpp_type = "bool"
|
||||
default_value = "false"
|
||||
decode_varint = "value != 0"
|
||||
@@ -807,6 +854,16 @@ class StringType(TypeInfo):
|
||||
def get_estimated_size(self) -> int:
|
||||
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string
|
||||
|
||||
def get_max_encoded_size(self) -> int | None:
|
||||
max_len = self.max_data_length
|
||||
if max_len is not None:
|
||||
return (
|
||||
self.calculate_field_id_size()
|
||||
+ _varint_max_size(max_len.bit_length())
|
||||
+ max_len
|
||||
)
|
||||
return None # Unbounded
|
||||
|
||||
|
||||
@register_type(11)
|
||||
class MessageType(TypeInfo):
|
||||
@@ -1122,6 +1179,16 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
|
||||
def get_estimated_size(self) -> int:
|
||||
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string
|
||||
|
||||
def get_max_encoded_size(self) -> int | None:
|
||||
max_len = self.max_data_length
|
||||
if max_len is not None:
|
||||
return (
|
||||
self.calculate_field_id_size()
|
||||
+ _varint_max_size(max_len.bit_length())
|
||||
+ max_len
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class PackedBufferTypeInfo(TypeInfo):
|
||||
"""Type for packed repeated fields that expose raw buffer instead of decoding.
|
||||
@@ -1299,14 +1366,23 @@ class FixedArrayBytesType(TypeInfo):
|
||||
self.calculate_field_id_size() + 1 + 31
|
||||
) # field ID + length byte + typical 31 bytes
|
||||
|
||||
def get_max_encoded_size(self) -> int:
|
||||
# field_id + varint(array_size) + array_size
|
||||
return (
|
||||
self.calculate_field_id_size()
|
||||
+ _varint_max_size(self.array_size.bit_length())
|
||||
+ self.array_size
|
||||
)
|
||||
|
||||
@property
|
||||
def wire_type(self) -> WireType:
|
||||
return WireType.LENGTH_DELIMITED
|
||||
|
||||
|
||||
@register_type(13)
|
||||
class UInt32Type(TypeInfo):
|
||||
class UInt32Type(VarintTypeMixin, TypeInfo):
|
||||
cpp_type = "uint32_t"
|
||||
_varint_max_bits = 32
|
||||
default_value = "0"
|
||||
decode_varint = "value"
|
||||
encode_func = "encode_uint32"
|
||||
@@ -1328,7 +1404,9 @@ class UInt32Type(TypeInfo):
|
||||
|
||||
|
||||
@register_type(14)
|
||||
class EnumType(TypeInfo):
|
||||
class EnumType(VarintTypeMixin, TypeInfo):
|
||||
_varint_max_bits = 32
|
||||
|
||||
@property
|
||||
def cpp_type(self) -> str:
|
||||
return f"enums::{self._field.type_name[1:]}"
|
||||
@@ -1379,7 +1457,7 @@ class EnumType(TypeInfo):
|
||||
|
||||
|
||||
@register_type(15)
|
||||
class SFixed32Type(TypeInfo):
|
||||
class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
|
||||
cpp_type = "int32_t"
|
||||
default_value = "0"
|
||||
decode_32bit = "value.as_sfixed32()"
|
||||
@@ -1405,7 +1483,7 @@ class SFixed32Type(TypeInfo):
|
||||
|
||||
|
||||
@register_type(16)
|
||||
class SFixed64Type(TypeInfo):
|
||||
class SFixed64Type(FixedSizeTypeMixin, TypeInfo):
|
||||
cpp_type = "int64_t"
|
||||
default_value = "0"
|
||||
decode_64bit = "value.as_sfixed64()"
|
||||
@@ -1431,8 +1509,9 @@ class SFixed64Type(TypeInfo):
|
||||
|
||||
|
||||
@register_type(17)
|
||||
class SInt32Type(TypeInfo):
|
||||
class SInt32Type(VarintTypeMixin, TypeInfo):
|
||||
cpp_type = "int32_t"
|
||||
_varint_max_bits = 32 # zigzag encoding keeps it 32-bit
|
||||
default_value = "0"
|
||||
decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))"
|
||||
encode_func = "encode_sint32"
|
||||
@@ -1451,8 +1530,9 @@ class SInt32Type(TypeInfo):
|
||||
|
||||
|
||||
@register_type(18)
|
||||
class SInt64Type(TypeInfo):
|
||||
class SInt64Type(VarintTypeMixin, TypeInfo):
|
||||
cpp_type = "int64_t"
|
||||
_varint_max_bits = 64
|
||||
default_value = "0"
|
||||
decode_varint = "decode_zigzag64(value)"
|
||||
encode_func = "encode_sint64"
|
||||
@@ -1500,6 +1580,91 @@ def _generate_array_dump_content(
|
||||
return o
|
||||
|
||||
|
||||
def _is_inline_encode(sub_msg_name: str) -> bool:
|
||||
"""Check if a sub-message type has the (inline_encode) option set."""
|
||||
sub_desc = _message_desc_map.get(sub_msg_name)
|
||||
if not sub_desc:
|
||||
return False
|
||||
inline_opt = getattr(pb, "inline_encode", None)
|
||||
if inline_opt is None:
|
||||
return False
|
||||
return get_opt(sub_desc, inline_opt, False)
|
||||
|
||||
|
||||
def _generate_inline_encode_block(
|
||||
field_number: int, sub_msg_name: str, element: str
|
||||
) -> str:
|
||||
"""Generate inline encode code for a sub-message with (inline_encode) = true.
|
||||
|
||||
Instead of calling encode_sub_message (function pointer indirection),
|
||||
this inlines the sub-message's field encoding directly. Uses 1-byte
|
||||
backpatch for the length (validated to be < 128 at generation time).
|
||||
|
||||
Uses a local reference alias 'sub_msg' to avoid issues with this-> replacement
|
||||
on complex element expressions.
|
||||
|
||||
Args:
|
||||
field_number: The parent field number for this sub-message
|
||||
sub_msg_name: The sub-message type name
|
||||
element: C++ expression for the element (e.g., "it" or "this->field[i]")
|
||||
"""
|
||||
sub_desc = _message_desc_map[sub_msg_name]
|
||||
tag = (field_number << 3) | 2 # wire type 2 = LENGTH_DELIMITED
|
||||
assert tag < 128, f"inline_encode requires single-byte tag, got {tag}"
|
||||
|
||||
lines = []
|
||||
lines.append(f"auto &sub_msg = {element};")
|
||||
lines.append(f"ProtoEncode::write_raw_byte(pos, {tag});")
|
||||
lines.append("uint8_t *len_pos = pos;")
|
||||
lines.append("ProtoEncode::reserve_byte(pos);")
|
||||
|
||||
# Generate inline field encoding for each sub-message field
|
||||
for field in sub_desc.field:
|
||||
if field.options.deprecated:
|
||||
continue
|
||||
ti = create_field_type_info(field, needs_decode=False, needs_encode=True)
|
||||
encode_line = ti.encode_content
|
||||
# Replace this-> with sub_msg reference for the sub-message fields
|
||||
encode_line = encode_line.replace("this->", "sub_msg.")
|
||||
lines.extend(wrap_with_ifdef(encode_line, get_field_opt(field, pb.field_ifdef)))
|
||||
|
||||
lines.append("*len_pos = static_cast<uint8_t>(pos - len_pos - 1);")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _generate_inline_size_block(
|
||||
field_number: int, sub_msg_name: str, element: str
|
||||
) -> str:
|
||||
"""Generate inline size calculation for a sub-message with (inline_encode) = true.
|
||||
|
||||
Uses a local reference alias 'sub_msg' to avoid issues with this-> replacement
|
||||
on complex element expressions like 'this->advertisements[i]'.
|
||||
|
||||
Args:
|
||||
field_number: The parent field number for this sub-message
|
||||
sub_msg_name: The sub-message type name
|
||||
element: C++ expression for the element
|
||||
"""
|
||||
sub_desc = _message_desc_map[sub_msg_name]
|
||||
|
||||
lines = []
|
||||
lines.append(f"auto &sub_msg = {element};")
|
||||
# 1 byte tag + 1 byte length (guaranteed < 128 by validation)
|
||||
lines.append("size += 2;")
|
||||
|
||||
for field in sub_desc.field:
|
||||
if field.options.deprecated:
|
||||
continue
|
||||
ti = create_field_type_info(field, needs_decode=False, needs_encode=True)
|
||||
force = get_field_opt(field, pb.force, False)
|
||||
size_line = ti.get_size_calculation(f"sub_msg.{ti.field_name}", force)
|
||||
# Replace hardcoded this-> references (e.g., FixedArrayBytesType uses this->field_len)
|
||||
size_line = size_line.replace("this->", "sub_msg.")
|
||||
lines.extend(wrap_with_ifdef(size_line, get_field_opt(field, pb.field_ifdef)))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class FixedArrayRepeatedType(TypeInfo):
|
||||
"""Special type for fixed-size repeated fields using std::array.
|
||||
|
||||
@@ -1526,6 +1691,10 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);"
|
||||
# Repeated message elements use encode_sub_message (force=true is default)
|
||||
if isinstance(self._ti, MessageType):
|
||||
if _is_inline_encode(self._ti.cpp_type):
|
||||
return _generate_inline_encode_block(
|
||||
self.number, self._ti.cpp_type, element
|
||||
)
|
||||
return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});"
|
||||
return (
|
||||
f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);"
|
||||
@@ -1633,8 +1802,19 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
]
|
||||
return f"if ({non_zero_checks}) {{\n" + "\n".join(size_lines) + "\n}"
|
||||
|
||||
is_inline = isinstance(self._ti, MessageType) and _is_inline_encode(
|
||||
self._ti.cpp_type
|
||||
)
|
||||
|
||||
# When using a define, always use loop-based approach
|
||||
if self.is_define:
|
||||
if is_inline:
|
||||
o = f"for (const auto &it : {name}) {{\n"
|
||||
o += indent(
|
||||
_generate_inline_size_block(self.number, self._ti.cpp_type, "it")
|
||||
)
|
||||
o += "\n}"
|
||||
return o
|
||||
o = f"for (const auto &it : {name}) {{\n"
|
||||
o += f" {self._ti.get_size_calculation('it', True)}\n"
|
||||
o += "}"
|
||||
@@ -1642,6 +1822,14 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
|
||||
# For fixed arrays, we always encode all elements
|
||||
|
||||
if is_inline:
|
||||
o = f"for (const auto &it : {name}) {{\n"
|
||||
o += indent(
|
||||
_generate_inline_size_block(self.number, self._ti.cpp_type, "it")
|
||||
)
|
||||
o += "\n}"
|
||||
return o
|
||||
|
||||
# Special case for single-element arrays - no loop needed
|
||||
if self.array_size == 1:
|
||||
return self._ti.get_size_calculation(f"{name}[0]", True)
|
||||
@@ -1714,6 +1902,15 @@ class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType):
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
# Calculate size only for active elements
|
||||
if isinstance(self._ti, MessageType) and _is_inline_encode(self._ti.cpp_type):
|
||||
o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n"
|
||||
o += indent(
|
||||
_generate_inline_size_block(
|
||||
self.number, self._ti.cpp_type, f"{name}[i]"
|
||||
)
|
||||
)
|
||||
o += "\n}"
|
||||
return o
|
||||
o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n"
|
||||
o += f" {self._ti.get_size_calculation(f'{name}[i]', True)}\n"
|
||||
o += "}"
|
||||
@@ -2222,6 +2419,28 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
|
||||
return total_size
|
||||
|
||||
|
||||
def calculate_message_max_size(desc: descriptor.DescriptorProto) -> int | None:
|
||||
"""Calculate the maximum possible encoded size for a message.
|
||||
|
||||
Returns None if any field has unbounded size (e.g., variable-length strings).
|
||||
Used to validate that (inline_encode) messages fit in a single-byte length varint.
|
||||
"""
|
||||
total_size = 0
|
||||
|
||||
for field in desc.field:
|
||||
if field.options.deprecated:
|
||||
continue
|
||||
|
||||
ti = create_field_type_info(field, needs_decode=False, needs_encode=True)
|
||||
max_size = ti.get_max_encoded_size()
|
||||
if max_size is None:
|
||||
return None
|
||||
|
||||
total_size += max_size
|
||||
|
||||
return total_size
|
||||
|
||||
|
||||
def build_message_type(
|
||||
desc: descriptor.DescriptorProto,
|
||||
base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]],
|
||||
@@ -2451,11 +2670,23 @@ def build_message_type(
|
||||
prot = "void decode(const uint8_t *buffer, size_t length);"
|
||||
public_content.append(prot)
|
||||
|
||||
# Check if this message uses inline_encode — if so, skip generating standalone
|
||||
# encode/calculate_size methods since the encoding is inlined into the parent.
|
||||
inline_opt = getattr(pb, "inline_encode", None)
|
||||
is_inline_only = (
|
||||
message_id is None # Not a service message (no id)
|
||||
and inline_opt is not None
|
||||
and get_opt(desc, inline_opt, False)
|
||||
)
|
||||
|
||||
# Only generate encode method if this message needs encoding and has fields
|
||||
if needs_encode and encode:
|
||||
if needs_encode and encode and not is_inline_only:
|
||||
# Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls
|
||||
encode_debug = [
|
||||
line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,") for line in encode
|
||||
line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,").replace(
|
||||
"(pos)", "(pos PROTO_ENCODE_DEBUG_ARG)"
|
||||
)
|
||||
for line in encode
|
||||
]
|
||||
o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n"
|
||||
o += " uint8_t *__restrict__ pos = buffer.get_pos();\n"
|
||||
@@ -2470,7 +2701,7 @@ def build_message_type(
|
||||
# If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used
|
||||
|
||||
# Add calculate_size method only if this message needs encoding and has fields
|
||||
if needs_encode and size_calc:
|
||||
if needs_encode and size_calc and not is_inline_only:
|
||||
o = f"uint32_t {desc.name}::calculate_size() const {{\n"
|
||||
o += " uint32_t size = 0;\n"
|
||||
o += indent("\n".join(size_calc)) + "\n"
|
||||
@@ -2830,6 +3061,32 @@ def main() -> None:
|
||||
if not enum.options.deprecated and enum.value:
|
||||
_enum_max_values[f".{enum.name}"] = max(v.number for v in enum.value)
|
||||
|
||||
# Build message descriptor map for inline_encode lookups
|
||||
mt = file.message_type
|
||||
_message_desc_map.update({m.name: m for m in mt if not m.options.deprecated})
|
||||
|
||||
# Validate inline_encode messages fit in single-byte length varint
|
||||
inline_encode_opt = getattr(pb, "inline_encode", None)
|
||||
if inline_encode_opt is not None:
|
||||
for m in mt:
|
||||
if m.options.deprecated:
|
||||
continue
|
||||
if not get_opt(m, inline_encode_opt, False):
|
||||
continue
|
||||
max_size = calculate_message_max_size(m)
|
||||
if max_size is None:
|
||||
raise ValueError(
|
||||
f"Message '{m.name}' has (inline_encode) = true but contains "
|
||||
f"fields with unbounded size. Inline encoding requires all "
|
||||
f"fields to have bounded maximum size."
|
||||
)
|
||||
if max_size >= 128:
|
||||
raise ValueError(
|
||||
f"Message '{m.name}' has (inline_encode) = true but max "
|
||||
f"encoded size is {max_size} bytes (>= 128). Inline encoding "
|
||||
f"requires sub-messages that fit in a single-byte length varint."
|
||||
)
|
||||
|
||||
# Build dynamic ifdef mappings early so we can emit USE_API_VARINT64 before includes
|
||||
enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = (
|
||||
build_type_usage_map(file)
|
||||
@@ -3048,8 +3305,6 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
|
||||
|
||||
content += "\n} // namespace enums\n\n"
|
||||
|
||||
mt = file.message_type
|
||||
|
||||
# Identify empty SOURCE_CLIENT messages that don't need class generation
|
||||
for m in mt:
|
||||
if m.options.deprecated:
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace esphome::benchmarks {
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- random_float() ---
|
||||
// Ported from ol.yaml:148 "Random Float Benchmark"
|
||||
|
||||
static void RandomFloat(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
@@ -38,4 +37,274 @@ static void RandomUint32(benchmark::State &state) {
|
||||
}
|
||||
BENCHMARK(RandomUint32);
|
||||
|
||||
// --- format_hex_to() - 6 bytes (MAC address sized) ---
|
||||
|
||||
static void FormatHexTo_6Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
|
||||
char buffer[13]; // 6 * 2 + 1
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_to(buffer, data, 6);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexTo_6Bytes);
|
||||
|
||||
// --- format_hex_to() - 16 bytes (UUID sized) ---
|
||||
|
||||
static void FormatHexTo_16Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89,
|
||||
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10};
|
||||
char buffer[33]; // 16 * 2 + 1
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_to(buffer, data, 16);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexTo_16Bytes);
|
||||
|
||||
// --- format_hex_to() - 100 bytes (large payload) ---
|
||||
|
||||
static void FormatHexTo_100Bytes(benchmark::State &state) {
|
||||
uint8_t data[100];
|
||||
for (int i = 0; i < 100; i++) {
|
||||
data[i] = static_cast<uint8_t>(i);
|
||||
}
|
||||
char buffer[201]; // 100 * 2 + 1
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_to(buffer, data, 100);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexTo_100Bytes);
|
||||
|
||||
// --- format_hex_pretty_to() - 6 bytes with ':' separator ---
|
||||
|
||||
static void FormatHexPrettyTo_6Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
|
||||
char buffer[18]; // 6 * 3
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_pretty_to(buffer, data, 6);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexPrettyTo_6Bytes);
|
||||
|
||||
// --- format_mac_addr_upper() ---
|
||||
|
||||
static void FormatMacAddrUpper(benchmark::State &state) {
|
||||
const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
|
||||
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_mac_addr_upper(mac, buffer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatMacAddrUpper);
|
||||
|
||||
// --- fnv1_hash() - short string ---
|
||||
|
||||
static void Fnv1Hash_Short(benchmark::State &state) {
|
||||
const char *str = "sensor.temperature";
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1_hash(str);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1Hash_Short);
|
||||
|
||||
// --- fnv1_hash() - long string ---
|
||||
|
||||
static void Fnv1Hash_Long(benchmark::State &state) {
|
||||
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1_hash(str);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1Hash_Long);
|
||||
|
||||
// --- fnv1a_hash() - short string ---
|
||||
// Use DoNotOptimize on the input pointer to prevent constexpr evaluation
|
||||
|
||||
static void Fnv1aHash_Short(benchmark::State &state) {
|
||||
const char *str = "sensor.temperature";
|
||||
benchmark::DoNotOptimize(str);
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1a_hash(str);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1aHash_Short);
|
||||
|
||||
// --- fnv1a_hash() - long string ---
|
||||
|
||||
static void Fnv1aHash_Long(benchmark::State &state) {
|
||||
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
|
||||
benchmark::DoNotOptimize(str);
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1a_hash(str);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1aHash_Long);
|
||||
|
||||
// --- fnv1_hash_object_id() - typical entity name ---
|
||||
|
||||
static void Fnv1HashObjectId(benchmark::State &state) {
|
||||
char name[] = "Living Room Temperature Sensor";
|
||||
size_t len = sizeof(name) - 1;
|
||||
benchmark::DoNotOptimize(name);
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1_hash_object_id(name, len);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1HashObjectId);
|
||||
|
||||
// --- parse_hex() - 6 bytes from string ---
|
||||
|
||||
static void ParseHex_6Bytes(benchmark::State &state) {
|
||||
const char *hex_str = "ABCDEF012345";
|
||||
uint8_t data[6];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
parse_hex(hex_str, data, 6);
|
||||
}
|
||||
benchmark::DoNotOptimize(data);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ParseHex_6Bytes);
|
||||
|
||||
// --- parse_hex() - 16 bytes from string ---
|
||||
|
||||
static void ParseHex_16Bytes(benchmark::State &state) {
|
||||
const char *hex_str = "ABCDEF0123456789FEDCBA9876543210";
|
||||
uint8_t data[16];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
parse_hex(hex_str, data, 16);
|
||||
}
|
||||
benchmark::DoNotOptimize(data);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ParseHex_16Bytes);
|
||||
|
||||
// --- crc8() - 8 bytes ---
|
||||
|
||||
static void CRC8_8Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
||||
for (auto _ : state) {
|
||||
uint8_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= crc8(data, 8);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CRC8_8Bytes);
|
||||
|
||||
// --- crc16() - 8 bytes ---
|
||||
|
||||
static void CRC16_8Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
||||
for (auto _ : state) {
|
||||
uint16_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= crc16(data, 8);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CRC16_8Bytes);
|
||||
|
||||
// --- value_accuracy_to_buf() - typical sensor value ---
|
||||
|
||||
static void ValueAccuracyToBuf(benchmark::State &state) {
|
||||
char raw_buf[VALUE_ACCURACY_MAX_LEN] = {};
|
||||
std::span<char, VALUE_ACCURACY_MAX_LEN> buf(raw_buf);
|
||||
float value = 23.456f;
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
value_accuracy_to_buf(buf, value, 2);
|
||||
}
|
||||
benchmark::DoNotOptimize(raw_buf);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ValueAccuracyToBuf);
|
||||
|
||||
// --- int8_to_str() ---
|
||||
|
||||
static void Int8ToStr(benchmark::State &state) {
|
||||
char buffer[5] = {};
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
int8_to_str(buffer, static_cast<int8_t>(i & 0xFF));
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Int8ToStr);
|
||||
|
||||
// --- base64_decode() - into pre-allocated buffer ---
|
||||
|
||||
static void Base64Decode_32Bytes(benchmark::State &state) {
|
||||
// 32 bytes encoded = 44 base64 chars
|
||||
const uint8_t encoded[] = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGx0eHw==";
|
||||
size_t encoded_len = 44;
|
||||
uint8_t output[32];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
base64_decode(encoded, encoded_len, output, sizeof(output));
|
||||
}
|
||||
benchmark::DoNotOptimize(output);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Base64Decode_32Bytes);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
mcp23016:
|
||||
i2c_id: i2c_bus
|
||||
id: mcp23016_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23016_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23016_hub_int
|
||||
address: 0x21
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ pca6416a:
|
||||
- id: pca6416a_hub
|
||||
i2c_id: i2c_bus
|
||||
address: 0x21
|
||||
- id: pca6416a_hub_int
|
||||
i2c_id: i2c_bus
|
||||
address: 0x22
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ tca9555:
|
||||
- id: tca9555_hub
|
||||
i2c_id: i2c_bus
|
||||
address: 0x21
|
||||
- id: tca9555_hub_int
|
||||
i2c_id: i2c_bus
|
||||
address: 0x22
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
Reference in New Issue
Block a user