mirror of
https://github.com/esphome/esphome.git
synced 2026-06-30 04:26:07 +00:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 359c6a7265 | |||
| a031da351d | |||
| 091b6a0ba0 | |||
| cf9d97d5ae | |||
| 3e1a6b4e11 | |||
| 5c7245dfcd | |||
| 1611345c55 | |||
| b36e20d60b | |||
| 93eb6f78e0 | |||
| e308075e3f | |||
| 405607e9d2 | |||
| 797ed23765 | |||
| 8780c7e0ac | |||
| 136e343988 | |||
| b8690c8e31 | |||
| 2778c62d07 | |||
| 7984349c36 | |||
| 0cbbd64577 | |||
| a618ee11b4 | |||
| 6251c26cc6 | |||
| 4fbe0d87ec | |||
| 24d8e99c50 | |||
| 14b6a0ede1 | |||
| 1793ca5eac | |||
| 62e19bcb27 | |||
| 84d1c34c28 | |||
| f78cbf9200 | |||
| eb711381d3 | |||
| 9a1daa5247 | |||
| f3d61ca3e1 | |||
| 29dfd820c6 | |||
| 8bc5b97298 | |||
| 7a64163c4f | |||
| dfe14f9c3a | |||
| 26cf373ae7 | |||
| 94ccddf176 | |||
| 2ec24505d0 | |||
| 4f7faa7712 | |||
| b3dcaac262 | |||
| ee118d384a | |||
| 8d36167e11 | |||
| 6d559a32df | |||
| bf0d31b3ab | |||
| d8ffb732b7 | |||
| 9e8261056c | |||
| 5f311d281e | |||
| a336ad6732 | |||
| 8434d54cc7 | |||
| b62f7a41c9 | |||
| 4ebecf514a | |||
| 2f32c88ae5 | |||
| 95449068e7 | |||
| 556def78aa | |||
| b7803cf9b5 | |||
| 40820287f1 | |||
| 6210dfb4d0 | |||
| 45c712b17b | |||
| 8e23065b86 | |||
| d3892b8399 | |||
| bda789052d | |||
| 0fb100f2d1 | |||
| fd7fc6b8e8 | |||
| 690e8c3fb9 | |||
| a0742a9535 | |||
| ccc57475b7 | |||
| 24ec65e68e | |||
| 436938b931 | |||
| 063c4371de | |||
| 7ad4cbf46f | |||
| 88875daf52 | |||
| 7811781a96 | |||
| da5e11d196 | |||
| 75cdabee3d |
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
cache-key: ${{ hashFiles('.cache-key') }}
|
||||
|
||||
- name: Download memory analysis artifacts
|
||||
|
||||
+15
-15
@@ -12,8 +12,8 @@ permissions:
|
||||
contents: read # actions/checkout for all jobs; individual jobs add their own scopes when they need to write
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
PYUPGRADE_TARGET: "--py311-plus"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
PYUPGRADE_TARGET: "--py312-plus"
|
||||
|
||||
concurrency:
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
os:
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -365,7 +365,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -509,14 +509,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -820,7 +820,7 @@ jobs:
|
||||
run: echo ${{ matrix.components }}
|
||||
|
||||
- name: Cache apt packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
@@ -1098,7 +1098,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -1122,7 +1122,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -1164,7 +1164,7 @@ jobs:
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -1211,7 +1211,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
@@ -40,7 +40,7 @@ repos:
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py311-plus]
|
||||
args: [--py312-plus]
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.37.1
|
||||
hooks:
|
||||
|
||||
@@ -9,7 +9,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
## 2. Core Technologies & Stack
|
||||
|
||||
* **Languages:** Python (>=3.11), C++ (gnu++20)
|
||||
* **Languages:** Python (>=3.12), C++ (gnu++20)
|
||||
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
|
||||
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
|
||||
* **Configuration:** YAML.
|
||||
@@ -709,3 +709,9 @@ This document provides essential context for AI models interacting with this pro
|
||||
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
|
||||
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
|
||||
```
|
||||
## 9. English Language
|
||||
|
||||
The project uses English for non-code content. When drafting documentation, code comments, commit messages,
|
||||
PR descriptions, and similar text, avoid technical jargon. Instead, express concepts in plain English,
|
||||
using standard technical terms only when required. Ensure the text is readily comprehensible to a wide
|
||||
audience, including non-native English speakers.
|
||||
|
||||
@@ -123,6 +123,7 @@ esphome/components/cs5460a/* @balrog-kun
|
||||
esphome/components/cse7761/* @berfenger
|
||||
esphome/components/cst226/* @clydebarrow
|
||||
esphome/components/cst816/* @clydebarrow
|
||||
esphome/components/cst9220/* @clydebarrow
|
||||
esphome/components/ct_clamp/* @jesserockz
|
||||
esphome/components/current_based/* @djwmarcx
|
||||
esphome/components/dac7678/* @NickB1
|
||||
@@ -266,6 +267,7 @@ esphome/components/integration/* @OttoWinter
|
||||
esphome/components/internal_temperature/* @Mat931
|
||||
esphome/components/interval/* @esphome/core
|
||||
esphome/components/ir_rf_proxy/* @kbx81
|
||||
esphome/components/it8951/* @koosoli @limengdu @Passific
|
||||
esphome/components/jsn_sr04t/* @Mafus1
|
||||
esphome/components/json/* @esphome/core
|
||||
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
||||
@@ -385,6 +387,7 @@ esphome/components/pcm5122/* @remcom
|
||||
esphome/components/pi4ioe5v6408/* @jesserockz
|
||||
esphome/components/pid/* @OttoWinter
|
||||
esphome/components/pipsolar/* @andreashergert1984
|
||||
esphome/components/pixoo/* @jesserockz
|
||||
esphome/components/pm1006/* @habbie
|
||||
esphome/components/pm2005/* @andrewjswan
|
||||
esphome/components/pmsa003i/* @sjtrny
|
||||
@@ -404,6 +407,7 @@ esphome/components/psram/* @esphome/core
|
||||
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
|
||||
esphome/components/pvvx_mithermometer/* @pasiz
|
||||
esphome/components/pylontech/* @functionpointer
|
||||
esphome/components/qmi8658/* @clydebarrow
|
||||
esphome/components/qmp6988/* @andrewpc
|
||||
esphome/components/qr_code/* @wjtje
|
||||
esphome/components/qspi_dbi/* @clydebarrow
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.19
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.22
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -2,6 +2,6 @@ esphome:
|
||||
name: docker-test-ln882x-arduino
|
||||
|
||||
ln882x:
|
||||
board: generic-ln882hki
|
||||
board: generic-ln882h
|
||||
|
||||
logger:
|
||||
|
||||
+22
-5
@@ -1488,12 +1488,29 @@ _LEGACY_REDACTION_REMOVAL = "2026.12.0"
|
||||
|
||||
def _redact_with_legacy_fallback(output: str) -> str:
|
||||
unmarked: set[str] = set()
|
||||
# Track the top-level ``substitutions:`` block. Its keys are arbitrary
|
||||
# user-chosen names with no schema validator, so the ``cv.sensitive(...)``
|
||||
# migration named in the warning can't be applied to them. Their values are
|
||||
# still redacted, but emitting the (unactionable) deprecation warning would
|
||||
# only confuse users.
|
||||
in_substitutions = False
|
||||
|
||||
def _replace(m: re.Match[str]) -> str:
|
||||
unmarked.add(m.group("key"))
|
||||
return f"{m.group('key')}: \\033[8m{m.group('val')}\\033[28m"
|
||||
|
||||
output = _LEGACY_REDACTION_RE.sub(_replace, output)
|
||||
lines = output.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
# A non-indented, non-blank line is a top-level key that opens or
|
||||
# closes the substitutions block.
|
||||
if line and not line[0].isspace():
|
||||
in_substitutions = line.startswith(f"{CONF_SUBSTITUTIONS}:")
|
||||
m = _LEGACY_REDACTION_RE.search(line)
|
||||
if m is None:
|
||||
continue
|
||||
if not in_substitutions:
|
||||
unmarked.add(m.group("key"))
|
||||
lines[i] = (
|
||||
f"{line[: m.start()]}{m.group('key')}: "
|
||||
f"\\033[8m{m.group('val')}\\033[28m{line[m.end() :]}"
|
||||
)
|
||||
output = "\n".join(lines)
|
||||
for key in sorted(unmarked):
|
||||
_LOGGER.warning(
|
||||
"Field '%s' is being redacted by a legacy substring heuristic. "
|
||||
|
||||
@@ -12,12 +12,9 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
import threading
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class AsyncThreadRunner(threading.Thread, Generic[_T]):
|
||||
class AsyncThreadRunner[T](threading.Thread):
|
||||
"""Run an async coroutine in a daemon thread and expose its result.
|
||||
|
||||
The runner catches all exceptions from the coroutine and stores them in
|
||||
@@ -35,10 +32,10 @@ class AsyncThreadRunner(threading.Thread, Generic[_T]):
|
||||
result = runner.result
|
||||
"""
|
||||
|
||||
def __init__(self, coro_factory: Callable[[], Awaitable[_T]]) -> None:
|
||||
def __init__(self, coro_factory: Callable[[], Awaitable[T]]) -> None:
|
||||
super().__init__(daemon=True)
|
||||
self._coro_factory = coro_factory
|
||||
self.result: _T | None = None
|
||||
self.result: T | None = None
|
||||
self.exception: BaseException | None = None
|
||||
self.event = threading.Event()
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ void A01nyubComponent::check_buffer_() {
|
||||
if (this->buffer_[3] == checksum) {
|
||||
float distance = (this->buffer_[1] << 8) + this->buffer_[2];
|
||||
if (distance > 280) {
|
||||
float meters = distance / 1000.0;
|
||||
float meters = distance / 1000.0f;
|
||||
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
|
||||
this->publish_state(meters);
|
||||
} else {
|
||||
|
||||
@@ -216,7 +216,7 @@ void AcDimmer::setup() {
|
||||
}
|
||||
|
||||
void AcDimmer::write_state(float state) {
|
||||
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
|
||||
state = std::acos(1 - (2 * state)) / std::numbers::pi_v<float>; // RMS power compensation
|
||||
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
|
||||
if (new_value != 0 && this->store_.value == 0)
|
||||
this->store_.init_cycle = this->init_with_half_cycle_;
|
||||
|
||||
@@ -114,13 +114,13 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
this->decoder_->decode(param->notify.value, param->notify.value_len);
|
||||
|
||||
if (this->decoder_->has_position()) {
|
||||
this->position = ((float) this->decoder_->position_ / 100.0);
|
||||
this->position = ((float) this->decoder_->position_ / 100.0f);
|
||||
if (!this->invert_position_)
|
||||
this->position = 1 - this->position;
|
||||
if (this->position > 0.97)
|
||||
this->position = 1.0;
|
||||
if (this->position < 0.02)
|
||||
this->position = 0.0;
|
||||
if (this->position > 0.97f)
|
||||
this->position = 1.0f;
|
||||
if (this->position < 0.02f)
|
||||
this->position = 0.0f;
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
namespace esphome::anova {
|
||||
|
||||
float ftoc(float f) { return (f - 32.0) * (5.0f / 9.0f); }
|
||||
float ftoc(float f) { return (f - 32.0f) * (5.0f / 9.0f); }
|
||||
|
||||
float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0; }
|
||||
float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0f; }
|
||||
|
||||
AnovaPacket *AnovaCodec::clean_packet_() {
|
||||
this->packet_.length = strlen((char *) this->packet_.data);
|
||||
|
||||
@@ -305,6 +305,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
rtl87xx=4, # Moderate RAM, BSD-style sockets
|
||||
host=4, # Abundant resources
|
||||
ln882x=4, # Moderate RAM
|
||||
nrf52=4, # ~256KB RAM, BSD sockets
|
||||
): cv.int_range(min=1, max=10),
|
||||
cv.SplitDefault(
|
||||
CONF_MAX_CONNECTIONS,
|
||||
@@ -315,6 +316,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
rtl87xx=5, # Moderate RAM
|
||||
host=8, # Abundant resources
|
||||
ln882x=5, # Moderate RAM
|
||||
nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller)
|
||||
): cv.int_range(min=1, max=20),
|
||||
# Maximum queued send buffers per connection before dropping connection
|
||||
# Each buffer uses ~8-12 bytes overhead plus actual message size
|
||||
|
||||
@@ -31,6 +31,13 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#if defined(LOG_LEVEL_NONE)
|
||||
// Zephyr defines LOG_LEVEL_NONE as a logging macro that collides with the LogLevel enum value of
|
||||
// the same name in the generated api_pb2.h. Undefine it for the rest of this translation unit so
|
||||
// the enum parses; nothing below needs Zephyr's logging macro.
|
||||
#undef LOG_LEVEL_NONE
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// This file only provides includes, no actual code
|
||||
|
||||
@@ -395,7 +395,7 @@ async def to_code(config):
|
||||
)
|
||||
if data.mp3_support:
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-mp3", ref="0.3.0")
|
||||
add_idf_component(name="esphome/micro-mp3", ref="0.4.0")
|
||||
_emit_memory_pair(
|
||||
data.mp3.buffer_memory,
|
||||
"CONFIG_MICRO_MP3_PREFER_PSRAM",
|
||||
|
||||
@@ -112,7 +112,7 @@ float BinarySensorMap::bayesian_predicate_(bool sensor_state, float prior, float
|
||||
prob_state_source_false = 1 - prob_given_false;
|
||||
}
|
||||
|
||||
return prob_state_source_true / (prior * prob_state_source_true + (1.0 - prior) * prob_state_source_false);
|
||||
return prob_state_source_true / (prior * prob_state_source_true + (1.0f - prior) * prob_state_source_false);
|
||||
}
|
||||
|
||||
void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float value) {
|
||||
|
||||
+1127
-1051
File diff suppressed because it is too large
Load Diff
@@ -205,7 +205,7 @@ void BL0906::read_data_(const uint8_t address, const float reference, sensor::Se
|
||||
// Chip temperature
|
||||
if (reference == BL0906_TREF) {
|
||||
value = (float) to_int32_t(data_s24);
|
||||
value = (value - 64) * 12.5 / 59 - 40;
|
||||
value = (value - 64) * 12.5f / 59 - 40;
|
||||
}
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ float BL0940::calculate_power_reference_() {
|
||||
float BL0940::calculate_energy_reference_() {
|
||||
// formula: 3600000 * 4046 * RL * R1 * 1000 / (1638.4 * 256) / Vref² / (R1 + R2)
|
||||
// or: power_reference_ * 3600000 / (1638.4 * 256)
|
||||
return this->power_reference_cal_ * 3600000 / (1638.4 * 256);
|
||||
return this->power_reference_cal_ * 3600000 / (1638.4f * 256);
|
||||
}
|
||||
|
||||
float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; }
|
||||
|
||||
@@ -124,14 +124,14 @@ void BL0942::setup() {
|
||||
// If either current or voltage references are set explicitly by the user,
|
||||
// calculate the power reference from it unless that is also explicitly set.
|
||||
if ((this->current_reference_set_ || this->voltage_reference_set_) && !this->power_reference_set_) {
|
||||
this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0 / 305978.0) / 73989.0;
|
||||
this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0f / 305978.0f) / 73989.0f;
|
||||
this->power_reference_set_ = true;
|
||||
}
|
||||
|
||||
// Similarly for energy reference, if the power reference was set by the user
|
||||
// either implicitly or explicitly.
|
||||
if (this->power_reference_set_ && !this->energy_reference_set_) {
|
||||
this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4;
|
||||
this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4f;
|
||||
this->energy_reference_set_ = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ void MedianCombinationComponent::handle_new_value(float value) {
|
||||
median = sensor_states[sensor_states_size / 2];
|
||||
} else {
|
||||
// Even number of measurements, use the average of the two middle measurements
|
||||
median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0;
|
||||
median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
cst9220_ns = cg.esphome_ns.namespace("cst9220")
|
||||
@@ -0,0 +1,36 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
|
||||
from .. import cst9220_ns
|
||||
|
||||
CST9220Touchscreen = cst9220_ns.class_(
|
||||
"CST9220Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
touchscreen.touchscreen_schema("100ms")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CST9220Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x5A))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
if reset_pin := config.get(CONF_RESET_PIN):
|
||||
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "cst9220_touchscreen.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::cst9220 {
|
||||
|
||||
void CST9220Touchscreen::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
// Wait for the controller to leave its bootloader before talking to it.
|
||||
this->set_timeout(30, [this] { this->continue_setup_(); });
|
||||
}
|
||||
|
||||
void CST9220Touchscreen::continue_setup_() {
|
||||
uint8_t buffer[4];
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
|
||||
// Enter command mode so the configuration registers can be read.
|
||||
if (this->write_register16(REG_CMD_MODE, buffer, 0) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to enter command mode"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(10);
|
||||
|
||||
// The firmware check code confirms that valid firmware is loaded.
|
||||
if (this->read_register16(REG_CHECKCODE, buffer, 4) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to read check code"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint32_t checkcode = encode_uint32(buffer[3], buffer[2], buffer[1], buffer[0]);
|
||||
if ((checkcode & 0xFFFF0000) != 0xCACA0000) {
|
||||
ESP_LOGE(TAG, "Invalid firmware check code: 0x%08" PRIX32, checkcode);
|
||||
this->status_set_error(LOG_STR("Invalid firmware check code"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the panel resolution unless the user supplied calibration values.
|
||||
if (this->read_register16(REG_RESOLUTION, buffer, 4) == i2c::ERROR_OK) {
|
||||
if (this->x_raw_max_ == this->x_raw_min_)
|
||||
this->x_raw_max_ = encode_uint16(buffer[1], buffer[0]);
|
||||
if (this->y_raw_max_ == this->y_raw_min_)
|
||||
this->y_raw_max_ = encode_uint16(buffer[3], buffer[2]);
|
||||
}
|
||||
|
||||
// Read the chip type and project id and validate the controller.
|
||||
if (this->read_register16(REG_CHIP_INFO, buffer, 4) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to read chip ID"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->chip_id_ = encode_uint16(buffer[3], buffer[2]);
|
||||
this->project_id_ = encode_uint16(buffer[1], buffer[0]);
|
||||
if (this->chip_id_ != CST9220_CHIP_ID && this->chip_id_ != CST9217_CHIP_ID) {
|
||||
ESP_LOGE(TAG, "Unknown chip ID: 0x%04X", this->chip_id_);
|
||||
this->status_set_error(LOG_STR("Unknown chip ID"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to the display dimensions if the resolution read failed.
|
||||
if (this->x_raw_max_ == this->x_raw_min_)
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
if (this->y_raw_max_ == this->y_raw_min_)
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
|
||||
this->setup_complete_ = true;
|
||||
}
|
||||
|
||||
void CST9220Touchscreen::update_touches() {
|
||||
if (!this->setup_complete_)
|
||||
return;
|
||||
uint8_t data[CST9220_DATA_LENGTH];
|
||||
// Only an actual I2C failure should skip the update; a successful read with no
|
||||
// touches is a real "all fingers lifted" state that must flow through so the
|
||||
// base class can generate the release event.
|
||||
if (this->read_register16(REG_TOUCH_DATA, data, sizeof(data)) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
this->skip_update_ = true;
|
||||
return;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
|
||||
// Acknowledge the report so the controller can prepare the next one.
|
||||
uint8_t ack = TOUCH_ACK;
|
||||
this->write_register16(REG_TOUCH_DATA, &ack, 1);
|
||||
|
||||
// A valid report carries the ACK marker at offset 6; offset 0 holds the first
|
||||
// point and must be neither the ACK marker nor empty. Anything else means no
|
||||
// valid touch data this cycle, which we report as zero touches (not a skip).
|
||||
if (data[0] == TOUCH_ACK || data[0] == 0x00 || data[6] != TOUCH_ACK)
|
||||
return;
|
||||
|
||||
uint8_t num_touches = data[5] & 0x7F;
|
||||
if (num_touches > CST9220_MAX_TOUCHES)
|
||||
num_touches = CST9220_MAX_TOUCHES;
|
||||
|
||||
for (uint8_t i = 0; i < num_touches; i++) {
|
||||
// The first point starts at offset 0; subsequent points are offset by the
|
||||
// two status bytes that follow it.
|
||||
const uint8_t *p = data + i * 5 + (i == 0 ? 0 : 2);
|
||||
uint8_t id = p[0] >> 4;
|
||||
uint8_t event = p[0] & 0x0F;
|
||||
if (event != TOUCH_EVENT_DOWN)
|
||||
continue;
|
||||
// p[3] is shared: high nibble holds the X LSBs, low nibble the Y LSBs.
|
||||
uint16_t x = (p[1] << 4) | (p[3] >> 4);
|
||||
uint16_t y = (p[2] << 4) | (p[3] & 0x0F);
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
|
||||
this->add_raw_touch_position_(id, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void CST9220Touchscreen::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"CST9220 Touchscreen:\n"
|
||||
" Chip ID: 0x%04X\n"
|
||||
" Project ID: 0x%04X\n"
|
||||
" X Raw Min: %d, X Raw Max: %d\n"
|
||||
" Y Raw Min: %d, Y Raw Max: %d",
|
||||
this->chip_id_, this->project_id_, this->x_raw_min_, this->x_raw_max_, this->y_raw_min_,
|
||||
this->y_raw_max_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
}
|
||||
|
||||
} // namespace esphome::cst9220
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::cst9220 {
|
||||
|
||||
static const char *const TAG = "cst9220.touchscreen";
|
||||
|
||||
// The CST92xx family uses 16-bit (big-endian) register addresses.
|
||||
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
|
||||
static const uint16_t REG_CMD_MODE = 0xD101; // enter command mode
|
||||
static const uint16_t REG_CHECKCODE = 0xD1FC; // firmware check code
|
||||
static const uint16_t REG_RESOLUTION = 0xD1F8; // panel resolution
|
||||
static const uint16_t REG_CHIP_INFO = 0xD204; // chip type + project id
|
||||
|
||||
static const uint8_t TOUCH_ACK = 0xAB;
|
||||
static const uint8_t TOUCH_EVENT_DOWN = 0x06;
|
||||
|
||||
static const uint16_t CST9220_CHIP_ID = 0x9220;
|
||||
static const uint16_t CST9217_CHIP_ID = 0x9217;
|
||||
|
||||
// Maximum simultaneous touch points reported by the family.
|
||||
static const uint8_t CST9220_MAX_TOUCHES = 5;
|
||||
// Report layout: 5 bytes per touch point plus 5 bytes of status/ack overhead.
|
||||
static const size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
|
||||
|
||||
class CST9220Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
void update_touches() override;
|
||||
void continue_setup_();
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
uint16_t chip_id_{};
|
||||
uint16_t project_id_{};
|
||||
bool setup_complete_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::cst9220
|
||||
@@ -39,7 +39,7 @@ void CurrentBasedCover::control(const CoverCall &call) {
|
||||
auto opt_pos = call.get_position();
|
||||
if (opt_pos.has_value()) {
|
||||
auto pos = *opt_pos;
|
||||
if (fabsf(this->position - pos) < 0.01) {
|
||||
if (fabsf(this->position - pos) < 0.01f) {
|
||||
// already at target
|
||||
} else {
|
||||
auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
|
||||
|
||||
@@ -151,7 +151,7 @@ uint8_t DaikinBrcClimate::temperature_() {
|
||||
// Temperature in remote is in F
|
||||
if (this->fahrenheit_) {
|
||||
temperature = (uint8_t) roundf(
|
||||
clamp<float>(((this->target_temperature * 1.8) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F));
|
||||
clamp<float>(((this->target_temperature * 1.8f) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F));
|
||||
} else {
|
||||
temperature = ((uint8_t) roundf(this->target_temperature) - 9) << 1;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ float DallasTemperatureSensor::get_temp_c_() {
|
||||
if (this->scratch_pad_[7] == 0) {
|
||||
return NAN;
|
||||
}
|
||||
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
|
||||
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25f;
|
||||
}
|
||||
switch (this->resolution_) {
|
||||
case 9:
|
||||
|
||||
@@ -96,7 +96,8 @@ class DeepSleepComponent final : public Component {
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void set_touch_wakeup(bool touch_wakeup);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace esphome::deep_sleep {
|
||||
// | ESP32-S3 | ✓ | ✓ | ✓ | |
|
||||
// | ESP32-C2 | | | | ✓ |
|
||||
// | ESP32-C3 | | | | ✓ |
|
||||
// | ESP32-C5 | | (✓) | | (✓) |
|
||||
// | ESP32-C5 | | ✓ | | ✓ |
|
||||
// | ESP32-C6 | | ✓ | | ✓ |
|
||||
// | ESP32-C61 | | ✓ | | ✓ |
|
||||
// | ESP32-H2 | | ✓ | | |
|
||||
@@ -56,7 +56,8 @@ void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wa
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
#endif
|
||||
|
||||
@@ -99,7 +100,8 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
|
||||
// Single pin wakeup (ext0) - ESP32, S2, S3 only
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
|
||||
if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
|
||||
@@ -122,9 +124,9 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// GPIO wakeup - C2, C3, C6, C61 only
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C61)
|
||||
// GPIO wakeup - C2, C3, C5, C6, C61 only
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61)
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
|
||||
// Make sure GPIO is in input mode, not all RTC GPIO pins are input by default
|
||||
@@ -154,7 +156,8 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
|
||||
// Touch wakeup - ESP32, S2, S3 only
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) {
|
||||
esp_sleep_enable_touchpad_wakeup();
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
|
||||
@@ -15,7 +15,7 @@ class DemoSensor final : public sensor::Sensor, public PollingComponent {
|
||||
float base = std::isnan(this->state) ? 0.0f : this->state;
|
||||
this->publish_state(base + val * 10);
|
||||
} else {
|
||||
if (val < 0.1) {
|
||||
if (val < 0.1f) {
|
||||
this->publish_state(NAN);
|
||||
} else {
|
||||
this->publish_state(val * 100);
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace esphome::demo {
|
||||
class DemoSwitch final : public switch_::Switch, public Component {
|
||||
public:
|
||||
void setup() override {
|
||||
bool initial = random_float() < 0.5;
|
||||
bool initial = random_float() < 0.5f;
|
||||
this->publish_state(initial);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ class DemoTextSensor final : public text_sensor::TextSensor, public PollingCompo
|
||||
public:
|
||||
void update() override {
|
||||
float val = random_float();
|
||||
if (val < 0.33) {
|
||||
if (val < 0.33f) {
|
||||
this->publish_state("foo");
|
||||
} else if (val < 0.66) {
|
||||
} else if (val < 0.66f) {
|
||||
this->publish_state("bar");
|
||||
} else {
|
||||
this->publish_state("foobar");
|
||||
|
||||
@@ -121,51 +121,51 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
|
||||
|
||||
this->cmd_ = "detRangeCfg -1 0 0";
|
||||
} else if (min2 < 0 || max2 < 0) {
|
||||
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
|
||||
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
|
||||
this->max4_ = max4 = -1;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15f, max1 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
} else if (min3 < 0 || max3 < 0) {
|
||||
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
|
||||
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
|
||||
this->min2_ = min2 = round(min2 / 0.15) * 0.15;
|
||||
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
|
||||
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
|
||||
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
|
||||
max2 / 0.15);
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
|
||||
max2 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
} else if (min4 < 0 || max4 < 0) {
|
||||
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
|
||||
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
|
||||
this->min2_ = min2 = round(min2 / 0.15) * 0.15;
|
||||
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
|
||||
this->min3_ = min3 = round(min3 / 0.15) * 0.15;
|
||||
this->max3_ = max3 = round(max3 / 0.15) * 0.15;
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
|
||||
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
|
||||
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
|
||||
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
|
||||
this->min4_ = min4 = this->max4_ = max4 = -1;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
|
||||
max2 / 0.15, min3 / 0.15, max3 / 0.15);
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
|
||||
max2 / 0.15f, min3 / 0.15f, max3 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
} else {
|
||||
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
|
||||
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
|
||||
this->min2_ = min2 = round(min2 / 0.15) * 0.15;
|
||||
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
|
||||
this->min3_ = min3 = round(min3 / 0.15) * 0.15;
|
||||
this->max3_ = max3 = round(max3 / 0.15) * 0.15;
|
||||
this->min4_ = min4 = round(min4 / 0.15) * 0.15;
|
||||
this->max4_ = max4 = round(max4 / 0.15) * 0.15;
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
|
||||
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
|
||||
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
|
||||
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
|
||||
this->min4_ = min4 = roundf(min4 / 0.15f) * 0.15f;
|
||||
this->max4_ = max4 = roundf(max4 / 0.15f) * 0.15f;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15,
|
||||
min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, max4 / 0.15);
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f,
|
||||
min2 / 0.15f, max2 / 0.15f, min3 / 0.15f, max3 / 0.15f, min4 / 0.15f, max4 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,10 +42,10 @@ void Display::line_at_angle(int x, int y, int angle, int length, Color color) {
|
||||
|
||||
void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_radius, Color color) {
|
||||
// Calculate start and end points
|
||||
int x1 = (start_radius * cos(angle * M_PI / 180)) + x;
|
||||
int y1 = (start_radius * sin(angle * M_PI / 180)) + y;
|
||||
int x2 = (stop_radius * cos(angle * M_PI / 180)) + x;
|
||||
int y2 = (stop_radius * sin(angle * M_PI / 180)) + y;
|
||||
int x1 = (start_radius * std::cos(angle * std::numbers::pi_v<float> / 180)) + x;
|
||||
int y1 = (start_radius * std::sin(angle * std::numbers::pi_v<float> / 180)) + y;
|
||||
int x2 = (stop_radius * std::cos(angle * std::numbers::pi_v<float> / 180)) + x;
|
||||
int y2 = (stop_radius * std::sin(angle * std::numbers::pi_v<float> / 180)) + y;
|
||||
|
||||
// Draw line
|
||||
this->line(x1, y1, x2, y2, color);
|
||||
@@ -228,7 +228,7 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
|
||||
int e2max, e2min;
|
||||
progress = std::max(0, std::min(progress, 100)); // 0..100
|
||||
int draw_progress = progress > 50 ? (100 - progress) : progress;
|
||||
float tan_a = (progress == 50) ? 65535 : tan(float(draw_progress) * M_PI / 100); // slope
|
||||
float tan_a = (progress == 50) ? 65535 : tanf(float(draw_progress) * std::numbers::pi_v<float> / 100); // slope
|
||||
|
||||
do {
|
||||
// outer dots
|
||||
@@ -444,15 +444,15 @@ void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *
|
||||
// hence we rotate the shape by 270° to orient the polygon up.
|
||||
rotation_degrees += ROTATION_270_DEGREES;
|
||||
// Convert the rotation to radians, easier to use in trigonometrical calculations
|
||||
float rotation_radians = rotation_degrees * std::numbers::pi / 180;
|
||||
float rotation_radians = rotation_degrees * std::numbers::pi_v<float> / 180;
|
||||
// A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no
|
||||
// additional rotation of the shape.
|
||||
// A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal,
|
||||
// this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the
|
||||
// left side of the first horizontal edge.
|
||||
rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0;
|
||||
rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi_v<float> / edges : 0.0f;
|
||||
|
||||
float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians;
|
||||
float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi_v<float> + rotation_radians;
|
||||
*vertex_x = (int) std::round(std::cos(vertex_angle) * radius) + center_x;
|
||||
*vertex_y = (int) std::round(std::sin(vertex_angle) * radius) + center_y;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class DS2484OneWireBus final : public one_wire::OneWireBus, public i2c::I2CDevic
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS - 1.0; }
|
||||
float get_setup_priority() const override { return setup_priority::BUS - 1.0f; }
|
||||
|
||||
bool reset_device();
|
||||
int reset_int() override;
|
||||
|
||||
@@ -169,14 +169,14 @@ bool ES7210::configure_mic_gain_() {
|
||||
|
||||
uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) {
|
||||
// reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB
|
||||
mic_gain += 0.5;
|
||||
if (mic_gain <= 33.0) {
|
||||
mic_gain += 0.5f;
|
||||
if (mic_gain <= 33.0f) {
|
||||
return (uint8_t) (mic_gain / 3);
|
||||
}
|
||||
if (mic_gain < 36.0) {
|
||||
if (mic_gain < 36.0f) {
|
||||
return 12;
|
||||
}
|
||||
if (mic_gain < 37.0) {
|
||||
if (mic_gain < 37.0f) {
|
||||
return 13;
|
||||
}
|
||||
return 14;
|
||||
|
||||
@@ -105,14 +105,14 @@ bool ES7243E::configure_mic_gain_() {
|
||||
|
||||
uint8_t ES7243E::es7243e_gain_reg_value_(float mic_gain) {
|
||||
// reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB
|
||||
mic_gain += 0.5;
|
||||
if (mic_gain <= 33.0) {
|
||||
mic_gain += 0.5f;
|
||||
if (mic_gain <= 33.0f) {
|
||||
return (uint8_t) mic_gain / 3;
|
||||
}
|
||||
if (mic_gain < 36.0) {
|
||||
if (mic_gain < 36.0f) {
|
||||
return 12;
|
||||
}
|
||||
if (mic_gain < 37.0) {
|
||||
if (mic_gain < 37.0f) {
|
||||
return 13;
|
||||
}
|
||||
return 14;
|
||||
|
||||
@@ -173,8 +173,14 @@ bool ES8388::set_mute_state_(bool mute_state) {
|
||||
ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL3, &value));
|
||||
ESP_LOGV(TAG, "Read ES8388_DACCONTROL3: 0x%02X", value);
|
||||
|
||||
// Only toggle the DACMute bit; the other bits of this register hold unrelated
|
||||
// DAC settings that must be preserved. Previously muting overwrote the whole
|
||||
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
|
||||
// could not be unmuted again.
|
||||
if (mute_state) {
|
||||
value = 0x3C;
|
||||
value |= ES8388_DACCONTROL3_DAC_MUTE;
|
||||
} else {
|
||||
value &= ~ES8388_DACCONTROL3_DAC_MUTE;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
|
||||
|
||||
@@ -38,6 +38,7 @@ static const uint8_t ES8388_ADCCONTROL14 = 0x16;
|
||||
static const uint8_t ES8388_DACCONTROL1 = 0x17;
|
||||
static const uint8_t ES8388_DACCONTROL2 = 0x18;
|
||||
static const uint8_t ES8388_DACCONTROL3 = 0x19;
|
||||
static const uint8_t ES8388_DACCONTROL3_DAC_MUTE = 0x04; // DACMute, bit 2 of DACCONTROL3
|
||||
static const uint8_t ES8388_DACCONTROL4 = 0x1a;
|
||||
static const uint8_t ES8388_DACCONTROL5 = 0x1b;
|
||||
static const uint8_t ES8388_DACCONTROL6 = 0x1c;
|
||||
|
||||
@@ -1102,6 +1102,8 @@ def final_validate(config):
|
||||
# Imported locally to avoid circular import issues
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
|
||||
from .gpio import final_validate_pins
|
||||
|
||||
errs = []
|
||||
conf_fw = config[CONF_FRAMEWORK]
|
||||
advanced = conf_fw[CONF_ADVANCED]
|
||||
@@ -1185,6 +1187,8 @@ def final_validate(config):
|
||||
)
|
||||
)
|
||||
|
||||
final_validate_pins(full_config)
|
||||
|
||||
if (
|
||||
config[CONF_FLASH_SIZE] == "32MB"
|
||||
and "ota" in full_config
|
||||
|
||||
@@ -18,6 +18,7 @@ from esphome.const import (
|
||||
PLATFORM_ESP32,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import boards
|
||||
from .const import (
|
||||
@@ -50,7 +51,11 @@ from .gpio_esp32_h4 import esp32_h4_validate_gpio_pin, esp32_h4_validate_support
|
||||
from .gpio_esp32_h21 import esp32_h21_validate_gpio_pin, esp32_h21_validate_supports
|
||||
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
|
||||
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
|
||||
from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports
|
||||
from .gpio_esp32_s3 import (
|
||||
esp32_s3_final_validate_pins,
|
||||
esp32_s3_validate_gpio_pin,
|
||||
esp32_s3_validate_supports,
|
||||
)
|
||||
from .gpio_esp32_s31 import esp32_s31_validate_gpio_pin, esp32_s31_validate_supports
|
||||
|
||||
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
|
||||
@@ -96,6 +101,7 @@ def _translate_pin(value):
|
||||
class ESP32ValidationFunctions:
|
||||
pin_validation: Callable[[int], int]
|
||||
usage_validation: Callable[[dict[str, Any]], dict[str, Any]]
|
||||
final_validate: Callable[[ConfigType], None] | None = None
|
||||
|
||||
|
||||
_esp32_validations = {
|
||||
@@ -145,6 +151,7 @@ _esp32_validations = {
|
||||
VARIANT_ESP32S3: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s3_validate_gpio_pin,
|
||||
usage_validation=esp32_s3_validate_supports,
|
||||
final_validate=esp32_s3_final_validate_pins,
|
||||
),
|
||||
VARIANT_ESP32S31: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s31_validate_gpio_pin,
|
||||
@@ -261,3 +268,10 @@ async def esp32_pin_to_code(config):
|
||||
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
|
||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||
return var
|
||||
|
||||
|
||||
def final_validate_pins(full_config: ConfigType) -> None:
|
||||
"""Run the active variant's pin final-validation, if it defines one."""
|
||||
funcs = _esp32_validations.get(CORE.data[KEY_ESP32][KEY_VARIANT])
|
||||
if funcs is not None and funcs.final_validate is not None:
|
||||
funcs.final_validate(full_config)
|
||||
|
||||
@@ -2,8 +2,15 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
from esphome.const import (
|
||||
CONF_DISABLED,
|
||||
CONF_INPUT,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
PLATFORM_ESP32,
|
||||
)
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY, check_strapping_pin
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_ESP32S3_SPI_PSRAM_PINS = {
|
||||
26: "SPICS1",
|
||||
@@ -38,11 +45,9 @@ def esp32_s3_validate_gpio_pin(value: int) -> int:
|
||||
raise cv.Invalid(
|
||||
f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP32S3_SPI_PSRAM_PINS[value]})"
|
||||
)
|
||||
if value in _ESP32S3R8_PSRAM_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models",
|
||||
value,
|
||||
)
|
||||
# GPIO33-37 (_ESP32S3R8_PSRAM_PINS) are only taken by the PSRAM interface in
|
||||
# octal mode -- whether that applies isn't known here, so the warning is
|
||||
# deferred to final_validate_pins() in gpio.py once the PSRAM mode is resolved.
|
||||
|
||||
if value in (22, 23, 24, 25):
|
||||
# These pins are not exposed in GPIO mux (reason unknown)
|
||||
@@ -71,3 +76,29 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_s3_final_validate_pins(full_config: ConfigType) -> None:
|
||||
"""Warn about GPIO33-37 usage, but only when octal PSRAM (which uses them) is set.
|
||||
|
||||
These pins are only taken by the PSRAM interface in octal mode (ESP32-S3R8 /
|
||||
S3R8V); on quad-PSRAM variants -- or when the psram block is disabled, so the
|
||||
octal interface is never configured -- they are free. The per-pin validator
|
||||
can't know the PSRAM mode, so the check is deferred here, where
|
||||
PIN_SCHEMA_REGISTRY.pins_used already lists every used pin.
|
||||
"""
|
||||
# Imported locally to avoid circular import issues
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN, TYPE_OCTAL
|
||||
|
||||
psram_config = full_config.get(PSRAM_DOMAIN, {})
|
||||
if psram_config.get(CONF_DISABLED) or psram_config.get(CONF_MODE) != TYPE_OCTAL:
|
||||
return
|
||||
for number in sorted(
|
||||
number
|
||||
for key, _client_id, number in PIN_SCHEMA_REGISTRY.pins_used
|
||||
if key == PLATFORM_ESP32 and number in _ESP32S3R8_PSRAM_PINS
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is used by the PSRAM interface in octal mode and should be avoided",
|
||||
number,
|
||||
)
|
||||
|
||||
@@ -28,9 +28,6 @@ namespace esphome::espnow {
|
||||
|
||||
static constexpr const char *TAG = "espnow";
|
||||
|
||||
static const esp_err_t CONFIG_ESPNOW_WAKE_WINDOW = 50;
|
||||
static const esp_err_t CONFIG_ESPNOW_WAKE_INTERVAL = 100;
|
||||
|
||||
ESPNowComponent *global_esp_now = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static const LogString *espnow_error_to_str(esp_err_t error) {
|
||||
@@ -204,11 +201,6 @@ void ESPNowComponent::enable_() {
|
||||
|
||||
esp_wifi_get_mac(WIFI_IF_STA, this->own_address_);
|
||||
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
esp_now_set_wake_window(CONFIG_ESPNOW_WAKE_WINDOW);
|
||||
esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL);
|
||||
#endif
|
||||
|
||||
this->state_ = ESPNOW_STATE_ENABLED;
|
||||
|
||||
for (auto peer : this->peers_) {
|
||||
@@ -311,7 +303,9 @@ void ESPNowComponent::loop() {
|
||||
ESP_LOGV(TAG, ">>> [%s] %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status)));
|
||||
#endif
|
||||
if (this->current_send_packet_ != nullptr) {
|
||||
this->current_send_packet_->callback_(packet->packet_.sent.status);
|
||||
if (this->current_send_packet_->callback_ != nullptr) {
|
||||
this->current_send_packet_->callback_(packet->packet_.sent.status);
|
||||
}
|
||||
this->send_packet_pool_.release(this->current_send_packet_);
|
||||
this->current_send_packet_ = nullptr; // Reset current packet after sending
|
||||
}
|
||||
|
||||
@@ -126,6 +126,8 @@ ETHERNET_TYPES = {
|
||||
"ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60,
|
||||
"W6100": EthernetType.ETHERNET_TYPE_W6100,
|
||||
"W6300": EthernetType.ETHERNET_TYPE_W6300,
|
||||
"GENERIC": EthernetType.ETHERNET_TYPE_GENERIC,
|
||||
"YT8531": EthernetType.ETHERNET_TYPE_YT8531,
|
||||
}
|
||||
|
||||
# PHY types that need compile-time defines for conditional compilation
|
||||
@@ -145,6 +147,8 @@ _PHY_TYPE_TO_DEFINE = {
|
||||
"ENC28J60": "USE_ETHERNET_ENC28J60",
|
||||
"W6100": "USE_ETHERNET_W6100",
|
||||
"W6300": "USE_ETHERNET_W6300",
|
||||
"GENERIC": "USE_ETHERNET_GENERIC",
|
||||
"YT8531": "USE_ETHERNET_YT8531",
|
||||
}
|
||||
|
||||
|
||||
@@ -309,6 +313,24 @@ def _validate(config):
|
||||
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
|
||||
f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]."
|
||||
)
|
||||
elif config[CONF_TYPE] in ("GENERIC", "YT8531"):
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S31,
|
||||
get_esp32_variant,
|
||||
idf_version,
|
||||
)
|
||||
|
||||
eth_type = config[CONF_TYPE]
|
||||
variant = get_esp32_variant()
|
||||
if variant != VARIANT_ESP32S31:
|
||||
raise cv.Invalid(
|
||||
f"The '{eth_type}' (RGMII) PHY is only supported on gigabit-capable "
|
||||
f"variants (ESP32-S31), not {variant}"
|
||||
)
|
||||
if idf_version() < cv.Version(6, 0, 0):
|
||||
raise cv.Invalid(
|
||||
f"The '{eth_type}' (RGMII) PHY requires ESP-IDF 6.0 or newer."
|
||||
)
|
||||
elif config[CONF_TYPE] != "OPENETH":
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
@@ -392,6 +414,23 @@ RMII_SCHEMA = cv.All(
|
||||
cv.only_on([Platform.ESP32]),
|
||||
)
|
||||
|
||||
# Generic IEEE 802.3 PHY over the internal EMAC RGMII interface (e.g. ESP32-S31).
|
||||
# RGMII data pins come from the IDF per-target default config.
|
||||
GENERIC_SCHEMA = cv.All(
|
||||
BASE_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31),
|
||||
cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA),
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.only_on([Platform.ESP32]),
|
||||
)
|
||||
|
||||
SPI_SCHEMA = cv.All(
|
||||
BASE_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
@@ -442,6 +481,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
|
||||
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
|
||||
"LAN8670": RMII_SCHEMA,
|
||||
"GENERIC": GENERIC_SCHEMA,
|
||||
"YT8531": GENERIC_SCHEMA,
|
||||
},
|
||||
upper=True,
|
||||
),
|
||||
@@ -571,6 +612,20 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
elif config[CONF_TYPE] == "OPENETH":
|
||||
cg.add_define("USE_ETHERNET_OPENETH")
|
||||
add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True)
|
||||
elif config[CONF_TYPE] in ("GENERIC", "YT8531"):
|
||||
# RGMII data pins come from the IDF default config; set MDC/MDIO + PHY addr.
|
||||
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
|
||||
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
|
||||
cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN]))
|
||||
if CONF_POWER_PIN in config:
|
||||
cg.add(var.set_power_pin(config[CONF_POWER_PIN]))
|
||||
for register_value in config.get(CONF_PHY_REGISTERS, []):
|
||||
reg = phy_register(
|
||||
register_value.get(CONF_ADDRESS),
|
||||
register_value.get(CONF_VALUE),
|
||||
register_value.get(CONF_PAGE_ID),
|
||||
)
|
||||
cg.add(var.add_phy_register(reg))
|
||||
else:
|
||||
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
|
||||
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
|
||||
|
||||
@@ -86,6 +86,8 @@ enum EthernetType : uint8_t {
|
||||
ETHERNET_TYPE_ENC28J60,
|
||||
ETHERNET_TYPE_W6100,
|
||||
ETHERNET_TYPE_W6300,
|
||||
ETHERNET_TYPE_GENERIC,
|
||||
ETHERNET_TYPE_YT8531,
|
||||
};
|
||||
|
||||
struct ManualIP {
|
||||
@@ -229,6 +231,11 @@ class EthernetComponent final : public Component {
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
|
||||
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_YT8531
|
||||
/// @brief Apply YT8531-specific config: re-enable auto-negotiation (disabled on
|
||||
/// reset) and set the RGMII Tx/Rx clock delays needed for reliable data sampling.
|
||||
void yt8531_phy_init_();
|
||||
#endif
|
||||
/// @brief Set arbitratry PHY registers from config.
|
||||
void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data);
|
||||
|
||||
@@ -254,9 +254,14 @@ void EthernetComponent::ethernet_lazy_init_() {
|
||||
esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
|
||||
esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_;
|
||||
#endif
|
||||
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
|
||||
esp32_emac_config.clock_config.rmii.clock_gpio =
|
||||
static_cast<decltype(esp32_emac_config.clock_config.rmii.clock_gpio)>(this->clk_pin_);
|
||||
// The RGMII types (GENERIC, YT8531) use the RGMII interface and default GPIO map from
|
||||
// eth_esp32_emac_default_config(); writing the RMII clock config would clobber that
|
||||
// union, so skip the RMII clock override for them.
|
||||
if (this->type_ != ETHERNET_TYPE_GENERIC && this->type_ != ETHERNET_TYPE_YT8531) {
|
||||
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
|
||||
esp32_emac_config.clock_config.rmii.clock_gpio =
|
||||
static_cast<decltype(esp32_emac_config.clock_config.rmii.clock_gpio)>(this->clk_pin_);
|
||||
}
|
||||
|
||||
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
|
||||
#endif
|
||||
@@ -319,6 +324,20 @@ void EthernetComponent::ethernet_lazy_init_() {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// GENERIC and YT8531 both use the built-in generic 802.3 PHY driver; YT8531 gets
|
||||
// extra chip-specific tuning applied later in ethernet_lazy_init_().
|
||||
#ifdef USE_ETHERNET_GENERIC
|
||||
case ETHERNET_TYPE_GENERIC:
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_YT8531
|
||||
case ETHERNET_TYPE_YT8531:
|
||||
#endif
|
||||
#if defined(USE_ETHERNET_GENERIC) || defined(USE_ETHERNET_YT8531)
|
||||
this->phy_ = esp_eth_phy_new_generic(&phy_config);
|
||||
break;
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
#if defined(USE_ETHERNET_W5500)
|
||||
@@ -363,7 +382,30 @@ void EthernetComponent::ethernet_lazy_init_() {
|
||||
for (const auto &phy_register : this->phy_registers_) {
|
||||
this->write_phy_register_(mac, phy_register);
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
#ifdef USE_ETHERNET_GENERIC
|
||||
// The generic 802.3 PHY driver only resets the PHY in its init; it never enables
|
||||
// auto-negotiation. A PHY that resets into a forced-speed mode (BMCR auto-nego bit
|
||||
// clear) therefore stays there, and esp_eth_start() skips negotiation because the
|
||||
// driver cached auto_nego_en=false at install time. Force auto-negotiation on here
|
||||
// (which also updates that cached state) so esp_eth_start() restarts a proper
|
||||
// negotiation. (YT8531 does this as part of its own chip-specific init below.)
|
||||
if (this->type_ == ETHERNET_TYPE_GENERIC) {
|
||||
bool autoneg_enable = true;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_AUTONEGO, &autoneg_enable);
|
||||
ESPHL_ERROR_CHECK(err, "Enable auto-negotiation failed");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_YT8531
|
||||
if (this->type_ == ETHERNET_TYPE_YT8531) {
|
||||
this->yt8531_phy_init_();
|
||||
if (this->is_failed())
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#endif // ESP_IDF_VERSION >= 6.0.0
|
||||
#endif // !USE_ETHERNET_SPI
|
||||
|
||||
// use ESP internal eth mac
|
||||
uint8_t mac_addr[6];
|
||||
@@ -486,6 +528,16 @@ void EthernetComponent::dump_config() {
|
||||
eth_type = "LAN8670";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_GENERIC
|
||||
case ETHERNET_TYPE_GENERIC:
|
||||
eth_type = "Generic (RGMII)";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_YT8531
|
||||
case ETHERNET_TYPE_YT8531:
|
||||
eth_type = "YT8531 (RGMII)";
|
||||
break;
|
||||
#endif
|
||||
|
||||
default:
|
||||
eth_type = "Unknown";
|
||||
@@ -782,6 +834,19 @@ void EthernetComponent::dump_connect_params_() {
|
||||
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
uint16_t link_speed = 10;
|
||||
switch (this->get_link_speed()) {
|
||||
case ETH_SPEED_100M:
|
||||
link_speed = 100;
|
||||
break;
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
case ETH_SPEED_1000M:
|
||||
link_speed = 1000;
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" IP Address: %s\n"
|
||||
" Hostname: '%s'\n"
|
||||
@@ -796,7 +861,7 @@ void EthernetComponent::dump_connect_params_() {
|
||||
network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf),
|
||||
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf),
|
||||
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
|
||||
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
|
||||
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), link_speed);
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
@@ -958,6 +1023,50 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_ETHERNET_YT8531
|
||||
void EthernetComponent::yt8531_phy_init_() {
|
||||
esp_err_t err;
|
||||
|
||||
// The YT8531 disables auto-negotiation on hardware reset (undocumented behavior), and the
|
||||
// generic 802.3 driver only resets the PHY, so re-enable it (this also updates the driver's
|
||||
// cached auto-nego state used by esp_eth_start()).
|
||||
bool autoneg_enable = true;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_AUTONEGO, &autoneg_enable);
|
||||
ESPHL_ERROR_CHECK(err, "YT8531 enable auto-negotiation failed");
|
||||
|
||||
// RGMII needs ~2 ns Tx and Rx clock delays for reliable data sampling. These are set through
|
||||
// the YT8531 extended-register interface: write the ext-register address to 0x1E, then
|
||||
// read/modify/write its value via 0x1F.
|
||||
esp_eth_phy_reg_rw_data_t phy_reg;
|
||||
uint32_t reg_val;
|
||||
phy_reg.reg_value_p = ®_val;
|
||||
|
||||
// RX ~2 ns coarse delay: EXT_CHIP_CONFIG (0xA001), set rxc_dly_en (bit 8).
|
||||
reg_val = 0xA001;
|
||||
phy_reg.reg_addr = 0x1E;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
|
||||
ESPHL_ERROR_CHECK(err, "YT8531 select Chip_Config failed");
|
||||
phy_reg.reg_addr = 0x1F;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_READ_PHY_REG, &phy_reg);
|
||||
ESPHL_ERROR_CHECK(err, "YT8531 read Chip_Config failed");
|
||||
reg_val |= (1U << 8);
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
|
||||
ESPHL_ERROR_CHECK(err, "YT8531 write Chip_Config failed");
|
||||
|
||||
// TX ~2 ns delay: EXT_RGMII_CONFIG1 (0xA003), tx_delay_sel[3:0] and tx_delay_sel_fe[7:4] = 13.
|
||||
reg_val = 0xA003;
|
||||
phy_reg.reg_addr = 0x1E;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
|
||||
ESPHL_ERROR_CHECK(err, "YT8531 select RGMII_Config1 failed");
|
||||
phy_reg.reg_addr = 0x1F;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_READ_PHY_REG, &phy_reg);
|
||||
ESPHL_ERROR_CHECK(err, "YT8531 read RGMII_Config1 failed");
|
||||
reg_val = (reg_val & ~0x00FFU) | (13U << 4) | (13U << 0);
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
|
||||
ESPHL_ERROR_CHECK(err, "YT8531 write RGMII_Config1 failed");
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
@@ -139,7 +139,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
|
||||
/// Draw grid
|
||||
if (!std::isnan(this->gridspacing_y_)) {
|
||||
for (int y = yn; y <= ym; y++) {
|
||||
int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (float) (y - yn) / (ym - yn)));
|
||||
int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0f - (float) (y - yn) / (ym - yn)));
|
||||
for (uint32_t x = 0; x < this->width_; x += 2) {
|
||||
buff->draw_pixel_at(x_offset + x, y_offset + py, color);
|
||||
}
|
||||
@@ -177,7 +177,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
|
||||
uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick);
|
||||
bool b = (trace->get_line_type() & bit) == bit;
|
||||
if (b) {
|
||||
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
|
||||
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0f - v)) - thick / 2 + y_offset;
|
||||
auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
|
||||
if (y >= y_offset && static_cast<uint32_t>(y) < y_offset + this->height_)
|
||||
buff->draw_pixel_at(x, y, c);
|
||||
|
||||
@@ -122,7 +122,7 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps,
|
||||
|
||||
rpm = clamp<uint16_t>(rpm, 1, 300);
|
||||
|
||||
ms_per_step = (uint16_t) (3000.0 / (float) rpm);
|
||||
ms_per_step = (uint16_t) (3000.0f / (float) rpm);
|
||||
buffer_[0] = mode;
|
||||
buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw
|
||||
buffer_[2] = steps;
|
||||
@@ -153,7 +153,7 @@ void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t
|
||||
uint16_t ms_per_step = 0;
|
||||
|
||||
rpm = clamp<uint16_t>(rpm, 1, 300);
|
||||
ms_per_step = (uint16_t) (3000.0 / (float) rpm);
|
||||
ms_per_step = (uint16_t) (3000.0f / (float) rpm);
|
||||
|
||||
buffer_[0] = mode;
|
||||
buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw
|
||||
|
||||
@@ -607,7 +607,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
|
||||
if (climate_control.target_temperature.has_value()) {
|
||||
float target_temp = climate_control.target_temperature.value();
|
||||
out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16
|
||||
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
|
||||
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49f) ? 1 : 0;
|
||||
}
|
||||
if (out_data->ac_power == 0) {
|
||||
// If AC is off - no presets allowed
|
||||
|
||||
@@ -341,7 +341,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
|
||||
if (climate_control.target_temperature.has_value()) {
|
||||
float target_temp = climate_control.target_temperature.value();
|
||||
out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16
|
||||
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
|
||||
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49f) ? 1 : 0;
|
||||
}
|
||||
if (out_data->ac_power == 0) {
|
||||
// If AC is off - no presets allowed
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
#include <numbers>
|
||||
|
||||
namespace esphome::hmc5883l {
|
||||
|
||||
static const char *const TAG = "hmc5883l";
|
||||
@@ -126,7 +128,7 @@ void HMC5883LComponent::update() {
|
||||
const float y = int16_t(raw_y) * mg_per_bit * 0.1f;
|
||||
const float z = int16_t(raw_z) * mg_per_bit * 0.1f;
|
||||
|
||||
float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
|
||||
float heading = atan2f(0.0f - x, y) * 180.0f / std::numbers::pi_v<float>;
|
||||
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading);
|
||||
|
||||
if (this->x_sensor_ != nullptr)
|
||||
|
||||
@@ -55,7 +55,9 @@ float HONEYWELLABPSensor::countstopressure_(const int counts, const float min_pr
|
||||
|
||||
// Converts a digital temperature measurement in counts to temperature in C
|
||||
// This will be invalid if sensore daoes not have temperature measurement capability
|
||||
float HONEYWELLABPSensor::countstotemperatures_(const int counts) { return (((float) counts / 2047.0) * 200.0) - 50.0; }
|
||||
float HONEYWELLABPSensor::countstotemperatures_(const int counts) {
|
||||
return (((float) counts / 2047.0f) * 200.0f) - 50.0f;
|
||||
}
|
||||
|
||||
// Pressure value from the most recent reading in units
|
||||
float HONEYWELLABPSensor::read_pressure_() {
|
||||
@@ -69,9 +71,9 @@ void HONEYWELLABPSensor::update() {
|
||||
ESP_LOGV(TAG, "Update Honeywell ABP Sensor");
|
||||
if (readsensor_() == 0) {
|
||||
if (this->pressure_sensor_ != nullptr)
|
||||
this->pressure_sensor_->publish_state(read_pressure_() * 1.0);
|
||||
this->pressure_sensor_->publish_state(read_pressure_() * 1.0f);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(read_temperature_() * 1.0);
|
||||
this->temperature_sensor_->publish_state(read_temperature_() * 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ void HrxlMaxsonarWrComponent::check_buffer_() {
|
||||
millimeters = millimeters * 10;
|
||||
}
|
||||
|
||||
float meters = float(millimeters) / 1000.0;
|
||||
float meters = float(millimeters) / 1000.0f;
|
||||
ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters);
|
||||
this->publish_state(meters);
|
||||
} else {
|
||||
|
||||
@@ -139,7 +139,7 @@ void I2SAudioSpeakerBase::set_volume(float volume) {
|
||||
this->volume_ = volume;
|
||||
#ifdef USE_AUDIO_DAC
|
||||
if (this->audio_dac_ != nullptr) {
|
||||
if (volume > 0.0) {
|
||||
if (volume > 0.0f) {
|
||||
this->audio_dac_->set_mute_off();
|
||||
}
|
||||
this->audio_dac_->set_volume(volume);
|
||||
|
||||
@@ -119,7 +119,7 @@ void INA219Component::setup() {
|
||||
}
|
||||
|
||||
this->calibration_lsb_ = lsb;
|
||||
auto calibration = uint32_t(0.04096f / (0.000001 * lsb * this->shunt_resistance_ohm_));
|
||||
auto calibration = uint32_t(0.04096f / (0.000001f * lsb * this->shunt_resistance_ohm_));
|
||||
ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration);
|
||||
if (!this->write_byte_16(INA219_REGISTER_CALIBRATION, calibration)) {
|
||||
this->mark_failed();
|
||||
|
||||
@@ -70,7 +70,7 @@ void INA226Component::setup() {
|
||||
|
||||
this->calibration_lsb_ = lsb;
|
||||
|
||||
auto calibration = uint32_t(0.00512 / (lsb * this->shunt_resistance_ohm_ / 1000000.0f));
|
||||
auto calibration = uint32_t(0.00512f / (lsb * this->shunt_resistance_ohm_ / 1000000.0f));
|
||||
|
||||
ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@Passific", "@koosoli", "@limengdu"]
|
||||
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
ESPHome configuration for the IT8951 e-paper controller.
|
||||
"""
|
||||
|
||||
from esphome import automation, core, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import update_interval
|
||||
from esphome.const import (
|
||||
CONF_BUSY_PIN,
|
||||
CONF_CS_PIN,
|
||||
CONF_DATA_RATE,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_FULL_UPDATE_EVERY,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_INVERT_COLORS,
|
||||
CONF_LAMBDA,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODE,
|
||||
CONF_MODEL,
|
||||
CONF_PAGES,
|
||||
CONF_RESET_DURATION,
|
||||
CONF_RESET_PIN,
|
||||
CONF_ROTATION,
|
||||
CONF_SLEEP_WHEN_DONE,
|
||||
CONF_SWAP_XY,
|
||||
CONF_TRANSFORM,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
AUTO_LOAD = ["split_buffer"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
CONF_VCOM = "vcom"
|
||||
CONF_VCOM_REGISTER = "vcom_register"
|
||||
CONF_FORCE_TEMPERATURE = "force_temperature"
|
||||
CONF_GRAYSCALE = "grayscale"
|
||||
CONF_DITHERING = "dithering"
|
||||
CONF_UPDATE_MODE = "update_mode"
|
||||
CONF_USE_LEGACY_DPY_AREA = "use_legacy_dpy_area"
|
||||
|
||||
# VCOM SET sub-command selectors. The IT8951 firmware accepts different
|
||||
# values across panels; most respond to 0x0001, but a few — e.g. the Seeed
|
||||
# reTerminal E1003 — only respond to 0x0002 and silently drop 0x0001.
|
||||
VCOM_REGISTER_DEFAULT = 0x0001
|
||||
VCOM_REGISTER_ALT = 0x0002
|
||||
VCOM_REGISTER_OPTIONS = (VCOM_REGISTER_DEFAULT, VCOM_REGISTER_ALT)
|
||||
|
||||
it8951_ns = cg.esphome_ns.namespace("it8951")
|
||||
IT8951Display = it8951_ns.class_("IT8951Display", display.Display, spi.SPIDevice)
|
||||
IT8951UpdateAction = it8951_ns.class_("IT8951UpdateAction", automation.Action)
|
||||
|
||||
# Hardware waveform modes exposed to YAML. Strings are mapped to the C++
|
||||
# UpdateMode enum so the runtime can store the mode as a uint16_t rather
|
||||
# than a std::string (avoiding a heap-resident member; see ESPHome
|
||||
# CLAUDE.md "STL Container Guidelines"). "fast" and "full" are
|
||||
# convenience aliases for DU and GC16 respectively.
|
||||
UpdateMode = it8951_ns.enum("UpdateMode")
|
||||
UPDATE_MODE_OPTIONS = {
|
||||
"INIT": UpdateMode.UPDATE_MODE_INIT,
|
||||
"DU": UpdateMode.UPDATE_MODE_DU,
|
||||
"GC16": UpdateMode.UPDATE_MODE_GC16,
|
||||
"GL16": UpdateMode.UPDATE_MODE_GL16,
|
||||
"GLR16": UpdateMode.UPDATE_MODE_GLR16,
|
||||
"GLD16": UpdateMode.UPDATE_MODE_GLD16,
|
||||
"DU4": UpdateMode.UPDATE_MODE_DU4,
|
||||
"A2": UpdateMode.UPDATE_MODE_A2,
|
||||
"FAST": UpdateMode.UPDATE_MODE_DU,
|
||||
"FULL": UpdateMode.UPDATE_MODE_GC16,
|
||||
}
|
||||
# Maps the YAML mode string directly to the C++ UpdateMode enum value, so the
|
||||
# config option and the it8951.update action share one validator.
|
||||
update_mode = cv.enum(UPDATE_MODE_OPTIONS, upper=True)
|
||||
|
||||
# Transform flag values mirror the C++ TRANSFORM_* constants.
|
||||
_TRANSFORM_NONE = 0
|
||||
_TRANSFORM_MIRROR_X = 1
|
||||
_TRANSFORM_MIRROR_Y = 2
|
||||
_TRANSFORM_SWAP_XY = 4
|
||||
_TRANSFORM_FLAGS = {
|
||||
CONF_MIRROR_X: _TRANSFORM_MIRROR_X,
|
||||
CONF_MIRROR_Y: _TRANSFORM_MIRROR_Y,
|
||||
CONF_SWAP_XY: _TRANSFORM_SWAP_XY,
|
||||
}
|
||||
|
||||
|
||||
class IT8951Model:
|
||||
"""A specific board / panel preset for the IT8951 controller."""
|
||||
|
||||
models: dict[str, "IT8951Model"] = {}
|
||||
|
||||
def __init__(self, name: str, **defaults):
|
||||
name = name.upper()
|
||||
self.name = name
|
||||
self.defaults = defaults
|
||||
IT8951Model.models[name] = self
|
||||
|
||||
def get_default(self, key, fallback=None):
|
||||
return self.defaults.get(key, fallback)
|
||||
|
||||
def get_dimensions(self, config) -> tuple[int, int]:
|
||||
# If dimensions are in config, use them; otherwise fall back to model defaults.
|
||||
if CONF_DIMENSIONS in config:
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
return dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]
|
||||
return tuple(dimensions)
|
||||
# Model must have defaults if dimensions not in config.
|
||||
return self.get_default(CONF_WIDTH), self.get_default(CONF_HEIGHT)
|
||||
|
||||
|
||||
# --- Model presets ----------------------------------------------------------
|
||||
# The generic model leaves dimensions and pin choices up to the user.
|
||||
IT8951Model("it8951", vcom=2300, sleep_when_done=True, data_rate=12_000_000)
|
||||
|
||||
IT8951Model(
|
||||
"m5stack-m5paper",
|
||||
width=960,
|
||||
height=540,
|
||||
busy_pin=27,
|
||||
reset_pin=23,
|
||||
cs_pin=15,
|
||||
vcom=2300,
|
||||
sleep_when_done=True,
|
||||
data_rate=20_000_000,
|
||||
)
|
||||
|
||||
IT8951Model(
|
||||
"seeed-reterminal-e1003",
|
||||
width=1872,
|
||||
height=1404,
|
||||
busy_pin=13,
|
||||
reset_pin=12,
|
||||
cs_pin=10,
|
||||
# Board power-enable rails: 1.8V logic supply (GPIO21) and the EPD supply
|
||||
# (GPIO11). Driven high during setup so no separate power_supply is needed.
|
||||
enable_pin=[21, 11],
|
||||
vcom=1400,
|
||||
# reTerminal E1003 panel firmware only accepts the 0x0002 VCOM SET
|
||||
# selector; using the default 0x0001 leaves VCOM unchanged and breaks
|
||||
# grayscale waveforms (GC16/GL16) — INIT still works because it does
|
||||
# not depend on VCOM accuracy.
|
||||
vcom_register=VCOM_REGISTER_ALT,
|
||||
# The reTerminal E1003 ships with on-die temperature sensing disabled,
|
||||
# so the host must declare an operating temperature; otherwise the
|
||||
# waveform LUT defaults to a value that produces no visible change
|
||||
# for grayscale modes.
|
||||
force_temperature=25,
|
||||
sleep_when_done=False,
|
||||
data_rate=20_000_000,
|
||||
mirror_x=True,
|
||||
)
|
||||
|
||||
IT8951Model(
|
||||
"seeed-ee03",
|
||||
width=1872,
|
||||
height=1404,
|
||||
busy_pin=4,
|
||||
reset_pin=38,
|
||||
cs_pin=44,
|
||||
vcom=1400,
|
||||
sleep_when_done=False,
|
||||
data_rate=4_000_000,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DIMENSION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_WIDTH): cv.int_,
|
||||
cv.Required(CONF_HEIGHT): cv.int_,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _model_pin_option(model, key, schema):
|
||||
default = model.get_default(key)
|
||||
if default is None:
|
||||
return cv.Required(key), schema
|
||||
return cv.Optional(key, default=default), schema
|
||||
|
||||
|
||||
def _model_schema(config):
|
||||
model = IT8951Model.models[config[CONF_MODEL]]
|
||||
has_default_dimensions = (
|
||||
model.get_default(CONF_WIDTH) is not None
|
||||
and model.get_default(CONF_HEIGHT) is not None
|
||||
)
|
||||
dimensions_key = (
|
||||
cv.Optional(
|
||||
CONF_DIMENSIONS,
|
||||
default={
|
||||
CONF_WIDTH: model.get_default(CONF_WIDTH),
|
||||
CONF_HEIGHT: model.get_default(CONF_HEIGHT),
|
||||
},
|
||||
)
|
||||
if has_default_dimensions
|
||||
else cv.Required(CONF_DIMENSIONS)
|
||||
)
|
||||
|
||||
schema = display.FULL_DISPLAY_SCHEMA.extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_mode="MODE0",
|
||||
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
|
||||
)
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(IT8951Display),
|
||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True, space="-"),
|
||||
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): update_interval,
|
||||
cv.Optional(CONF_FULL_UPDATE_EVERY, default=30): cv.int_range(1, 255),
|
||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_INVERT_COLORS, default=model.get_default(CONF_INVERT_COLORS, False)
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_SLEEP_WHEN_DONE,
|
||||
default=model.get_default(CONF_SLEEP_WHEN_DONE, False),
|
||||
): cv.boolean,
|
||||
# Pixel format: true = 4bpp grayscale, false = packed 1bpp
|
||||
# monochrome. Monochrome halves the framebuffer and enables fast DU
|
||||
# partial refreshes; grayscale gives 16 levels but always uses GC16.
|
||||
cv.Optional(
|
||||
CONF_GRAYSCALE, default=model.get_default(CONF_GRAYSCALE, True)
|
||||
): cv.boolean,
|
||||
# Monochrome only: ordered-dither pale colours so they render as
|
||||
# visible stipple. Disable for a crisp hard black/white threshold
|
||||
# (better for purely black/white text). No effect in grayscale mode.
|
||||
cv.Optional(
|
||||
CONF_DITHERING, default=model.get_default(CONF_DITHERING, True)
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_VCOM, default=model.get_default(CONF_VCOM, 2300)
|
||||
): cv.int_range(0, 5000),
|
||||
cv.Optional(
|
||||
CONF_VCOM_REGISTER,
|
||||
default=model.get_default(CONF_VCOM_REGISTER, VCOM_REGISTER_DEFAULT),
|
||||
): cv.one_of(*VCOM_REGISTER_OPTIONS, int=True),
|
||||
**(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_FORCE_TEMPERATURE,
|
||||
default=model.get_default(CONF_FORCE_TEMPERATURE),
|
||||
): cv.int_range(min=-40, max=85)
|
||||
}
|
||||
if model.get_default(CONF_FORCE_TEMPERATURE) is not None
|
||||
else {}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_USE_LEGACY_DPY_AREA,
|
||||
default=model.get_default(CONF_USE_LEGACY_DPY_AREA, False),
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_UPDATE_MODE): update_mode,
|
||||
# One or more GPIOs driven high during setup to power on the panel
|
||||
# (e.g. board power-enable rails), before reset and init.
|
||||
cv.Optional(
|
||||
CONF_ENABLE_PIN, default=model.get_default(CONF_ENABLE_PIN, [])
|
||||
): cv.ensure_list(pins.gpio_output_pin_schema),
|
||||
cv.Optional(CONF_RESET_DURATION): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||
),
|
||||
dimensions_key: DIMENSION_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
# Pin options: required if the model doesn't supply a default.
|
||||
pin_specs = (
|
||||
(CONF_BUSY_PIN, pins.gpio_input_pin_schema),
|
||||
(CONF_RESET_PIN, pins.gpio_output_pin_schema),
|
||||
(CONF_CS_PIN, pins.gpio_output_pin_schema),
|
||||
)
|
||||
pin_extra = {}
|
||||
for key, schema_value in pin_specs:
|
||||
opt, sv = _model_pin_option(model, key, schema_value)
|
||||
pin_extra[opt] = sv
|
||||
return schema.extend(pin_extra)
|
||||
|
||||
|
||||
def _customise_schema(config):
|
||||
config = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MODEL): cv.one_of(
|
||||
*IT8951Model.models, upper=True, space="-"
|
||||
)
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
|
||||
model_config = _model_schema(config)(config)
|
||||
|
||||
model = IT8951Model.models[config[CONF_MODEL].upper()]
|
||||
width, height = model.get_dimensions(model_config)
|
||||
|
||||
display.add_metadata(
|
||||
model_config[CONF_ID],
|
||||
width,
|
||||
height,
|
||||
# Rotation is applied per-pixel in draw_pixel_at at no extra cost, so we
|
||||
# advertise hardware rotation: LVGL routes its rotation to the driver via
|
||||
# set_rotation rather than rotating the framebuffer in software.
|
||||
has_hardware_rotation=True,
|
||||
has_writer=any(
|
||||
model_config.get(key)
|
||||
for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
|
||||
),
|
||||
# Report the configured rotation so LVGL can detect (and reject) a
|
||||
# rotation set in the display config instead of the LVGL config.
|
||||
rotation=model_config.get(CONF_ROTATION, 0),
|
||||
# The IT8951 snaps partial display refreshes to a 32-pixel X boundary
|
||||
# (see prepare_update_region_), so have LVGL round its redraw areas to
|
||||
# 32px too — this keeps flush rectangles aligned with what the panel
|
||||
# actually refreshes and avoids redundant re-rounding/over-draw.
|
||||
draw_rounding=32,
|
||||
)
|
||||
|
||||
return model_config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = _customise_schema
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
# IT8951 reads from SPI (DevInfo, VCOM, register reads) so MISO is required.
|
||||
spi.final_validate_device_schema("it8951", require_miso=True, require_mosi=True)(
|
||||
config
|
||||
)
|
||||
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
model = IT8951Model.models[config[CONF_MODEL]]
|
||||
width, height = model.get_dimensions(config)
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID], model.name, width, height)
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=False)
|
||||
|
||||
if lambda_config := config.get(CONF_LAMBDA):
|
||||
lambda_ = await cg.process_lambda(
|
||||
lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
if reset_pin := config.get(CONF_RESET_PIN):
|
||||
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
|
||||
if busy_pin := config.get(CONF_BUSY_PIN):
|
||||
cg.add(var.set_busy_pin(await cg.gpio_pin_expression(busy_pin)))
|
||||
if enable_pins := config.get(CONF_ENABLE_PIN):
|
||||
cg.add(
|
||||
var.set_enable_pins(
|
||||
[await cg.gpio_pin_expression(pin) for pin in enable_pins]
|
||||
)
|
||||
)
|
||||
cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
|
||||
if (reset_duration := config.get(CONF_RESET_DURATION)) is not None:
|
||||
cg.add(var.set_reset_duration(reset_duration))
|
||||
if config.get(CONF_INVERT_COLORS):
|
||||
cg.add(var.set_invert_colors(True))
|
||||
if config.get(CONF_SLEEP_WHEN_DONE):
|
||||
cg.add(var.set_sleep_when_done(True))
|
||||
cg.add(var.set_vcom(config[CONF_VCOM]))
|
||||
cg.add(var.set_vcom_register(config[CONF_VCOM_REGISTER]))
|
||||
if CONF_FORCE_TEMPERATURE in config:
|
||||
cg.add(var.set_force_temperature(config[CONF_FORCE_TEMPERATURE]))
|
||||
if config.get(CONF_USE_LEGACY_DPY_AREA):
|
||||
cg.add(var.set_use_legacy_dpy_area(True))
|
||||
cg.add(var.set_grayscale(config[CONF_GRAYSCALE]))
|
||||
cg.add(var.set_dithering(config[CONF_DITHERING]))
|
||||
if (mode := config.get(CONF_UPDATE_MODE)) is not None:
|
||||
cg.add(var.set_update_mode(mode))
|
||||
|
||||
transform = config.get(
|
||||
CONF_TRANSFORM,
|
||||
{
|
||||
CONF_MIRROR_X: model.get_default(CONF_MIRROR_X),
|
||||
CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y),
|
||||
},
|
||||
)
|
||||
|
||||
transform_value = sum(
|
||||
flag for key, flag in _TRANSFORM_FLAGS.items() if transform.get(key)
|
||||
)
|
||||
if transform_value:
|
||||
cg.add(var.set_transform(RawExpression(str(transform_value))))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"it8951.update",
|
||||
IT8951UpdateAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(IT8951Display),
|
||||
cv.Optional(CONF_MODE): cv.templatable(update_mode),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def it8951_update_action_to_code(config, action_id, template_arg, args):
|
||||
display_var = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, display_var)
|
||||
if mode := config.get(CONF_MODE):
|
||||
mode = await cg.templatable(mode, args, UpdateMode)
|
||||
cg.add(var.set_mode(mode))
|
||||
return var
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,373 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "it8951_defs.h"
|
||||
|
||||
namespace esphome::it8951 {
|
||||
|
||||
using namespace display;
|
||||
|
||||
// --- Bounded op queue --------------------------------------------------------
|
||||
// Fixed-capacity ring buffer used by the loop scheduler. Replaces std::deque
|
||||
// to comply with ESPHome's STL container guidelines (std::deque allocates in
|
||||
// 512-byte blocks regardless of element size). Size analysis: the deepest
|
||||
// observed scenario is UPDATE_REFRESH (10 enqueued ops) + CHECK_LUT_IDLE's
|
||||
// 5 push_front rescheduling = 14 simultaneous entries. We use 32 for a
|
||||
// comfortable margin while keeping RAM cost low (~192 bytes per instance vs
|
||||
// 512+ bytes for std::deque).
|
||||
template<typename T, size_t N> class StaticOpQueue {
|
||||
public:
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
size_t size() const { return this->count_; }
|
||||
static constexpr size_t capacity() { return N; }
|
||||
|
||||
bool push_back(const T &value) {
|
||||
if (this->count_ >= N)
|
||||
return false;
|
||||
this->data_[(this->head_ + this->count_) % N] = value;
|
||||
++this->count_;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool push_front(const T &value) {
|
||||
if (this->count_ >= N)
|
||||
return false;
|
||||
this->head_ = (this->head_ + N - 1) % N;
|
||||
this->data_[this->head_] = value;
|
||||
++this->count_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void pop_front() {
|
||||
if (this->count_ == 0)
|
||||
return;
|
||||
this->head_ = (this->head_ + 1) % N;
|
||||
--this->count_;
|
||||
}
|
||||
|
||||
const T &front() const { return this->data_[this->head_]; }
|
||||
T &front() { return this->data_[this->head_]; }
|
||||
|
||||
void clear() {
|
||||
this->head_ = 0;
|
||||
this->count_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
T data_[N]{};
|
||||
size_t head_{0};
|
||||
size_t count_{0};
|
||||
};
|
||||
|
||||
// Op queue capacity. See StaticOpQueue comment for sizing analysis.
|
||||
static constexpr size_t OP_QUEUE_SIZE = 32;
|
||||
|
||||
// --- Op queue ---------------------------------------------------------------
|
||||
// Each Op is a single CS-asserted SPI transaction (or a tiny bookkeeping
|
||||
// step). The loop processes one Op per iteration after gating on HW_RDY, so
|
||||
// the natural ESPHome loop cadence (~8-16 ms) provides inter-op pacing
|
||||
// without any blocking waits.
|
||||
//
|
||||
// Compound Ops (READ_DEV_INFO, XFER_*, DPY_BUF_AREA, ENABLE_1BPP, ...) are
|
||||
// short self-contained methods that do all their SPI work inside a single
|
||||
// CS cycle (or a small handful of cycles) and complete well under 2ms, so
|
||||
// they don't break the no-blocking budget.
|
||||
//
|
||||
// Each write-type op is a SINGLE CS-asserted transaction. The loop-level
|
||||
// HW_RDY gate ensures the controller is ready before dispatching any op, so
|
||||
// no blocking waits are needed within write ops.
|
||||
//
|
||||
// Read ops are decomposed: the command/address that triggers data preparation
|
||||
// is sent as write ops (CMD, WRITE_W), then a separate read op runs only
|
||||
// after the loop confirms HW_RDY is back HIGH (data ready). No blocking.
|
||||
enum class OpType : uint8_t {
|
||||
CMD, // single CS: CMD preamble + command word (a)
|
||||
WRITE_W, // single CS: WRITE preamble + data word (a)
|
||||
WRITE_REG, // single CS: WRITE preamble + addr(a) + value(b)
|
||||
// (caller must enqueue CMD(TCON_REG_WR) before this)
|
||||
READ_DEV_INFO, // single CS: READ preamble + dummy + read DevInfo struct
|
||||
// (caller enqueues CMD(GET_DEV_INFO) first; loop HW_RDY gate
|
||||
// ensures data is ready before this op runs)
|
||||
READ_WORD, // single CS: READ preamble + dummy + read one 16-bit word
|
||||
// into read_result_. Loop HW_RDY gate ensures data ready.
|
||||
CHECK_LUT_IDLE, // checks read_result_; if non-zero, re-enqueues read sequence
|
||||
SET_1BPP, // uses read_result_ to set UP1SR bit 2, enqueues writes
|
||||
XFER_LISAR, // set image-buffer target address (2× reg write: 4 CS transactions)
|
||||
XFER_AREA_CMD, // single CS: CMD preamble + TCON_LD_IMG_AREA
|
||||
XFER_AREA_ARGS, // single CS: WRITE preamble + 5 area-parameter words
|
||||
XFER_ROWS, // single CS: WRITE preamble + row pixel data (time-sliced)
|
||||
XFER_AREA_END, // single CS: CMD preamble + TCON_LD_IMG_END
|
||||
DPY_BUF_CMD, // single CS: CMD preamble + I80_CMD_DPY_BUF_AREA
|
||||
DPY_BUF_ARGS, // single CS: WRITE preamble + 7 display-area words
|
||||
GPIO_RESET_LOW, // drive RESET pin low
|
||||
GPIO_RESET_HIGH, // drive RESET pin high
|
||||
DELAY_MS, // park `delay_until_` for a few ms (no SPI)
|
||||
};
|
||||
|
||||
struct Op {
|
||||
OpType type;
|
||||
uint16_t a{0};
|
||||
uint16_t b{0};
|
||||
};
|
||||
|
||||
// High-level controller phases. Each phase enqueues a sequence of Ops; when
|
||||
// the queue drains, advance_phase_() runs the next phase.
|
||||
// This separation keeps per-Op work tiny and predictable.
|
||||
enum class Phase : uint8_t {
|
||||
IDLE,
|
||||
// Initialisation
|
||||
INIT_RESET, // reset pulse + wake controller + packed-write enable
|
||||
INIT_DEV_INFO, // GET_DEV_INFO and validate
|
||||
INIT_VCOM, // write configured VCOM
|
||||
INIT_TEMP, // force temperature for waveform LUT selection
|
||||
INIT_DONE, // allocate framebuffer; transition to IDLE
|
||||
// Update flow
|
||||
UPDATE_PREPARE, // do_update_, compute dirty region, decide 4bpp/1bpp
|
||||
UPDATE_TRANSFER, // one LD_IMG_AREA, time-sliced row streaming, one LD_IMG_END
|
||||
UPDATE_REFRESH, // wait LUT idle, optionally enable 1bpp, send DPY_BUF_AREA
|
||||
UPDATE_SLEEP, // optional deep sleep
|
||||
};
|
||||
|
||||
class IT8951Display : public Display,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_2MHZ> {
|
||||
public:
|
||||
IT8951Display(const char *name, uint16_t width, uint16_t height) : name_(name), width_(width), height_(height) {
|
||||
this->row_width_ = this->compute_row_width_();
|
||||
this->buffer_length_ = static_cast<size_t>(this->row_width_) * static_cast<size_t>(height);
|
||||
}
|
||||
|
||||
// --- Component lifecycle ---
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_safe_shutdown() override;
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
// --- Config setters (called from generated code) ---
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
void set_busy_pin(GPIOPin *pin) { this->busy_pin_ = pin; }
|
||||
void set_enable_pins(std::vector<GPIOPin *> pins) { this->enable_pins_ = std::move(pins); }
|
||||
void set_reset_duration(uint32_t ms) { this->reset_duration_ = ms; }
|
||||
void set_full_update_every(uint8_t n) {
|
||||
this->full_update_every_ = n;
|
||||
// Seed the counter so the very first update trips the full-update branch in
|
||||
// prepare_update_region_, giving a freshly-booted panel a clean GC16 refresh
|
||||
// before any partial (fast-waveform) updates begin.
|
||||
this->partial_update_count_ = n;
|
||||
}
|
||||
void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
|
||||
void set_sleep_when_done(bool s) { this->sleep_when_done_ = s; }
|
||||
void set_vcom(uint16_t vcom_mv) { this->vcom_ = vcom_mv; }
|
||||
void set_vcom_register(uint16_t selector) { this->vcom_register_ = selector; }
|
||||
void set_force_temperature(int16_t celsius) {
|
||||
this->force_temperature_ = celsius;
|
||||
this->force_temperature_set_ = true;
|
||||
}
|
||||
void set_use_legacy_dpy_area(bool use) { this->use_legacy_dpy_area_ = use; }
|
||||
// Pixel format: true = 4bpp grayscale framebuffer, false = packed 1bpp
|
||||
// monochrome framebuffer. Chosen at config time; the framebuffer is stored
|
||||
// in this native format and every update uses the matching transfer path.
|
||||
void set_grayscale(bool g) { this->grayscale_ = g; }
|
||||
// Monochrome only: ordered-dither pale colours (true) vs a hard 50% threshold.
|
||||
void set_dithering(bool d) { this->dithering_ = d; }
|
||||
void set_update_mode(uint16_t m) { this->default_update_mode_ = static_cast<UpdateMode>(m); }
|
||||
void set_transform(uint8_t t) {
|
||||
this->transform_ = t;
|
||||
this->update_effective_transform_();
|
||||
}
|
||||
void set_rotation(DisplayRotation rotation) override {
|
||||
Display::set_rotation(rotation);
|
||||
this->update_effective_transform_();
|
||||
}
|
||||
|
||||
// --- Display API ---
|
||||
void update() override;
|
||||
void update_mode(UpdateMode mode);
|
||||
DisplayType get_display_type() override { return this->grayscale_ ? DISPLAY_TYPE_GRAYSCALE : DISPLAY_TYPE_BINARY; }
|
||||
void fill(Color color) override;
|
||||
void clear() override { this->fill(Color::WHITE); }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
// Bulk pixel blit (used by LVGL and image rendering). Overridden to write
|
||||
// straight into the framebuffer, avoiding the base class's per-pixel
|
||||
// draw_pixel_at overhead (watchdog feed, clipping test, dirty-box clamps).
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
|
||||
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
|
||||
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; }
|
||||
int get_width_internal() override { return this->width_; }
|
||||
|
||||
// --- Coord transform / dirty region ---
|
||||
void update_effective_transform_();
|
||||
// Map display (logical) coordinates to native framebuffer coordinates by
|
||||
// applying effective_transform_ (swap/mirror). Shared by rotate_coordinates_
|
||||
// and the bulk draw_pixels_at path.
|
||||
void apply_transform_(int &x, int &y) const;
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
void reset_dirty_region_();
|
||||
|
||||
// --- Framebuffer geometry / monochrome packing ---
|
||||
// Bytes per row for the configured pixel format: 4bpp grayscale packs two
|
||||
// pixels per byte; monochrome packs eight bits per byte, rounded up to a
|
||||
// whole 16-pixel group (matching the controller's 8bpp-load / 1bpp trick).
|
||||
uint16_t compute_row_width_() const {
|
||||
return this->grayscale_ ? static_cast<uint16_t>((static_cast<uint32_t>(this->width_) + 1) / 2)
|
||||
: static_cast<uint16_t>(((static_cast<uint32_t>(this->width_) + 15) / 16) * 2);
|
||||
}
|
||||
void set_mono_pixel_(uint16_t x, uint16_t y, bool value) const;
|
||||
// Write a 4bpp grayscale nibble into the framebuffer (two pixels per byte).
|
||||
void set_gray_pixel_(uint16_t x, uint16_t y, uint8_t nibble) const;
|
||||
// Convert a color and write it at native framebuffer coordinates: a 4bpp
|
||||
// nibble in grayscale mode, or an ordered-dithered bit in monochrome mode.
|
||||
void write_pixel_native_(uint16_t x, uint16_t y, const Color &color) const;
|
||||
|
||||
// --- Op queue / loop machinery ---
|
||||
void enqueue_(OpType type, uint16_t a = 0, uint16_t b = 0);
|
||||
void prepend_(OpType type, uint16_t a = 0, uint16_t b = 0);
|
||||
bool is_busy_() const;
|
||||
void process_op_(const Op &op);
|
||||
void advance_phase_();
|
||||
void set_phase_(Phase next);
|
||||
void start_update_(UpdateMode mode);
|
||||
|
||||
// --- SPI primitives (each is one CS-asserted burst, fully non-blocking) ---
|
||||
void spi_cmd_(uint16_t cmd);
|
||||
void spi_write_word_(uint16_t value);
|
||||
void spi_write_reg_(uint16_t addr, uint16_t value);
|
||||
void spi_write_args_(const uint16_t *args, uint16_t count);
|
||||
uint16_t spi_read_word_(); // non-blocking: HW_RDY confirmed by loop gate
|
||||
void spi_read_dev_info_(); // non-blocking: HW_RDY confirmed by loop gate
|
||||
|
||||
// --- Compound Ops (small bounded helpers) ---
|
||||
void op_xfer_lisar_();
|
||||
void op_xfer_area_args_();
|
||||
void op_xfer_area_end_();
|
||||
bool op_xfer_rows_(); // returns true when current update area fully sent
|
||||
void op_dpy_buf_args_();
|
||||
void op_check_lut_idle_();
|
||||
void op_set_1bpp_();
|
||||
|
||||
// --- Phase enqueuers ---
|
||||
void enqueue_init_reset_();
|
||||
void enqueue_init_dev_info_();
|
||||
void enqueue_init_vcom_();
|
||||
void enqueue_init_temp_();
|
||||
void enqueue_update_transfer_();
|
||||
void enqueue_update_refresh_();
|
||||
void enqueue_update_sleep_();
|
||||
|
||||
bool prepare_update_region_(UpdateMode &mode);
|
||||
|
||||
// --- Recovery ---
|
||||
void recover_();
|
||||
|
||||
// --- State ---
|
||||
static constexpr uint32_t BUSY_TIMEOUT_MS = 5000;
|
||||
|
||||
StaticOpQueue<Op, OP_QUEUE_SIZE> queue_;
|
||||
Phase phase_{Phase::IDLE};
|
||||
uint32_t delay_until_{0};
|
||||
uint32_t phase_started_at_{0};
|
||||
// Requests a continuous (non-throttled) main loop while streaming image data
|
||||
// so 20ms transfer slices aren't separated by the ~16ms default loop interval.
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
|
||||
// Pending update bookkeeping
|
||||
bool update_pending_{false};
|
||||
UpdateMode pending_update_mode_{UPDATE_MODE_NONE};
|
||||
UpdateMode active_mode_{UPDATE_MODE_NONE};
|
||||
uint16_t area_x_{0}, area_y_{0}, area_w_{0}, area_h_{0};
|
||||
uint16_t transfer_row_{0};
|
||||
bool initialised_{false};
|
||||
// True once TCON_SLEEP has been sent and the controller has not been woken
|
||||
// since. The next update must issue TCON_SYS_RUN before any SPI op.
|
||||
bool asleep_{false};
|
||||
uint32_t partial_update_count_{0};
|
||||
uint32_t update_started_at_{0};
|
||||
|
||||
// Read result storage for decomposed read-modify-write op sequences
|
||||
uint16_t read_result_{0};
|
||||
|
||||
// Device info
|
||||
DevInfo dev_info_{};
|
||||
uint16_t img_buf_addr_l_{0};
|
||||
uint16_t img_buf_addr_h_{0};
|
||||
|
||||
// Configured properties
|
||||
const char *name_;
|
||||
uint16_t width_;
|
||||
uint16_t height_;
|
||||
uint16_t row_width_;
|
||||
size_t buffer_length_{};
|
||||
uint8_t *buffer_{};
|
||||
uint8_t transform_{0};
|
||||
uint8_t effective_transform_{0};
|
||||
uint8_t full_update_every_{1};
|
||||
uint32_t reset_duration_{10};
|
||||
uint16_t vcom_{2300};
|
||||
uint16_t vcom_register_{I80_CMD_VCOM_WRITE};
|
||||
int16_t force_temperature_{DEFAULT_FORCE_TEMP_C};
|
||||
bool force_temperature_set_{false};
|
||||
bool use_legacy_dpy_area_{false};
|
||||
bool invert_colors_{false};
|
||||
bool sleep_when_done_{false};
|
||||
// Pixel format selector (see set_grayscale): true = 4bpp grayscale,
|
||||
// false = packed 1bpp monochrome.
|
||||
bool grayscale_{true};
|
||||
// Monochrome dithering (see set_dithering): true = ordered dither.
|
||||
bool dithering_{true};
|
||||
UpdateMode default_update_mode_{UPDATE_MODE_NONE};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
GPIOPin *busy_pin_{nullptr};
|
||||
// GPIOs driven high during setup to power on the panel (empty if unused).
|
||||
std::vector<GPIOPin *> enable_pins_;
|
||||
|
||||
// Dirty region (pixel coordinates of bounding box of changes since last update)
|
||||
uint16_t x_low_{0}, y_low_{0}, x_high_{0}, y_high_{0};
|
||||
|
||||
// Saved data rate so we can probe slow then run fast
|
||||
uint32_t configured_data_rate_{0};
|
||||
|
||||
// Consecutive recovery attempts; used to give up rather than infinite-loop
|
||||
// when the controller is unresponsive (e.g. wiring issue).
|
||||
uint8_t recovery_attempts_{0};
|
||||
|
||||
// DevInfo read retry counter (controller often returns garbage on the first
|
||||
// read after reset; the original driver retried up to 3 times with 100ms
|
||||
// between attempts).
|
||||
uint8_t dev_info_attempts_{0};
|
||||
};
|
||||
|
||||
// --- Automation action ---
|
||||
template<typename... Ts> class IT8951UpdateAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit IT8951UpdateAction(IT8951Display *display) : display_(display) {}
|
||||
TEMPLATABLE_VALUE(UpdateMode, mode)
|
||||
|
||||
protected:
|
||||
void play(const Ts &...x) override {
|
||||
if (!this->display_->is_ready())
|
||||
return;
|
||||
if (this->mode_.has_value()) {
|
||||
this->display_->update_mode(this->mode_.value(x...));
|
||||
} else {
|
||||
this->display_->update();
|
||||
}
|
||||
}
|
||||
|
||||
IT8951Display *display_;
|
||||
};
|
||||
|
||||
} // namespace esphome::it8951
|
||||
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::it8951 {
|
||||
|
||||
struct DevInfo {
|
||||
uint16_t panel_width{0};
|
||||
uint16_t panel_height{0};
|
||||
uint16_t img_buf_addr_l{0};
|
||||
uint16_t img_buf_addr_h{0};
|
||||
uint16_t fw_version[8]{};
|
||||
uint16_t lut_version[8]{};
|
||||
};
|
||||
|
||||
// --- IT8951 SPI packet preambles ---
|
||||
static constexpr uint16_t PACKET_TYPE_CMD = 0x6000;
|
||||
static constexpr uint16_t PACKET_TYPE_WRITE = 0x0000;
|
||||
static constexpr uint16_t PACKET_TYPE_READ = 0x1000;
|
||||
|
||||
// --- Built-in I80 commands ---
|
||||
static constexpr uint16_t TCON_SYS_RUN = 0x0001;
|
||||
static constexpr uint16_t TCON_STANDBY = 0x0002;
|
||||
static constexpr uint16_t TCON_SLEEP = 0x0003;
|
||||
static constexpr uint16_t TCON_REG_RD = 0x0010;
|
||||
static constexpr uint16_t TCON_REG_WR = 0x0011;
|
||||
|
||||
static constexpr uint16_t TCON_LD_IMG = 0x0020;
|
||||
static constexpr uint16_t TCON_LD_IMG_AREA = 0x0021;
|
||||
static constexpr uint16_t TCON_LD_IMG_END = 0x0022;
|
||||
|
||||
// --- I80 user-defined commands ---
|
||||
static constexpr uint16_t I80_CMD_DPY_AREA = 0x0034;
|
||||
static constexpr uint16_t I80_CMD_GET_DEV_INFO = 0x0302;
|
||||
static constexpr uint16_t I80_CMD_DPY_BUF_AREA = 0x0037;
|
||||
static constexpr uint16_t I80_CMD_VCOM = 0x0039;
|
||||
static constexpr uint16_t I80_CMD_VCOM_READ = 0x0000;
|
||||
// VCOM write selectors. Different IT8951-driven panels accept different
|
||||
// selector values for the VCOM SET sub-command. Most panels (m5stack-m5paper,
|
||||
// generic dev kits) accept 0x0001. Some panels — notably the Seeed
|
||||
// reTerminal E1003 — only respond to selector 0x0002 and silently ignore
|
||||
// 0x0001, leaving VCOM at its default and making grayscale waveforms
|
||||
// (GC16/GL16) ineffective even though INIT still works.
|
||||
static constexpr uint16_t I80_CMD_VCOM_WRITE = 0x0001;
|
||||
static constexpr uint16_t I80_CMD_VCOM_WRITE_ALT = 0x0002;
|
||||
|
||||
// Force temperature command. The IT8951 selects waveform LUTs based on
|
||||
// panel temperature; if it is left at the controller default, panels with
|
||||
// auto-temperature disabled (notably the Seeed reTerminal E1003) will
|
||||
// run waveforms against a mismatched LUT, leaving pixels visually
|
||||
// unchanged even though the LUT engine completes a full cycle. The
|
||||
// selector word selects the operation (0x0001 = write); the value word
|
||||
// is the temperature in degrees Celsius.
|
||||
static constexpr uint16_t I80_CMD_FORCE_TEMP = 0x0040;
|
||||
static constexpr uint16_t I80_CMD_FORCE_TEMP_WRITE = 0x0001;
|
||||
static constexpr int16_t DEFAULT_FORCE_TEMP_C = 25;
|
||||
|
||||
// --- Pixel mode (bits per pixel encoding) ---
|
||||
static constexpr uint8_t PIXEL_2BPP = 0;
|
||||
static constexpr uint8_t PIXEL_3BPP = 1;
|
||||
static constexpr uint8_t PIXEL_4BPP = 2;
|
||||
static constexpr uint8_t PIXEL_8BPP = 3;
|
||||
|
||||
// --- Endian flags for LD_IMG_AREA ---
|
||||
static constexpr uint8_t LDIMG_L_ENDIAN = 0;
|
||||
static constexpr uint8_t LDIMG_B_ENDIAN = 1;
|
||||
|
||||
// --- SPI probe frequency used for initial controller handshake ---
|
||||
static constexpr uint32_t SPI_PROBE_FREQUENCY = 1'000'000;
|
||||
|
||||
// --- Refresh modes ---
|
||||
/*
|
||||
INIT The initialization (INIT) mode is
|
||||
used to completely erase the display and leave it in the white state. It is
|
||||
useful for situations where the display information in memory is not a faithful
|
||||
representation of the optical state of the display, for example, after the
|
||||
device receives power after it has been fully powered down. This waveform
|
||||
switches the display several times and leaves it in the white state.
|
||||
|
||||
DU
|
||||
The direct update (DU) is a very fast, non-flashy update. This mode supports
|
||||
transitions from any graytone to black or white only. It cannot be used to
|
||||
update to any graytone other than black or white. The fast update time for this
|
||||
mode makes it useful for response to touch sensor or pen input or menu selection
|
||||
indictors.
|
||||
|
||||
GC16
|
||||
The grayscale clearing (GC16) mode is used to update the full display and
|
||||
provide a high image quality. When GC16 is used with Full Display Update the
|
||||
entire display will update as the new image is written. If a Partial Update
|
||||
command is used the only pixels with changing graytone values will update. The
|
||||
GC16 mode has 16 unique gray levels.
|
||||
|
||||
GL16
|
||||
The GL16 waveform is primarily used to update sparse content on a white
|
||||
background, such as a page of anti-aliased text, with reduced flash. The
|
||||
GL16 waveform has 16 unique gray levels.
|
||||
|
||||
GLR16
|
||||
The GLR16 mode is used in conjunction with an image preprocessing algorithm to
|
||||
update sparse content on a white background with reduced flash and reduced image
|
||||
artifacts. The GLR16 mode supports 16 graytones. If only the even pixel states
|
||||
are used (0, 2, 4, … 30), the mode will behave exactly as a traditional GL16
|
||||
waveform mode. If a separately-supplied image preprocessing algorithm is used,
|
||||
the transitions invoked by the pixel states 29 and 31 are used to improve
|
||||
display quality. For the AF waveform, it is assured that the GLR16 waveform data
|
||||
will point to the same voltage lists as the GL16 data and does not need to be
|
||||
stored in a separate memory.
|
||||
|
||||
GLD16
|
||||
The GLD16 mode is used in conjunction with an image preprocessing algorithm to
|
||||
update sparse content on a white background with reduced flash and reduced image
|
||||
artifacts. It is recommended to be used only with the full display update. The
|
||||
GLD16 mode supports 16 graytones. If only the even pixel states are used (0, 2,
|
||||
4, … 30), the mode will behave exactly as a traditional GL16 waveform mode. If a
|
||||
separately-supplied image preprocessing algorithm is used, the transitions
|
||||
invoked by the pixel states 29 and 31 are used to refresh the background with a
|
||||
lighter flash compared to GC16 mode following a predetermined pixel map as
|
||||
encoded in the waveform file, and reduce image artifacts even more compared to
|
||||
the GLR16 mode. For the AF waveform, it is assured that the GLD16 waveform data
|
||||
will point to the same voltage lists as the GL16 data and does not need to be
|
||||
stored in a separate memory.
|
||||
|
||||
DU4
|
||||
The DU4 is a fast update time (similar to DU), non-flashy waveform. This mode
|
||||
supports transitions from any gray tone to gray tones 1,6,11,16 represented by
|
||||
pixel states [0 10 20 30]. The combination of fast update time and four gray
|
||||
tones make it useful for anti-aliased text in menus. There is a moderate
|
||||
increase in ghosting compared with GC16.
|
||||
|
||||
A2
|
||||
The A2 mode is a fast, non-flash update mode designed for fast paging turning or
|
||||
simple black/white animation. This mode supports transitions from and to black
|
||||
or white only. It cannot be used to update to any graytone other than black or
|
||||
white. The recommended update sequence to transition into repeated A2 updates is
|
||||
shown in Figure 1. The use of a white image in the transition from 4-bit to
|
||||
1-bit images will reduce ghosting and improve image quality for A2 updates.
|
||||
*/
|
||||
enum UpdateMode : uint16_t {
|
||||
UPDATE_MODE_INIT = 0,
|
||||
UPDATE_MODE_DU = 1,
|
||||
UPDATE_MODE_GC16 = 2,
|
||||
UPDATE_MODE_GL16 = 3,
|
||||
UPDATE_MODE_GLR16 = 4,
|
||||
UPDATE_MODE_GLD16 = 5,
|
||||
UPDATE_MODE_DU4 = 6,
|
||||
UPDATE_MODE_A2 = 7,
|
||||
UPDATE_MODE_NONE = 8,
|
||||
};
|
||||
|
||||
// --- Registers ---
|
||||
static constexpr uint16_t DISPLAY_REG_BASE = 0x1000;
|
||||
static constexpr uint16_t UP1SR = DISPLAY_REG_BASE + 0x138;
|
||||
static constexpr uint16_t LUTAFSR = DISPLAY_REG_BASE + 0x224;
|
||||
static constexpr uint16_t BGVR = DISPLAY_REG_BASE + 0x250;
|
||||
|
||||
static constexpr uint16_t I80CPCR = 0x0004;
|
||||
|
||||
static constexpr uint16_t MCSR_BASE_ADDR = 0x0200;
|
||||
static constexpr uint16_t LISAR = MCSR_BASE_ADDR + 0x0008;
|
||||
|
||||
// Display orientation flags
|
||||
static constexpr uint8_t TRANSFORM_NONE = 0;
|
||||
static constexpr uint8_t TRANSFORM_MIRROR_X = 1;
|
||||
static constexpr uint8_t TRANSFORM_MIRROR_Y = 2;
|
||||
static constexpr uint8_t TRANSFORM_SWAP_XY = 4;
|
||||
|
||||
} // namespace esphome::it8951
|
||||
@@ -211,14 +211,14 @@ def _notify_old_style(config):
|
||||
# The dev and latest branches will be at *least* this version, which is what matters.
|
||||
# Use GitHub releases directly to avoid PlatformIO moderation delays.
|
||||
ARDUINO_VERSIONS = {
|
||||
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
|
||||
"dev": (cv.Version(1, 13, 0), "https://github.com/libretiny-eu/libretiny.git"),
|
||||
"latest": (
|
||||
cv.Version(1, 12, 1),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
|
||||
cv.Version(1, 13, 0),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
|
||||
),
|
||||
"recommended": (
|
||||
cv.Version(1, 12, 1),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
|
||||
cv.Version(1, 13, 0),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -359,7 +359,9 @@ if __name__ == "__main__":
|
||||
check_base_code(BASE_CODE_INIT)
|
||||
# list all boards from ltchiptool
|
||||
components_dir = Path(__file__).parent.parent
|
||||
boards = [Board(b) for b in Board.get_list()]
|
||||
# Board.get_list() returns glob (filesystem) order, which is non-deterministic
|
||||
# and produces noisy diffs on regeneration; sort by board id for stable output.
|
||||
boards = sorted((Board(b) for b in Board.get_list()), key=lambda b: b.name)
|
||||
# keep track of all supported root- and chip-families
|
||||
components = set()
|
||||
families = {}
|
||||
|
||||
@@ -315,14 +315,14 @@ class LightColorValues {
|
||||
if (this->color_temperature_ <= 0) {
|
||||
return this->color_temperature_;
|
||||
}
|
||||
return 1000000.0 / this->color_temperature_;
|
||||
return 1000000.0f / this->color_temperature_;
|
||||
}
|
||||
/// Set the color temperature property of these light color values in kelvin.
|
||||
void set_color_temperature_kelvin(float color_temperature) {
|
||||
if (color_temperature <= 0) {
|
||||
return;
|
||||
}
|
||||
this->color_temperature_ = 1000000.0 / color_temperature;
|
||||
this->color_temperature_ = 1000000.0f / color_temperature;
|
||||
}
|
||||
|
||||
/// Get the cold white property of these light color values. In range 0.0 to 1.0.
|
||||
|
||||
@@ -47,7 +47,7 @@ class LightTransitionTransformer : public LightTransformer {
|
||||
LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_;
|
||||
LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->end_values_;
|
||||
if (this->changing_color_mode_)
|
||||
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
|
||||
p = p < 0.5f ? p * 2 : (p - 0.5f) * 2;
|
||||
|
||||
float v = LightTransformer::smoothed_progress(p);
|
||||
return LightColorValues::lerp(start, end, v);
|
||||
|
||||
@@ -15,26 +15,38 @@ Any manual changes WILL BE LOST on regeneration.
|
||||
from esphome.components.libretiny.const import FAMILY_LN882H
|
||||
|
||||
LN882X_BOARDS = {
|
||||
"generic-ln882hki": {
|
||||
"name": "Generic - LN882HKI",
|
||||
"generic-ln882h": {
|
||||
"name": "Generic - LN882H",
|
||||
"family": FAMILY_LN882H,
|
||||
},
|
||||
"wb02a": {
|
||||
"name": "WB02A Wi-Fi/BLE Module",
|
||||
"family": FAMILY_LN882H,
|
||||
},
|
||||
"wl2s": {
|
||||
"name": "WL2S Wi-Fi/BLE Module",
|
||||
"generic-ln882h-tuya": {
|
||||
"name": "Generic - LN882H (Tuya)",
|
||||
"family": FAMILY_LN882H,
|
||||
},
|
||||
"ln-02": {
|
||||
"name": "LN-02 Wi-Fi/BLE Module",
|
||||
"family": FAMILY_LN882H,
|
||||
},
|
||||
"ln-cb3s-v1.0": {
|
||||
"name": "LN-CB3S V1.0",
|
||||
"family": FAMILY_LN882H,
|
||||
},
|
||||
"wb02a": {
|
||||
"name": "WB02A Wi-Fi/BLE Module",
|
||||
"family": FAMILY_LN882H,
|
||||
},
|
||||
"wl2h-u": {
|
||||
"name": "WL2H-U Wi-Fi/BLE Module",
|
||||
"family": FAMILY_LN882H,
|
||||
},
|
||||
"wl2s": {
|
||||
"name": "WL2S Wi-Fi/BLE Module",
|
||||
"family": FAMILY_LN882H,
|
||||
},
|
||||
}
|
||||
|
||||
LN882X_BOARD_PINS = {
|
||||
"generic-ln882hki": {
|
||||
"generic-ln882h": {
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
@@ -153,27 +165,292 @@ LN882X_BOARD_PINS = {
|
||||
"A6": 20,
|
||||
"A7": 21,
|
||||
},
|
||||
"generic-ln882h-tuya": {
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
"WIRE0_SCL_3": 3,
|
||||
"WIRE0_SCL_4": 4,
|
||||
"WIRE0_SCL_5": 5,
|
||||
"WIRE0_SCL_6": 6,
|
||||
"WIRE0_SCL_7": 7,
|
||||
"WIRE0_SCL_8": 8,
|
||||
"WIRE0_SCL_9": 9,
|
||||
"WIRE0_SCL_10": 10,
|
||||
"WIRE0_SCL_11": 11,
|
||||
"WIRE0_SCL_12": 12,
|
||||
"WIRE0_SCL_13": 19,
|
||||
"WIRE0_SCL_14": 20,
|
||||
"WIRE0_SCL_15": 21,
|
||||
"WIRE0_SCL_16": 22,
|
||||
"WIRE0_SCL_17": 23,
|
||||
"WIRE0_SCL_18": 24,
|
||||
"WIRE0_SCL_19": 25,
|
||||
"WIRE0_SDA_0": 0,
|
||||
"WIRE0_SDA_1": 1,
|
||||
"WIRE0_SDA_2": 2,
|
||||
"WIRE0_SDA_3": 3,
|
||||
"WIRE0_SDA_4": 4,
|
||||
"WIRE0_SDA_5": 5,
|
||||
"WIRE0_SDA_6": 6,
|
||||
"WIRE0_SDA_7": 7,
|
||||
"WIRE0_SDA_8": 8,
|
||||
"WIRE0_SDA_9": 9,
|
||||
"WIRE0_SDA_10": 10,
|
||||
"WIRE0_SDA_11": 11,
|
||||
"WIRE0_SDA_12": 12,
|
||||
"WIRE0_SDA_13": 19,
|
||||
"WIRE0_SDA_14": 20,
|
||||
"WIRE0_SDA_15": 21,
|
||||
"WIRE0_SDA_16": 22,
|
||||
"WIRE0_SDA_17": 23,
|
||||
"WIRE0_SDA_18": 24,
|
||||
"WIRE0_SDA_19": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
"SERIAL1_TX": 25,
|
||||
"ADC2": 0,
|
||||
"ADC3": 1,
|
||||
"ADC4": 4,
|
||||
"ADC5": 19,
|
||||
"ADC6": 20,
|
||||
"ADC7": 21,
|
||||
"PA00": 0,
|
||||
"PA0": 0,
|
||||
"PA01": 1,
|
||||
"PA1": 1,
|
||||
"PA02": 2,
|
||||
"PA2": 2,
|
||||
"PA03": 3,
|
||||
"PA3": 3,
|
||||
"PA04": 4,
|
||||
"PA4": 4,
|
||||
"PA05": 5,
|
||||
"PA5": 5,
|
||||
"PA06": 6,
|
||||
"PA6": 6,
|
||||
"PA07": 7,
|
||||
"PA7": 7,
|
||||
"PA08": 8,
|
||||
"PA8": 8,
|
||||
"PA09": 9,
|
||||
"PA9": 9,
|
||||
"PA10": 10,
|
||||
"PA11": 11,
|
||||
"PA12": 12,
|
||||
"PB03": 19,
|
||||
"PB3": 19,
|
||||
"PB04": 20,
|
||||
"PB4": 20,
|
||||
"PB05": 21,
|
||||
"PB5": 21,
|
||||
"PB06": 22,
|
||||
"PB6": 22,
|
||||
"PB07": 23,
|
||||
"PB7": 23,
|
||||
"PB08": 24,
|
||||
"PB8": 24,
|
||||
"PB09": 25,
|
||||
"PB9": 25,
|
||||
"RX0": 3,
|
||||
"RX1": 24,
|
||||
"TX0": 2,
|
||||
"TX1": 25,
|
||||
"D0": 0,
|
||||
"D1": 1,
|
||||
"D2": 2,
|
||||
"D3": 3,
|
||||
"D4": 4,
|
||||
"D5": 5,
|
||||
"D6": 6,
|
||||
"D7": 7,
|
||||
"D8": 8,
|
||||
"D9": 9,
|
||||
"D10": 10,
|
||||
"D11": 11,
|
||||
"D12": 12,
|
||||
"D13": 19,
|
||||
"D14": 20,
|
||||
"D15": 21,
|
||||
"D16": 22,
|
||||
"D17": 23,
|
||||
"D18": 24,
|
||||
"D19": 25,
|
||||
"A2": 0,
|
||||
"A3": 1,
|
||||
"A4": 4,
|
||||
"A5": 19,
|
||||
"A6": 20,
|
||||
"A7": 21,
|
||||
},
|
||||
"ln-02": {
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
"WIRE0_SCL_3": 3,
|
||||
"WIRE0_SCL_4": 9,
|
||||
"WIRE0_SCL_5": 11,
|
||||
"WIRE0_SCL_6": 19,
|
||||
"WIRE0_SCL_7": 24,
|
||||
"WIRE0_SCL_8": 25,
|
||||
"WIRE0_SDA_0": 0,
|
||||
"WIRE0_SDA_1": 1,
|
||||
"WIRE0_SDA_2": 2,
|
||||
"WIRE0_SDA_3": 3,
|
||||
"WIRE0_SDA_4": 9,
|
||||
"WIRE0_SDA_5": 11,
|
||||
"WIRE0_SDA_6": 19,
|
||||
"WIRE0_SDA_7": 24,
|
||||
"WIRE0_SDA_8": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
"SERIAL1_TX": 25,
|
||||
"ADC2": 0,
|
||||
"ADC3": 1,
|
||||
"ADC5": 19,
|
||||
"PA00": 0,
|
||||
"PA0": 0,
|
||||
"PA01": 1,
|
||||
"PA1": 1,
|
||||
"PA02": 2,
|
||||
"PA2": 2,
|
||||
"PA03": 3,
|
||||
"PA3": 3,
|
||||
"PA09": 9,
|
||||
"PA9": 9,
|
||||
"PA11": 11,
|
||||
"PB03": 19,
|
||||
"PB3": 19,
|
||||
"PB08": 24,
|
||||
"PB8": 24,
|
||||
"PB09": 25,
|
||||
"PB9": 25,
|
||||
"RX0": 3,
|
||||
"RX1": 24,
|
||||
"SCL0": 9,
|
||||
"SDA0": 9,
|
||||
"TX0": 2,
|
||||
"TX1": 25,
|
||||
"D0": 11,
|
||||
"D1": 19,
|
||||
"D2": 3,
|
||||
"D3": 24,
|
||||
"D4": 2,
|
||||
"D5": 25,
|
||||
"D6": 1,
|
||||
"D7": 0,
|
||||
"D8": 9,
|
||||
"A0": 19,
|
||||
"A1": 1,
|
||||
"A2": 0,
|
||||
},
|
||||
"ln-cb3s-v1.0": {
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
"WIRE0_SCL_3": 3,
|
||||
"WIRE0_SCL_4": 4,
|
||||
"WIRE0_SCL_5": 5,
|
||||
"WIRE0_SCL_6": 6,
|
||||
"WIRE0_SCL_7": 9,
|
||||
"WIRE0_SCL_8": 11,
|
||||
"WIRE0_SCL_9": 20,
|
||||
"WIRE0_SCL_10": 21,
|
||||
"WIRE0_SCL_11": 22,
|
||||
"WIRE0_SCL_12": 25,
|
||||
"WIRE0_SDA_0": 0,
|
||||
"WIRE0_SDA_1": 1,
|
||||
"WIRE0_SDA_2": 2,
|
||||
"WIRE0_SDA_3": 3,
|
||||
"WIRE0_SDA_4": 4,
|
||||
"WIRE0_SDA_5": 5,
|
||||
"WIRE0_SDA_6": 6,
|
||||
"WIRE0_SDA_7": 9,
|
||||
"WIRE0_SDA_8": 11,
|
||||
"WIRE0_SDA_9": 20,
|
||||
"WIRE0_SDA_10": 21,
|
||||
"WIRE0_SDA_11": 22,
|
||||
"WIRE0_SDA_12": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_TX": 25,
|
||||
"ADC2": 0,
|
||||
"ADC3": 1,
|
||||
"ADC4": 4,
|
||||
"ADC6": 20,
|
||||
"ADC7": 21,
|
||||
"PA00": 0,
|
||||
"PA0": 0,
|
||||
"PA01": 1,
|
||||
"PA1": 1,
|
||||
"PA02": 2,
|
||||
"PA2": 2,
|
||||
"PA03": 3,
|
||||
"PA3": 3,
|
||||
"PA04": 4,
|
||||
"PA4": 4,
|
||||
"PA05": 5,
|
||||
"PA5": 5,
|
||||
"PA06": 6,
|
||||
"PA6": 6,
|
||||
"PA09": 9,
|
||||
"PA9": 9,
|
||||
"PA11": 11,
|
||||
"PB04": 20,
|
||||
"PB4": 20,
|
||||
"PB05": 21,
|
||||
"PB5": 21,
|
||||
"PB06": 22,
|
||||
"PB6": 22,
|
||||
"PB09": 25,
|
||||
"PB9": 25,
|
||||
"RX0": 3,
|
||||
"TX0": 2,
|
||||
"TX1": 25,
|
||||
"D0": 0,
|
||||
"D1": 1,
|
||||
"D2": 4,
|
||||
"D3": 5,
|
||||
"D4": 6,
|
||||
"D5": 20,
|
||||
"D6": 25,
|
||||
"D7": 9,
|
||||
"D8": 21,
|
||||
"D9": 22,
|
||||
"D10": 3,
|
||||
"D11": 2,
|
||||
"D12": 11,
|
||||
"A0": 0,
|
||||
"A1": 1,
|
||||
"A2": 4,
|
||||
"A3": 20,
|
||||
"A4": 21,
|
||||
},
|
||||
"wb02a": {
|
||||
"WIRE0_SCL_0": 1,
|
||||
"WIRE0_SCL_1": 2,
|
||||
"WIRE0_SCL_2": 3,
|
||||
"WIRE0_SCL_3": 4,
|
||||
"WIRE0_SCL_4": 5,
|
||||
"WIRE0_SCL_5": 7,
|
||||
"WIRE0_SCL_6": 9,
|
||||
"WIRE0_SCL_7": 10,
|
||||
"WIRE0_SCL_8": 24,
|
||||
"WIRE0_SCL_9": 25,
|
||||
"WIRE0_SCL_5": 6,
|
||||
"WIRE0_SCL_6": 7,
|
||||
"WIRE0_SCL_7": 9,
|
||||
"WIRE0_SCL_8": 10,
|
||||
"WIRE0_SCL_9": 24,
|
||||
"WIRE0_SCL_10": 25,
|
||||
"WIRE0_SDA_0": 1,
|
||||
"WIRE0_SDA_1": 2,
|
||||
"WIRE0_SDA_2": 3,
|
||||
"WIRE0_SDA_3": 4,
|
||||
"WIRE0_SDA_4": 5,
|
||||
"WIRE0_SDA_5": 7,
|
||||
"WIRE0_SDA_6": 9,
|
||||
"WIRE0_SDA_7": 10,
|
||||
"WIRE0_SDA_8": 24,
|
||||
"WIRE0_SDA_9": 25,
|
||||
"WIRE0_SDA_5": 6,
|
||||
"WIRE0_SDA_6": 7,
|
||||
"WIRE0_SDA_7": 9,
|
||||
"WIRE0_SDA_8": 10,
|
||||
"WIRE0_SDA_9": 24,
|
||||
"WIRE0_SDA_10": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
@@ -190,6 +467,8 @@ LN882X_BOARD_PINS = {
|
||||
"PA4": 4,
|
||||
"PA05": 5,
|
||||
"PA5": 5,
|
||||
"PA06": 6,
|
||||
"PA6": 6,
|
||||
"PA07": 7,
|
||||
"PA7": 7,
|
||||
"PA09": 9,
|
||||
@@ -206,18 +485,128 @@ LN882X_BOARD_PINS = {
|
||||
"TX0": 2,
|
||||
"TX1": 25,
|
||||
"D0": 7,
|
||||
"D1": 5,
|
||||
"D1": 6,
|
||||
"D2": 3,
|
||||
"D3": 10,
|
||||
"D4": 2,
|
||||
"D5": 1,
|
||||
"D6": 4,
|
||||
"D7": 9,
|
||||
"D8": 24,
|
||||
"D9": 25,
|
||||
"D7": 5,
|
||||
"D8": 9,
|
||||
"D9": 24,
|
||||
"D10": 25,
|
||||
"A0": 1,
|
||||
"A1": 4,
|
||||
},
|
||||
"wl2h-u": {
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
"WIRE0_SCL_3": 3,
|
||||
"WIRE0_SCL_4": 4,
|
||||
"WIRE0_SCL_5": 5,
|
||||
"WIRE0_SCL_6": 6,
|
||||
"WIRE0_SCL_7": 7,
|
||||
"WIRE0_SCL_8": 10,
|
||||
"WIRE0_SCL_9": 11,
|
||||
"WIRE0_SCL_10": 12,
|
||||
"WIRE0_SCL_11": 19,
|
||||
"WIRE0_SCL_12": 20,
|
||||
"WIRE0_SCL_13": 21,
|
||||
"WIRE0_SCL_14": 22,
|
||||
"WIRE0_SCL_15": 23,
|
||||
"WIRE0_SCL_16": 24,
|
||||
"WIRE0_SCL_17": 25,
|
||||
"WIRE0_SDA_0": 0,
|
||||
"WIRE0_SDA_1": 1,
|
||||
"WIRE0_SDA_2": 2,
|
||||
"WIRE0_SDA_3": 3,
|
||||
"WIRE0_SDA_4": 4,
|
||||
"WIRE0_SDA_5": 5,
|
||||
"WIRE0_SDA_6": 6,
|
||||
"WIRE0_SDA_7": 7,
|
||||
"WIRE0_SDA_8": 10,
|
||||
"WIRE0_SDA_9": 11,
|
||||
"WIRE0_SDA_10": 12,
|
||||
"WIRE0_SDA_11": 19,
|
||||
"WIRE0_SDA_12": 20,
|
||||
"WIRE0_SDA_13": 21,
|
||||
"WIRE0_SDA_14": 22,
|
||||
"WIRE0_SDA_15": 23,
|
||||
"WIRE0_SDA_16": 24,
|
||||
"WIRE0_SDA_17": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
"SERIAL1_TX": 25,
|
||||
"ADC2": 0,
|
||||
"ADC3": 1,
|
||||
"ADC4": 4,
|
||||
"ADC5": 19,
|
||||
"ADC6": 20,
|
||||
"ADC7": 21,
|
||||
"PA00": 0,
|
||||
"PA0": 0,
|
||||
"PA01": 1,
|
||||
"PA1": 1,
|
||||
"PA02": 2,
|
||||
"PA2": 2,
|
||||
"PA03": 3,
|
||||
"PA3": 3,
|
||||
"PA04": 4,
|
||||
"PA4": 4,
|
||||
"PA05": 5,
|
||||
"PA5": 5,
|
||||
"PA06": 6,
|
||||
"PA6": 6,
|
||||
"PA07": 7,
|
||||
"PA7": 7,
|
||||
"PA10": 10,
|
||||
"PA11": 11,
|
||||
"PA12": 12,
|
||||
"PB03": 19,
|
||||
"PB3": 19,
|
||||
"PB04": 20,
|
||||
"PB4": 20,
|
||||
"PB05": 21,
|
||||
"PB5": 21,
|
||||
"PB06": 22,
|
||||
"PB6": 22,
|
||||
"PB07": 23,
|
||||
"PB7": 23,
|
||||
"PB08": 24,
|
||||
"PB8": 24,
|
||||
"PB09": 25,
|
||||
"PB9": 25,
|
||||
"RX0": 3,
|
||||
"RX1": 24,
|
||||
"TX0": 2,
|
||||
"TX1": 25,
|
||||
"D0": 5,
|
||||
"D1": 6,
|
||||
"D2": 4,
|
||||
"D3": 1,
|
||||
"D4": 0,
|
||||
"D5": 24,
|
||||
"D6": 25,
|
||||
"D7": 7,
|
||||
"D8": 10,
|
||||
"D9": 11,
|
||||
"D10": 12,
|
||||
"D11": 19,
|
||||
"D12": 2,
|
||||
"D13": 3,
|
||||
"D14": 20,
|
||||
"D15": 21,
|
||||
"D16": 22,
|
||||
"D17": 23,
|
||||
"A0": 4,
|
||||
"A1": 1,
|
||||
"A2": 0,
|
||||
"A3": 19,
|
||||
"A4": 20,
|
||||
"A5": 21,
|
||||
},
|
||||
"wl2s": {
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
@@ -298,68 +687,6 @@ LN882X_BOARD_PINS = {
|
||||
"A1": 19,
|
||||
"A2": 1,
|
||||
},
|
||||
"ln-02": {
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
"WIRE0_SCL_3": 3,
|
||||
"WIRE0_SCL_4": 9,
|
||||
"WIRE0_SCL_5": 11,
|
||||
"WIRE0_SCL_6": 19,
|
||||
"WIRE0_SCL_7": 24,
|
||||
"WIRE0_SCL_8": 25,
|
||||
"WIRE0_SDA_0": 0,
|
||||
"WIRE0_SDA_1": 1,
|
||||
"WIRE0_SDA_2": 2,
|
||||
"WIRE0_SDA_3": 3,
|
||||
"WIRE0_SDA_4": 9,
|
||||
"WIRE0_SDA_5": 11,
|
||||
"WIRE0_SDA_6": 19,
|
||||
"WIRE0_SDA_7": 24,
|
||||
"WIRE0_SDA_8": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
"SERIAL1_TX": 25,
|
||||
"ADC2": 0,
|
||||
"ADC3": 1,
|
||||
"ADC5": 19,
|
||||
"PA00": 0,
|
||||
"PA0": 0,
|
||||
"PA01": 1,
|
||||
"PA1": 1,
|
||||
"PA02": 2,
|
||||
"PA2": 2,
|
||||
"PA03": 3,
|
||||
"PA3": 3,
|
||||
"PA09": 9,
|
||||
"PA9": 9,
|
||||
"PA11": 11,
|
||||
"PB03": 19,
|
||||
"PB3": 19,
|
||||
"PB08": 24,
|
||||
"PB8": 24,
|
||||
"PB09": 25,
|
||||
"PB9": 25,
|
||||
"RX0": 3,
|
||||
"RX1": 24,
|
||||
"SCL0": 9,
|
||||
"SDA0": 9,
|
||||
"TX0": 2,
|
||||
"TX1": 25,
|
||||
"D0": 11,
|
||||
"D1": 19,
|
||||
"D2": 3,
|
||||
"D3": 24,
|
||||
"D4": 2,
|
||||
"D5": 25,
|
||||
"D6": 1,
|
||||
"D7": 0,
|
||||
"D8": 9,
|
||||
"A0": 19,
|
||||
"A1": 1,
|
||||
"A2": 0,
|
||||
},
|
||||
}
|
||||
|
||||
BOARDS = LN882X_BOARDS
|
||||
|
||||
@@ -75,7 +75,7 @@ void LTR390Component::read_als_() {
|
||||
uint32_t als = *val;
|
||||
|
||||
if (this->light_sensor_ != nullptr) {
|
||||
float lux = ((0.6 * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_;
|
||||
float lux = ((0.6f * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_;
|
||||
this->light_sensor_->publish_state(lux);
|
||||
}
|
||||
|
||||
|
||||
@@ -500,12 +500,12 @@ void LTRAlsPs501Component::apply_lux_calculation_(AlsReadings &data) {
|
||||
// method from
|
||||
// https://github.com/fards/Ainol_fire_kernel/blob/83832cf8a3082fd8e963230f4b1984479d1f1a84/customer/drivers/lightsensor/ltr501als.c#L295
|
||||
|
||||
if (ratio < 0.45) {
|
||||
lux = 1.7743 * ch0 + 1.1059 * ch1;
|
||||
} else if (ratio < 0.64) {
|
||||
lux = 3.7725 * ch0 - 1.3363 * ch1;
|
||||
} else if (ratio < 0.85) {
|
||||
lux = 1.6903 * ch0 - 0.1693 * ch1;
|
||||
if (ratio < 0.45f) {
|
||||
lux = 1.7743f * ch0 + 1.1059f * ch1;
|
||||
} else if (ratio < 0.64f) {
|
||||
lux = 3.7725f * ch0 - 1.3363f * ch1;
|
||||
} else if (ratio < 0.85f) {
|
||||
lux = 1.6903f * ch0 - 0.1693f * ch1;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
|
||||
lux = 0.0f;
|
||||
|
||||
@@ -480,12 +480,12 @@ void LTRAlsPsComponent::apply_lux_calculation_(AlsReadings &data) {
|
||||
float inv_pfactor = this->glass_attenuation_factor_;
|
||||
float lux = 0.0f;
|
||||
|
||||
if (ratio < 0.45) {
|
||||
lux = (1.7743 * ch0 + 1.1059 * ch1);
|
||||
} else if (ratio < 0.64 && ratio >= 0.45) {
|
||||
lux = (4.2785 * ch0 - 1.9548 * ch1);
|
||||
} else if (ratio < 0.85 && ratio >= 0.64) {
|
||||
lux = (0.5926 * ch0 + 0.1185 * ch1);
|
||||
if (ratio < 0.45f) {
|
||||
lux = (1.7743f * ch0 + 1.1059f * ch1);
|
||||
} else if (ratio < 0.64f && ratio >= 0.45f) {
|
||||
lux = (4.2785f * ch0 - 1.9548f * ch1);
|
||||
} else if (ratio < 0.85f && ratio >= 0.64f) {
|
||||
lux = (0.5926f * ch0 + 0.1185f * ch1);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
|
||||
lux = 0.0f;
|
||||
|
||||
@@ -23,7 +23,7 @@ void MAX17043Component::update() {
|
||||
if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) {
|
||||
this->status_set_warning(LOG_STR("Unable to read MAX17043_VCELL"));
|
||||
} else {
|
||||
float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0;
|
||||
float voltage = (1.25f * (float) (raw_voltage >> 4)) / 1000.0f;
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ float MCP3204::read_data(uint8_t pin, bool differential) {
|
||||
this->disable();
|
||||
|
||||
uint16_t digital_value = encode_uint16(b0, b1) >> 4;
|
||||
return float(digital_value) / 4096.000 * this->reference_voltage_; // in V
|
||||
return float(digital_value) / 4096.000f * this->reference_voltage_; // in V
|
||||
}
|
||||
|
||||
} // namespace esphome::mcp3204
|
||||
|
||||
@@ -29,7 +29,9 @@ void Mcp4461Wiper::write_state(float state) {
|
||||
}
|
||||
}
|
||||
|
||||
float Mcp4461Wiper::read_state() { return (static_cast<float>(this->parent_->get_wiper_level_(this->wiper_)) / 256.0); }
|
||||
float Mcp4461Wiper::read_state() {
|
||||
return (static_cast<float>(this->parent_->get_wiper_level_(this->wiper_)) / 256.0f);
|
||||
}
|
||||
|
||||
float Mcp4461Wiper::update_state() {
|
||||
this->state_ = this->read_state();
|
||||
|
||||
@@ -24,7 +24,8 @@ void MCP4725::dump_config() {
|
||||
|
||||
// https://learn.sparkfun.com/tutorials/mcp4725-digital-to-analog-converter-hookup-guide?_ga=2.176055202.1402343014.1607953301-893095255.1606753886
|
||||
void MCP4725::write_state(float state) {
|
||||
const uint16_t value = (uint16_t) round(state * (pow(2, MCP4725_RES) - 1));
|
||||
constexpr uint16_t max_value = (1U << MCP4725_RES) - 1;
|
||||
const uint16_t value = (uint16_t) roundf(state * max_value);
|
||||
|
||||
this->write_byte_16(64, value << 4);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
static const uint8_t MCP4725_ADDR = 0x60;
|
||||
static const uint8_t MCP4725_RES = 12;
|
||||
|
||||
namespace esphome::mcp4725 {
|
||||
|
||||
static constexpr uint8_t MCP4725_ADDR = 0x60;
|
||||
static constexpr uint8_t MCP4725_RES = 12;
|
||||
|
||||
class MCP4725 final : public Component, public output::FloatOutput, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
@@ -71,10 +71,10 @@ void MICS4514Component::update() {
|
||||
float co = 0.0f;
|
||||
if (red_f > 3.4f) {
|
||||
co = 0.0;
|
||||
} else if (red_f < 0.01) {
|
||||
} else if (red_f < 0.01f) {
|
||||
co = 1000.0;
|
||||
} else {
|
||||
co = 4.2 / pow(red_f, 1.2);
|
||||
co = 4.2f / powf(red_f, 1.2f);
|
||||
}
|
||||
this->carbon_monoxide_sensor_->publish_state(co);
|
||||
}
|
||||
@@ -84,47 +84,47 @@ void MICS4514Component::update() {
|
||||
if (ox_f < 0.3f) {
|
||||
nitrogendioxide = 0.0;
|
||||
} else {
|
||||
nitrogendioxide = 0.164 * pow(ox_f, 0.975);
|
||||
nitrogendioxide = 0.164f * powf(ox_f, 0.975f);
|
||||
}
|
||||
this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide);
|
||||
}
|
||||
|
||||
if (this->methane_sensor_ != nullptr) {
|
||||
float methane = 0.0f;
|
||||
if (red_f > 0.9f || red_f < 0.5) { // outside the range->unlikely
|
||||
if (red_f > 0.9f || red_f < 0.5f) { // outside the range->unlikely
|
||||
methane = 0.0;
|
||||
} else {
|
||||
methane = 630 / pow(red_f, 4.4);
|
||||
methane = 630 / powf(red_f, 4.4f);
|
||||
}
|
||||
this->methane_sensor_->publish_state(methane);
|
||||
}
|
||||
|
||||
if (this->ethanol_sensor_ != nullptr) {
|
||||
float ethanol = 0.0f;
|
||||
if (red_f > 1.0f || red_f < 0.02) { // outside the range->unlikely
|
||||
if (red_f > 1.0f || red_f < 0.02f) { // outside the range->unlikely
|
||||
ethanol = 0.0;
|
||||
} else {
|
||||
ethanol = 1.52 / pow(red_f, 1.55);
|
||||
ethanol = 1.52f / powf(red_f, 1.55f);
|
||||
}
|
||||
this->ethanol_sensor_->publish_state(ethanol);
|
||||
}
|
||||
|
||||
if (this->hydrogen_sensor_ != nullptr) {
|
||||
float hydrogen = 0.0f;
|
||||
if (red_f > 0.9f || red_f < 0.02) { // outside the range->unlikely
|
||||
if (red_f > 0.9f || red_f < 0.02f) { // outside the range->unlikely
|
||||
hydrogen = 0.0;
|
||||
} else {
|
||||
hydrogen = 0.85 / pow(red_f, 1.75);
|
||||
hydrogen = 0.85f / powf(red_f, 1.75f);
|
||||
}
|
||||
this->hydrogen_sensor_->publish_state(hydrogen);
|
||||
}
|
||||
|
||||
if (this->ammonia_sensor_ != nullptr) {
|
||||
float ammonia = 0.0f;
|
||||
if (red_f > 0.98f || red_f < 0.2532) { // outside the ammonia range->unlikely
|
||||
if (red_f > 0.98f || red_f < 0.2532f) { // outside the ammonia range->unlikely
|
||||
ammonia = 0.0;
|
||||
} else {
|
||||
ammonia = 0.9 / pow(red_f, 4.6);
|
||||
ammonia = 0.9f / powf(red_f, 4.6f);
|
||||
}
|
||||
this->ammonia_sensor_->publish_state(ammonia);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "mmc5603.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <numbers>
|
||||
|
||||
namespace esphome::mmc5603 {
|
||||
|
||||
static const char *const TAG = "mmc5603";
|
||||
@@ -143,7 +145,7 @@ void MMC5603Component::update() {
|
||||
|
||||
const float z = 0.00625 * (raw_z - 524288);
|
||||
|
||||
const float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
|
||||
const float heading = atan2f(0.0f - x, y) * 180.0f / std::numbers::pi_v<float>;
|
||||
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading);
|
||||
|
||||
if (this->x_sensor_ != nullptr)
|
||||
|
||||
@@ -124,7 +124,6 @@ async def register_modbus_client_device(var, config):
|
||||
|
||||
async def register_modbus_server_device(var, config):
|
||||
parent = await cg.get_variable(config[CONF_MODBUS_ID])
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||
cg.add(parent.register_device(var))
|
||||
|
||||
|
||||
@@ -92,14 +92,10 @@ int32_t Modbus::tx_delay_remaining() {
|
||||
|
||||
int32_t ModbusClientHub::tx_delay_remaining() {
|
||||
const uint32_t now = millis();
|
||||
// Turnaround delay only applies after a broadcast: no response is expected, so we must give listening devices
|
||||
// quiet time to process it before the next request. For normal unicast request/response the received reply already
|
||||
// provides the inter-frame timing, so adding turnaround there just throttles throughput.
|
||||
const uint16_t turnaround = this->last_send_was_broadcast_ ? this->turnaround_delay_ms_ : 0;
|
||||
return std::max(
|
||||
{(int32_t) 0,
|
||||
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + turnaround - (now - this->last_send_)),
|
||||
(int32_t) (this->frame_delay_ms_ + turnaround - (now - this->last_modbus_byte_))});
|
||||
return std::max({(int32_t) 0,
|
||||
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + this->turnaround_delay_ms_ -
|
||||
(now - this->last_send_)),
|
||||
(int32_t) (this->frame_delay_ms_ + this->turnaround_delay_ms_ - (now - this->last_modbus_byte_))});
|
||||
}
|
||||
|
||||
bool Modbus::tx_blocked() {
|
||||
@@ -258,7 +254,7 @@ bool ModbusServerHub::parse_modbus_client_frame_() {
|
||||
std::memcpy(data, this->rx_buffer_.data() + data_offset, data_len);
|
||||
this->clear_rx_buffer_(LOG_STR("parse succeeded"), false, frame_length);
|
||||
|
||||
this->process_modbus_client_frame_(address, function_code, data, data_len);
|
||||
this->process_modbus_client_frame_(address, function_code, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -321,10 +317,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t funct
|
||||
}
|
||||
|
||||
void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *, uint16_t) {
|
||||
for (auto *device : this->devices_) {
|
||||
if (device->address_ == address) {
|
||||
ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address);
|
||||
}
|
||||
if (this->find_device_(address) != nullptr) {
|
||||
ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address);
|
||||
}
|
||||
|
||||
if (this->expecting_peer_response_ == address) {
|
||||
@@ -338,31 +332,124 @@ void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t funct
|
||||
this->expecting_peer_response_ = 0;
|
||||
}
|
||||
|
||||
void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data,
|
||||
uint16_t len) {
|
||||
bool found = false;
|
||||
|
||||
ModbusServerDevice *ModbusServerHub::find_device_(uint8_t address) {
|
||||
for (auto *device : this->devices_) {
|
||||
if (device->address_ == address) {
|
||||
found = true;
|
||||
|
||||
if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
|
||||
static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::READ_INPUT_REGISTERS) {
|
||||
device->on_modbus_read_registers(function_code, helpers::get_data<uint16_t>(data, 0),
|
||||
helpers::get_data<uint16_t>(data, 2));
|
||||
} else if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
|
||||
static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
|
||||
device->on_modbus_write_registers(function_code, std::vector<uint8_t>(data, data + len));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unsupported function code %" PRIu8, function_code);
|
||||
device->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
|
||||
}
|
||||
if (device->get_address() == address) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
bool ModbusServerHub::check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address,
|
||||
uint16_t number_of_registers) {
|
||||
if ((uint32_t) start_address + number_of_registers > 0x10000u) {
|
||||
ESP_LOGW(TAG, "Register address out of range - start: %" PRIu16 " num: %" PRIu16, start_address,
|
||||
number_of_registers);
|
||||
this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data) {
|
||||
ModbusServerDevice *device = this->find_device_(address);
|
||||
if (device == nullptr) {
|
||||
this->expecting_peer_response_ = address;
|
||||
ESP_LOGV(TAG, "Request to peer %" PRIu8 " received", address);
|
||||
return;
|
||||
}
|
||||
|
||||
ServerResponseStatus status;
|
||||
uint8_t response_buffer[modbus::MAX_RAW_SIZE];
|
||||
const uint8_t *response_data = response_buffer;
|
||||
uint16_t response_len = 0;
|
||||
|
||||
switch (static_cast<ModbusFunctionCode>(function_code)) {
|
||||
case ModbusFunctionCode::READ_HOLDING_REGISTERS:
|
||||
case ModbusFunctionCode::READ_INPUT_REGISTERS: {
|
||||
// PDU data: start address(2) + quantity(2).
|
||||
uint16_t start_address = helpers::get_data<uint16_t>(data, 0);
|
||||
uint16_t number_of_registers = helpers::get_data<uint16_t>(data, 2);
|
||||
if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_READ) {
|
||||
ESP_LOGW(TAG, "Invalid number of registers %" PRIu16, number_of_registers);
|
||||
this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) {
|
||||
return;
|
||||
}
|
||||
RegisterValues registers;
|
||||
if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::READ_HOLDING_REGISTERS) {
|
||||
status = device->on_modbus_read_holding_registers(start_address, number_of_registers, registers);
|
||||
} else {
|
||||
status = device->on_modbus_read_input_registers(start_address, number_of_registers, registers);
|
||||
}
|
||||
|
||||
// A handler that returns an exception leaves registers partially filled, so check the exception
|
||||
// first and forward it before validating the register count on the success path.
|
||||
if (status.has_value()) {
|
||||
this->send_exception_(address, function_code, status.value());
|
||||
return;
|
||||
}
|
||||
|
||||
if (registers.size() != number_of_registers) {
|
||||
ESP_LOGE(TAG, "Incorrect response %" PRIu16 " requested, %zu returned", number_of_registers, registers.size());
|
||||
this->send_exception_(address, function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
response_buffer[response_len++] = static_cast<uint8_t>(number_of_registers * 2); // actual byte count
|
||||
for (auto r : registers) {
|
||||
auto register_bytes = decode_value(r);
|
||||
response_buffer[response_len++] = register_bytes[0];
|
||||
response_buffer[response_len++] = register_bytes[1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ModbusFunctionCode::WRITE_SINGLE_REGISTER:
|
||||
case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS: {
|
||||
// PDU data: start address(2) [+ quantity(2) + byte count(1)] + register values.
|
||||
// A single-register write always targets one register; for a multiple-register write the
|
||||
// quantity is in the frame and its byte count must equal quantity * 2. The register values are
|
||||
// assembled into registers below so the handler doesn't have to know the request framing.
|
||||
uint16_t start_address = helpers::get_data<uint16_t>(data, 0);
|
||||
uint16_t number_of_registers = 1;
|
||||
uint16_t values_offset = 2; // single write: values follow the 2-byte start address
|
||||
if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
|
||||
number_of_registers = helpers::get_data<uint16_t>(data, 2);
|
||||
uint8_t number_of_bytes = helpers::get_data<uint8_t>(data, 4);
|
||||
values_offset = 5; // multiple write: values follow start address(2) + quantity(2) + byte count(1)
|
||||
if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_WRITE ||
|
||||
number_of_registers * 2 != number_of_bytes) {
|
||||
ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 " or bytes %" PRIu8, number_of_registers,
|
||||
number_of_bytes);
|
||||
this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Assemble the register values (host byte order) so the handler never sees wire framing.
|
||||
RegisterValues registers;
|
||||
for (uint16_t i = 0; i < number_of_registers; i++) {
|
||||
registers.push_back(helpers::get_data<uint16_t>(data, values_offset + i * 2));
|
||||
}
|
||||
status = device->on_modbus_write_registers(start_address, registers);
|
||||
response_data = data; // echo the request header per Modbus 6.6, 6.12
|
||||
response_len = 4;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unsupported function code %" PRIu8, function_code);
|
||||
this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
|
||||
return;
|
||||
}
|
||||
if (status.has_value()) {
|
||||
this->send_exception_(address, function_code, status.value());
|
||||
} else {
|
||||
this->send_response_(address, function_code, response_data, response_len);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +487,6 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
|
||||
format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), now - this->last_send_,
|
||||
now - this->last_modbus_byte_);
|
||||
this->last_send_ = now;
|
||||
this->last_send_was_broadcast_ = frame.size > 0 && frame.data[0] == 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -416,8 +502,7 @@ void ModbusClientHub::send_next_frame_() {
|
||||
ModbusDeviceCommand &command = this->tx_buffer_.front();
|
||||
|
||||
if (this->send_frame_(command.frame)) {
|
||||
if (!this->last_send_was_broadcast_)
|
||||
this->waiting_for_response_ = std::move(command);
|
||||
this->waiting_for_response_ = std::move(command);
|
||||
} else {
|
||||
if (command.device)
|
||||
command.device->on_modbus_not_sent();
|
||||
@@ -455,17 +540,27 @@ float Modbus::get_setup_priority() const {
|
||||
return setup_priority::BUS - 1.0f;
|
||||
}
|
||||
|
||||
void ModbusServerHub::send(uint8_t address, uint8_t function_code, const std::vector<uint8_t> &payload) {
|
||||
const uint16_t len = static_cast<uint16_t>(2 + payload.size());
|
||||
if (len > MAX_RAW_SIZE) {
|
||||
ESP_LOGE(TAG, "Server send frame too large (%" PRIu16 " bytes)", len);
|
||||
void ModbusServerHub::send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload,
|
||||
uint16_t payload_len) {
|
||||
// Build the raw frame (address + function code + payload) in a stack buffer; it's consumed
|
||||
// immediately by send_raw_ and a full raw frame never exceeds MAX_RAW_SIZE.
|
||||
if (payload_len + 2 > MAX_RAW_SIZE) {
|
||||
ESP_LOGE(TAG, "Server response too large (%" PRIu16 " bytes)", static_cast<uint16_t>(payload_len + 2));
|
||||
return;
|
||||
}
|
||||
uint8_t raw_frame[MAX_RAW_SIZE];
|
||||
raw_frame[0] = address;
|
||||
raw_frame[1] = function_code;
|
||||
std::memcpy(raw_frame + 2, payload.data(), payload.size());
|
||||
this->send_raw_(raw_frame, len);
|
||||
std::memcpy(raw_frame + 2, payload, payload_len);
|
||||
this->send_raw_(raw_frame, payload_len + 2);
|
||||
}
|
||||
|
||||
void ModbusServerHub::send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code) {
|
||||
uint8_t raw_frame[3];
|
||||
raw_frame[0] = address;
|
||||
raw_frame[1] = function_code | FUNCTION_CODE_EXCEPTION_MASK;
|
||||
raw_frame[2] = static_cast<uint8_t>(exception_code);
|
||||
this->send_raw_(raw_frame, 3);
|
||||
}
|
||||
|
||||
// Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload.
|
||||
|
||||
@@ -63,7 +63,6 @@ class Modbus : public uart::UARTDevice, public Component {
|
||||
uint32_t last_receive_check_{0};
|
||||
uint32_t last_send_{0};
|
||||
uint32_t last_send_tx_offset_{0};
|
||||
bool last_send_was_broadcast_{false};
|
||||
uint16_t frame_delay_ms_{5};
|
||||
uint16_t long_rx_buffer_delay_ms_{0};
|
||||
|
||||
@@ -130,22 +129,22 @@ class ModbusServerHub : public Modbus {
|
||||
public:
|
||||
ModbusServerHub() = default;
|
||||
void dump_config() override;
|
||||
void send(uint8_t address, uint8_t function_code, const std::vector<uint8_t> &payload);
|
||||
ESPDEPRECATED("Use ModbusServerDevice::send_raw instead. Removed in 2026.10.0", "2026.4.0")
|
||||
void send_raw(const std::vector<uint8_t> &payload) {
|
||||
this->send_raw_(payload.data(), static_cast<uint16_t>(payload.size()));
|
||||
};
|
||||
void register_device(ModbusServerDevice *device) { this->devices_.push_back(device); }
|
||||
|
||||
protected:
|
||||
friend class ModbusServerDevice;
|
||||
|
||||
void parse_modbus_frames() override;
|
||||
bool parse_modbus_client_frame_();
|
||||
// Parsers need to handle standard (ModbusFunctionCode) and custom (uint8_t) function codes, so we use uint8_t here.
|
||||
void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override;
|
||||
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len);
|
||||
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data);
|
||||
ModbusServerDevice *find_device_(uint8_t address);
|
||||
// Returns true if [start_address, start_address + number_of_registers) fits in the 16-bit address space.
|
||||
// On failure, logs and sends an ILLEGAL_DATA_ADDRESS exception to the client.
|
||||
bool check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address,
|
||||
uint16_t number_of_registers);
|
||||
void send_raw_(const uint8_t *payload, uint16_t len);
|
||||
void send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code);
|
||||
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len);
|
||||
uint8_t expecting_peer_response_{0};
|
||||
std::vector<ModbusServerDevice *> devices_;
|
||||
|
||||
@@ -200,35 +199,41 @@ class ModbusClientDevice {
|
||||
// This is for compatibility with external components using the former class name
|
||||
using ModbusDevice = ModbusClientDevice;
|
||||
|
||||
// Result of a server register handler: std::nullopt means success, otherwise the Modbus exception code to return.
|
||||
using ServerResponseStatus = std::optional<ModbusExceptionCode>;
|
||||
// Register values exchanged with server handlers, in host byte order. Sized at the larger of the two protocol
|
||||
// maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by
|
||||
// the capacity of this type.
|
||||
using RegisterValues = StaticVector<uint16_t, MAX_NUM_OF_REGISTERS_TO_READ>;
|
||||
|
||||
class ModbusServerDevice {
|
||||
public:
|
||||
ModbusServerDevice() = default;
|
||||
ModbusServerDevice(ModbusServerHub *parent, uint8_t address) : parent_(parent), address_(address) {}
|
||||
virtual ~ModbusServerDevice() = default;
|
||||
ModbusServerDevice() = default;
|
||||
// Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five).
|
||||
ModbusServerDevice(const ModbusServerDevice &) = delete;
|
||||
ModbusServerDevice &operator=(const ModbusServerDevice &) = delete;
|
||||
ModbusServerDevice(ModbusServerDevice &&) = delete;
|
||||
ModbusServerDevice &operator=(ModbusServerDevice &&) = delete;
|
||||
void set_parent(ModbusServerHub *parent) { this->parent_ = parent; }
|
||||
void set_address(uint8_t address) { this->address_ = address; }
|
||||
virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){};
|
||||
virtual void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data){};
|
||||
void send(uint8_t function, const std::vector<uint8_t> &payload) {
|
||||
this->parent_->send(this->address_, function, payload);
|
||||
}
|
||||
void send_raw(const std::vector<uint8_t> &payload) {
|
||||
this->parent_->send_raw_(payload.data(), static_cast<uint16_t>(payload.size()));
|
||||
}
|
||||
void send_error(uint8_t function_code, ModbusExceptionCode exception_code) {
|
||||
uint8_t error_response[3] = {this->address_, uint8_t(function_code | FUNCTION_CODE_EXCEPTION_MASK),
|
||||
static_cast<uint8_t>(exception_code)};
|
||||
this->parent_->send_raw_(error_response, 3);
|
||||
}
|
||||
uint8_t get_address() const { return this->address_; }
|
||||
virtual ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers,
|
||||
RegisterValues ®isters) {
|
||||
return ModbusExceptionCode::ILLEGAL_FUNCTION;
|
||||
};
|
||||
virtual ServerResponseStatus on_modbus_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
|
||||
RegisterValues ®isters) {
|
||||
return this->on_modbus_read_registers(start_address, number_of_registers, registers);
|
||||
};
|
||||
virtual ServerResponseStatus on_modbus_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
|
||||
RegisterValues ®isters) {
|
||||
return this->on_modbus_read_registers(start_address, number_of_registers, registers);
|
||||
};
|
||||
virtual ServerResponseStatus on_modbus_write_registers(uint16_t start_address, const RegisterValues ®isters) {
|
||||
return ModbusExceptionCode::ILLEGAL_FUNCTION;
|
||||
};
|
||||
|
||||
protected:
|
||||
friend ModbusServerHub;
|
||||
|
||||
ModbusServerHub *parent_{nullptr};
|
||||
uint8_t address_{0};
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ static constexpr uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D
|
||||
// Smallest possible frame is 4 bytes (custom function with no data): address(1) + function(1) + CRC(2)
|
||||
static constexpr uint16_t MIN_FRAME_SIZE = 4;
|
||||
static constexpr uint16_t MAX_PDU_SIZE = 253; // Max PDU size is 256 - address(1) - CRC(2) = 253
|
||||
static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 255 - CRC(2) = 254
|
||||
static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 256 - CRC(2) = 254
|
||||
static constexpr uint16_t MAX_FRAME_SIZE = 256;
|
||||
/// End of Modbus definitions
|
||||
} // namespace esphome::modbus
|
||||
|
||||
@@ -101,53 +101,19 @@ static size_t required_payload_size(SensorValueType sensor_value_type) {
|
||||
}
|
||||
}
|
||||
|
||||
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
|
||||
switch (value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
case SensorValueType::S_WORD:
|
||||
data.push_back(value & 0xFFFF);
|
||||
break;
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::S_DWORD:
|
||||
case SensorValueType::FP32:
|
||||
data.push_back((value & 0xFFFF0000) >> 16);
|
||||
data.push_back(value & 0xFFFF);
|
||||
break;
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::S_DWORD_R:
|
||||
case SensorValueType::FP32_R:
|
||||
data.push_back(value & 0xFFFF);
|
||||
data.push_back((value & 0xFFFF0000) >> 16);
|
||||
break;
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::S_QWORD:
|
||||
data.push_back((value & 0xFFFF000000000000) >> 48);
|
||||
data.push_back((value & 0xFFFF00000000) >> 32);
|
||||
data.push_back((value & 0xFFFF0000) >> 16);
|
||||
data.push_back(value & 0xFFFF);
|
||||
break;
|
||||
case SensorValueType::U_QWORD_R:
|
||||
case SensorValueType::S_QWORD_R:
|
||||
data.push_back(value & 0xFFFF);
|
||||
data.push_back((value & 0xFFFF0000) >> 16);
|
||||
data.push_back((value & 0xFFFF00000000) >> 32);
|
||||
data.push_back((value & 0xFFFF000000000000) >> 48);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast<uint16_t>(value_type));
|
||||
break;
|
||||
}
|
||||
void log_unsupported_value_type(SensorValueType value_type) {
|
||||
ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast<uint16_t>(value_type));
|
||||
}
|
||||
|
||||
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
|
||||
int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset,
|
||||
uint32_t bitmask, bool *error_return) {
|
||||
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
|
||||
|
||||
// Validate offset against the buffer for all types, including RAW/unsupported, so
|
||||
// a malformed or misconfigured frame still produces an error log.
|
||||
if (static_cast<size_t>(offset) > data.size()) {
|
||||
if (static_cast<size_t>(offset) > size) {
|
||||
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast<unsigned int>(sensor_value_type),
|
||||
static_cast<unsigned int>(offset), data.size());
|
||||
static_cast<unsigned int>(offset), size);
|
||||
if (error_return)
|
||||
*error_return = true;
|
||||
return value;
|
||||
@@ -158,10 +124,9 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
|
||||
return value;
|
||||
}
|
||||
|
||||
if (data.size() - offset < required_size) {
|
||||
if (size - offset < required_size) {
|
||||
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu",
|
||||
static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), data.size(),
|
||||
required_size);
|
||||
static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), size, required_size);
|
||||
if (error_return)
|
||||
*error_return = true;
|
||||
return value;
|
||||
@@ -214,6 +179,31 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
|
||||
return value;
|
||||
}
|
||||
|
||||
int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type,
|
||||
bool *error_return) {
|
||||
const size_t required_size = required_payload_size(sensor_value_type);
|
||||
if (required_size == 0) {
|
||||
return 0; // RAW/unsupported: nothing to read
|
||||
}
|
||||
const size_t required_words = required_size / 2;
|
||||
if (required_words > count) {
|
||||
ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%zu",
|
||||
static_cast<unsigned int>(sensor_value_type), count, required_words);
|
||||
if (error_return)
|
||||
*error_return = true;
|
||||
return 0;
|
||||
}
|
||||
// Serialize the needed words back to big-endian bytes and reuse the audited byte decoder so the
|
||||
// sign-extension behaviour stays identical to the wire path.
|
||||
uint8_t bytes[8]; // at most 4 registers (QWORD)
|
||||
for (size_t i = 0; i < required_words; i++) {
|
||||
uint16_t reg = registers[i];
|
||||
bytes[i * 2] = static_cast<uint8_t>(reg >> 8);
|
||||
bytes[i * 2 + 1] = static_cast<uint8_t>(reg & 0xFF);
|
||||
}
|
||||
return payload_to_number(bytes, required_size, sensor_value_type, 0, 0xFFFFFFFF, error_return);
|
||||
}
|
||||
|
||||
StaticVector<uint8_t, MAX_PDU_SIZE> create_client_pdu(ModbusFunctionCode function_code, uint16_t start_address,
|
||||
uint16_t number_of_entities, const uint8_t *values,
|
||||
size_t values_len) {
|
||||
|
||||
@@ -224,24 +224,77 @@ template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Convert float value to vector<uint16_t> suitable for sending
|
||||
* @param data target for payload
|
||||
* @param value float value to convert
|
||||
* @param value_type defines if 16/32 or FP32 is used
|
||||
* @return vector containing the modbus register words in correct order
|
||||
*/
|
||||
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
|
||||
// Logs an error for an unsupported value type. Defined in the .cpp so logging stays out of headers.
|
||||
void log_unsupported_value_type(SensorValueType value_type);
|
||||
|
||||
/** Convert vector<uint8_t> response payload to number.
|
||||
/** Append the Modbus register words for value to data.
|
||||
* Works with any container exposing push_back(uint16_t) (e.g. std::vector or StaticVector).
|
||||
*/
|
||||
template<typename Container> void number_to_payload(Container &data, int64_t value, SensorValueType value_type) {
|
||||
switch (value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
case SensorValueType::S_WORD:
|
||||
data.push_back(value & 0xFFFF);
|
||||
break;
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::S_DWORD:
|
||||
case SensorValueType::FP32:
|
||||
data.push_back((value & 0xFFFF0000) >> 16);
|
||||
data.push_back(value & 0xFFFF);
|
||||
break;
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::S_DWORD_R:
|
||||
case SensorValueType::FP32_R:
|
||||
data.push_back(value & 0xFFFF);
|
||||
data.push_back((value & 0xFFFF0000) >> 16);
|
||||
break;
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::S_QWORD:
|
||||
data.push_back((value & 0xFFFF000000000000) >> 48);
|
||||
data.push_back((value & 0xFFFF00000000) >> 32);
|
||||
data.push_back((value & 0xFFFF0000) >> 16);
|
||||
data.push_back(value & 0xFFFF);
|
||||
break;
|
||||
case SensorValueType::U_QWORD_R:
|
||||
case SensorValueType::S_QWORD_R:
|
||||
data.push_back(value & 0xFFFF);
|
||||
data.push_back((value & 0xFFFF0000) >> 16);
|
||||
data.push_back((value & 0xFFFF00000000) >> 32);
|
||||
data.push_back((value & 0xFFFF000000000000) >> 48);
|
||||
break;
|
||||
default:
|
||||
log_unsupported_value_type(value_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert a raw response payload to a number.
|
||||
* @param data payload with the data to convert
|
||||
* @param size number of bytes available in data
|
||||
* @param sensor_value_type defines if 16/32/64 bits or FP32 is used
|
||||
* @param offset offset to the data in data
|
||||
* @param bitmask bitmask used for masking and shifting
|
||||
* @return 64-bit number of the payload
|
||||
*/
|
||||
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
|
||||
int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset,
|
||||
uint32_t bitmask, bool *error_return = nullptr);
|
||||
|
||||
/** Convert vector<uint8_t> response payload to number. */
|
||||
inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
|
||||
uint32_t bitmask, bool *error_return = nullptr) {
|
||||
return payload_to_number(data.data(), data.size(), sensor_value_type, offset, bitmask, error_return);
|
||||
}
|
||||
|
||||
/** Reconstruct a number from register words (host byte order). Inverse of number_to_payload.
|
||||
* Decodes the value at the start of the given span; advance the pointer to read successive values.
|
||||
* @param registers register values in host byte order
|
||||
* @param count number of registers available in registers
|
||||
* @param sensor_value_type defines if 16/32/64 bits or FP32 is used
|
||||
* @return 64-bit number of the registers
|
||||
*/
|
||||
int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type,
|
||||
bool *error_return = nullptr);
|
||||
|
||||
/** Create a modbus clinet pdu for reading/writing single/multiple coils/register/inputs.
|
||||
* @param function_code the modbus function code to use. One of:
|
||||
* READ_COILS
|
||||
|
||||
@@ -3,26 +3,18 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::modbus_server {
|
||||
using modbus::ModbusFunctionCode;
|
||||
using modbus::ModbusExceptionCode;
|
||||
using modbus::helpers::payload_to_number;
|
||||
using modbus::helpers::registers_to_number;
|
||||
|
||||
static const char *const TAG = "modbus_server";
|
||||
|
||||
void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
|
||||
uint16_t number_of_registers) {
|
||||
modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t start_address,
|
||||
uint16_t number_of_registers,
|
||||
modbus::RegisterValues ®isters) {
|
||||
ESP_LOGV(TAG,
|
||||
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
|
||||
"0x%X.",
|
||||
this->address_, function_code, start_address, number_of_registers);
|
||||
"Received read holding/input registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%X.",
|
||||
this->address_, start_address, number_of_registers);
|
||||
|
||||
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
|
||||
ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 ". Sending exception response.", number_of_registers);
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint16_t> sixteen_bit_response;
|
||||
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
|
||||
bool found = false;
|
||||
for (auto *server_register : this->server_registers_) {
|
||||
@@ -36,10 +28,7 @@ void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t star
|
||||
server_register->address, static_cast<size_t>(server_register->value_type),
|
||||
server_register->register_count, server_register->format_value(value, value_buf, sizeof(value_buf)));
|
||||
|
||||
std::vector<uint16_t> payload;
|
||||
payload.reserve(server_register->register_count * 2);
|
||||
modbus::helpers::number_to_payload(payload, value, server_register->value_type);
|
||||
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
|
||||
modbus::helpers::number_to_payload(registers, value, server_register->value_type);
|
||||
current_address += server_register->register_count;
|
||||
found = true;
|
||||
break;
|
||||
@@ -53,92 +42,37 @@ void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t star
|
||||
"Could not match any register to address 0x%02X, but default allowed. "
|
||||
"Returning default value: %" PRIu16 ".",
|
||||
current_address, this->server_courtesy_response_.register_value);
|
||||
sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
|
||||
registers.push_back(this->server_courtesy_response_.register_value);
|
||||
current_address += 1; // Just increment by 1, as the default response is a single register
|
||||
} else {
|
||||
ESP_LOGW(TAG,
|
||||
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
|
||||
current_address);
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
return;
|
||||
return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> response;
|
||||
if (number_of_registers != sixteen_bit_response.size())
|
||||
ESP_LOGW(TAG, "Response size not matched to request register count.");
|
||||
response.push_back(sixteen_bit_response.size() * 2); // actual byte count
|
||||
for (auto v : sixteen_bit_response) {
|
||||
auto decoded_value = decode_value(v);
|
||||
response.push_back(decoded_value[0]);
|
||||
response.push_back(decoded_value[1]);
|
||||
}
|
||||
this->send(function_code, response);
|
||||
return {};
|
||||
}
|
||||
|
||||
void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) {
|
||||
uint16_t number_of_registers;
|
||||
uint16_t payload_offset;
|
||||
modbus::ServerResponseStatus ModbusServer::on_modbus_write_registers(uint16_t start_address,
|
||||
const modbus::RegisterValues ®isters) {
|
||||
// registers holds the values to write in host byte order; its size is the register count.
|
||||
ESP_LOGV(TAG, "Received write registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%zX.",
|
||||
this->address_, start_address, registers.size());
|
||||
|
||||
if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
|
||||
if (data.size() < 5) {
|
||||
ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size());
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
|
||||
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
|
||||
ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 ". Sending exception response.", number_of_registers);
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
uint16_t payload_size = data[4];
|
||||
if (payload_size != number_of_registers * 2) {
|
||||
ESP_LOGW(TAG,
|
||||
"Payload size of %" PRIu16 " bytes is not 2 times the number of registers (%" PRIu16
|
||||
"). Sending exception response.",
|
||||
payload_size, number_of_registers);
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
if (data.size() < 5 + payload_size) {
|
||||
ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(),
|
||||
5 + payload_size);
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
payload_offset = 5;
|
||||
} else if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
|
||||
if (data.size() < 4) {
|
||||
ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size());
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
|
||||
return;
|
||||
}
|
||||
number_of_registers = 1;
|
||||
payload_offset = 2;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8);
|
||||
ESP_LOGD(TAG,
|
||||
"Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
|
||||
"0x%X.",
|
||||
this->address_, function_code, start_address, number_of_registers);
|
||||
|
||||
auto for_each_register = [this, start_address, number_of_registers, payload_offset](
|
||||
const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool {
|
||||
uint16_t offset = payload_offset;
|
||||
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
|
||||
auto for_each_register =
|
||||
[this, start_address,
|
||||
®isters](const std::function<bool(ServerRegister *, uint16_t register_offset)> &callback) -> bool {
|
||||
uint16_t register_offset = 0;
|
||||
for (uint32_t current_address = start_address; current_address < start_address + registers.size();) {
|
||||
bool ok = false;
|
||||
for (auto *server_register : this->server_registers_) {
|
||||
if (server_register->address == current_address) {
|
||||
ok = callback(server_register, offset);
|
||||
ok = callback(server_register, register_offset);
|
||||
current_address += server_register->register_count;
|
||||
offset += server_register->register_count * sizeof(uint16_t);
|
||||
register_offset += server_register->register_count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -150,36 +84,41 @@ void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::v
|
||||
return true;
|
||||
};
|
||||
|
||||
// check all registers are writable before writing to any of them:
|
||||
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
|
||||
return server_register->write_lambda != nullptr;
|
||||
})) {
|
||||
ESP_LOGW(TAG, "Invalid register address. Sending exception response.");
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
return;
|
||||
}
|
||||
|
||||
// Actually write to the registers:
|
||||
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
|
||||
bool error = false;
|
||||
int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF, &error);
|
||||
if (error) {
|
||||
return false;
|
||||
} else {
|
||||
return server_register->write_lambda(number);
|
||||
// Pre-flight: every targeted register must be writable AND have its full value present in the request,
|
||||
// so we never apply a partial write before discovering a problem. The commit pass below re-runs
|
||||
// registers_to_number rather than caching the decoded values: using the same function for the check and
|
||||
// the write keeps a single source of truth for the decode bound, independent of how register_count was set.
|
||||
ModbusExceptionCode precheck = ModbusExceptionCode::ILLEGAL_DATA_ADDRESS; // unmatched or unwritable register
|
||||
if (!for_each_register([&precheck, ®isters](ServerRegister *server_register, uint16_t register_offset) -> bool {
|
||||
if (server_register->write_lambda == nullptr) {
|
||||
return false; // unwritable -> ILLEGAL_DATA_ADDRESS
|
||||
}
|
||||
bool error = false;
|
||||
registers_to_number(registers.data() + register_offset, registers.size() - register_offset,
|
||||
server_register->value_type, &error);
|
||||
if (error) {
|
||||
precheck = ModbusExceptionCode::ILLEGAL_DATA_VALUE; // request doesn't supply the full value
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
ESP_LOGW(TAG, "Could not write all registers. Sending exception response.");
|
||||
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
|
||||
return;
|
||||
ESP_LOGW(TAG, "Write request rejected before applying any register. Sending exception response.");
|
||||
return precheck;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> response;
|
||||
response.reserve(6);
|
||||
response.push_back(this->address_);
|
||||
response.push_back(function_code);
|
||||
response.insert(response.end(), data.begin(), data.begin() + 4);
|
||||
this->send_raw(response);
|
||||
// Commit: every value is known writable and decodable, so the only failure now is a user write callback
|
||||
// rejecting the value at runtime -- which cannot be rolled back.
|
||||
if (!for_each_register([®isters](ServerRegister *server_register, uint16_t register_offset) {
|
||||
int64_t number = registers_to_number(registers.data() + register_offset, registers.size() - register_offset,
|
||||
server_register->value_type);
|
||||
return server_register->write_lambda(number);
|
||||
})) {
|
||||
ESP_LOGW(TAG, "A register write callback failed mid-sequence; earlier writes were already applied.");
|
||||
return ModbusExceptionCode::SERVICE_DEVICE_FAILURE;
|
||||
}
|
||||
|
||||
// Success: the caller builds the write response (an echo of the request header).
|
||||
return {};
|
||||
}
|
||||
|
||||
void ModbusServer::dump_config() {
|
||||
|
||||
@@ -98,9 +98,11 @@ class ModbusServer : public Component, public modbus::ModbusServerDevice {
|
||||
/// Registers a server register with the controller. Called by esphomes code generator
|
||||
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
|
||||
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
|
||||
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
|
||||
modbus::ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers,
|
||||
modbus::RegisterValues ®isters) final;
|
||||
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
|
||||
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
|
||||
modbus::ServerResponseStatus on_modbus_write_registers(uint16_t start_address,
|
||||
const modbus::RegisterValues ®isters) final;
|
||||
/// Called by esphome generated code to set the server courtesy response object
|
||||
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
|
||||
this->server_courtesy_response_ = server_courtesy_response;
|
||||
|
||||
@@ -75,16 +75,16 @@ void MPL3115A2Component::update() {
|
||||
float altitude = 0, pressure = 0;
|
||||
if (this->altitude_ != nullptr) {
|
||||
int32_t alt = encode_uint32(buffer[0], buffer[1], buffer[2], 0);
|
||||
altitude = float(alt) / 65536.0;
|
||||
altitude = float(alt) / 65536.0f;
|
||||
this->altitude_->publish_state(altitude);
|
||||
} else {
|
||||
uint32_t p = encode_uint32(0, buffer[0], buffer[1], buffer[2]);
|
||||
pressure = float(p) / 6400.0;
|
||||
pressure = float(p) / 6400.0f;
|
||||
if (this->pressure_ != nullptr)
|
||||
this->pressure_->publish_state(pressure);
|
||||
}
|
||||
int16_t t = encode_uint16(buffer[3], buffer[4]);
|
||||
float temperature = float(t) / 256.0;
|
||||
float temperature = float(t) / 256.0f;
|
||||
if (this->temperature_ != nullptr)
|
||||
this->temperature_->publish_state(temperature);
|
||||
|
||||
|
||||
@@ -115,9 +115,9 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// max_temp
|
||||
root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature();
|
||||
// target_temp_step
|
||||
root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1;
|
||||
root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1f;
|
||||
// current_temp_step
|
||||
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1;
|
||||
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1f;
|
||||
// temperature units are always coerced to Celsius internally
|
||||
root[MQTT_TEMPERATURE_UNIT] = "C";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ static const char *const TAG = "msa3xx";
|
||||
const uint8_t MSA_3XX_PART_ID = 0x13;
|
||||
|
||||
const float GRAVITY_EARTH = 9.80665f;
|
||||
const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9); // LSB to 1 LSB = 3.9mg = 0.0039g
|
||||
const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9f); // LSB to 1 LSB = 3.9mg = 0.0039g
|
||||
const float G_OFFSET_MIN = -4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe
|
||||
const float G_OFFSET_MAX = 4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe
|
||||
|
||||
|
||||
@@ -221,6 +221,27 @@ async def to_code(config):
|
||||
zephyr_add_prj_conf("NET_IPV6", True)
|
||||
zephyr_add_prj_conf("NET_TCP", True)
|
||||
zephyr_add_prj_conf("NET_UDP", True)
|
||||
# The nRF Connect SDK replaces mbedTLS with PSA/Oberon crypto and does not provide the
|
||||
# legacy mbedtls_md5() symbol that Zephyr's RFC 6528 TCP ISN generator links against
|
||||
# (selecting MBEDTLS_MAC_MD5_ENABLED does not bring in the legacy C API here). Disable it so
|
||||
# TCP links; Zephyr falls back to sys_rand32_get() for the ISN (randomized, but not the
|
||||
# RFC 6528 keyed hash).
|
||||
zephyr_add_prj_conf("NET_TCP_ISN_RFC6528", False)
|
||||
# Enlarge the Zephyr network buffer pool and TCP windows for the Thread path.
|
||||
# Zephyr's defaults are tiny: NET_BUF_TX_COUNT=16 * NET_BUF_DATA_SIZE=128 is only
|
||||
# ~2 KB of TX data -- barely one 1280-byte IPv6 packet once 6LoWPAN fragments it.
|
||||
# The ESPHome API entity-sync burst overruns that instantly, so socket writes fail
|
||||
# with ENOBUFS ("Buffer full") and the connection is dropped. ESP32 sidesteps this
|
||||
# by enlarging the lwIP TCP window (CONFIG_LWIP_TCP_* above); give Zephyr the
|
||||
# equivalent headroom, sized to RAM and the Thread 1280-byte MTU (not ESP32's 64 KB).
|
||||
# The bounded send window also provides flow control so TCP stops queueing past
|
||||
# what the buffer pool can hold instead of erroring.
|
||||
zephyr_add_prj_conf("NET_PKT_RX_COUNT", 24)
|
||||
zephyr_add_prj_conf("NET_PKT_TX_COUNT", 24)
|
||||
zephyr_add_prj_conf("NET_BUF_RX_COUNT", 48)
|
||||
zephyr_add_prj_conf("NET_BUF_TX_COUNT", 48)
|
||||
zephyr_add_prj_conf("NET_TCP_MAX_RECV_WINDOW_SIZE", 2280)
|
||||
zephyr_add_prj_conf("NET_TCP_MAX_SEND_WINDOW_SIZE", 2280)
|
||||
|
||||
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
|
||||
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
|
||||
|
||||
@@ -176,7 +176,7 @@ void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_pr
|
||||
void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("page", "page %i", page); }
|
||||
|
||||
void Nextion::set_backlight_brightness(float brightness) {
|
||||
if (brightness < 0 || brightness > 1.0) {
|
||||
if (brightness < 0 || brightness > 1.0f) {
|
||||
ESP_LOGD(TAG, "Brightness out of bounds (0-1.0)");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from esphome import pins
|
||||
@@ -486,6 +487,16 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
|
||||
from esphome.__main__ import check_permissions
|
||||
from esphome.upload_targets import PortType, get_port_type
|
||||
|
||||
if KEY_ZEPHYR not in CORE.data:
|
||||
platform_config = config.get(CORE.target_platform)
|
||||
if not platform_config:
|
||||
raise EsphomeError(
|
||||
"nRF52 platform configuration is missing; "
|
||||
"please re-validate and recompile."
|
||||
)
|
||||
set_core_data(platform_config)
|
||||
set_framework(platform_config)
|
||||
|
||||
mcumgr_device: str | None = None
|
||||
|
||||
if get_port_type(host) == PortType.SERIAL:
|
||||
@@ -494,17 +505,122 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
|
||||
mcumgr_device = host
|
||||
else:
|
||||
if not CORE.using_toolchain_platformio:
|
||||
raise EsphomeError("Not implemented yet")
|
||||
result = _upload_using_platformio(config, host, ["-t", "upload"])
|
||||
if result != 0:
|
||||
raise EsphomeError(f"Upload failed with result: {result}")
|
||||
return True # Handled: platformio serial upload
|
||||
bootloader = zephyr_data()[KEY_BOOTLOADER]
|
||||
if bootloader not in (
|
||||
BOOTLOADER_ADAFRUIT,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
):
|
||||
raise EsphomeError("Not implemented yet")
|
||||
check_and_install()
|
||||
paths = get_build_paths()
|
||||
env = get_build_env()
|
||||
build_dir = CORE.relative_pioenvs_path(CORE.name)
|
||||
dfu_package = build_dir / "firmware.zip"
|
||||
if not dfu_package.is_file():
|
||||
raise EsphomeError("Firmware not found. Please compile first.")
|
||||
import time as _time
|
||||
|
||||
import serial as _serial
|
||||
import serial.tools.list_ports as _list_ports
|
||||
|
||||
try:
|
||||
ser = _serial.Serial(host, baudrate=1200, timeout=1)
|
||||
ser.close()
|
||||
except _serial.SerialException as err:
|
||||
raise EsphomeError(f"Failed to open {host}: {err}") from err
|
||||
|
||||
# Wait for device to reset (port disappears)
|
||||
deadline = _time.monotonic() + 5
|
||||
while _time.monotonic() < deadline:
|
||||
_time.sleep(0.1)
|
||||
if host not in {p.device for p in _list_ports.comports()}:
|
||||
break
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Device did not leave %s within 5 s; "
|
||||
"it may not have entered bootloader mode",
|
||||
host,
|
||||
)
|
||||
|
||||
# Wait for DFU port to reappear
|
||||
deadline = _time.monotonic() + 10
|
||||
while _time.monotonic() < deadline:
|
||||
_time.sleep(0.1)
|
||||
if host in {p.device for p in _list_ports.comports()}:
|
||||
break
|
||||
else:
|
||||
raise EsphomeError(
|
||||
f"DFU port {host!r} did not reappear within 10 s. "
|
||||
"Check that the device entered DFU mode."
|
||||
)
|
||||
|
||||
# Wait for udev to finish setting up device permissions
|
||||
deadline = _time.monotonic() + 5
|
||||
while _time.monotonic() < deadline:
|
||||
try:
|
||||
check_permissions(host)
|
||||
break
|
||||
except EsphomeError:
|
||||
_time.sleep(0.05)
|
||||
else:
|
||||
check_permissions(host) # raises with helpful message
|
||||
|
||||
python = str(paths["python_executable"])
|
||||
if not run_command_ok(
|
||||
[
|
||||
python,
|
||||
"-m",
|
||||
"nordicsemi.__main__",
|
||||
"dfu",
|
||||
"serial",
|
||||
"-pkg",
|
||||
str(dfu_package),
|
||||
"-p",
|
||||
host,
|
||||
"-b",
|
||||
"115200",
|
||||
"--singlebank",
|
||||
],
|
||||
env=env,
|
||||
stream_output=True,
|
||||
):
|
||||
raise EsphomeError("nRF52 serial DFU upload failed")
|
||||
else:
|
||||
result = _upload_using_platformio(config, host, ["-t", "upload"])
|
||||
if result != 0:
|
||||
raise EsphomeError(f"Upload failed with result: {result}")
|
||||
return True # Handled: serial upload
|
||||
|
||||
if host == "PYOCD":
|
||||
result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"])
|
||||
if result != 0:
|
||||
raise EsphomeError(f"Upload failed with result: {result}")
|
||||
return True # Handled: platformio PYOCD upload
|
||||
if not CORE.using_toolchain_platformio:
|
||||
check_and_install()
|
||||
paths = get_build_paths()
|
||||
env = get_build_env()
|
||||
build_dir = CORE.relative_pioenvs_path(CORE.name)
|
||||
west_cmd = [
|
||||
str(paths["python_executable"]),
|
||||
"-m",
|
||||
"west",
|
||||
"flash",
|
||||
"--runner",
|
||||
"pyocd",
|
||||
"-d",
|
||||
str(build_dir),
|
||||
]
|
||||
if not run_command_ok(
|
||||
west_cmd,
|
||||
env=env,
|
||||
stream_output=True,
|
||||
cwd=str(paths["framework_path"]),
|
||||
):
|
||||
raise EsphomeError("nRF52 pyocd flash failed")
|
||||
else:
|
||||
result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"])
|
||||
if result != 0:
|
||||
raise EsphomeError(f"Upload failed with result: {result}")
|
||||
return True # Handled: PYOCD upload
|
||||
|
||||
# Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths
|
||||
from .ble_logger import is_mac_address
|
||||
@@ -662,4 +778,43 @@ def run_compile(args, config: ConfigType) -> bool:
|
||||
):
|
||||
raise EsphomeError("nRF52 native build failed")
|
||||
|
||||
# Zephyr's cmake places kernel artifacts in build_dir/zephyr/zephyr/ and
|
||||
# merged.hex at build_dir/. Normalize to build_dir/zephyr/ so paths match
|
||||
# get_download_types (which mirrors the platformio build output layout).
|
||||
zephyr_dir = build_dir / "zephyr"
|
||||
west_out = zephyr_dir / "zephyr"
|
||||
for filename in ["zephyr.uf2"]:
|
||||
src = west_out / filename
|
||||
if src.is_file():
|
||||
shutil.copy2(src, zephyr_dir / filename)
|
||||
|
||||
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
|
||||
_GENPKG_PARAMS = {
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
|
||||
}
|
||||
bootloader = zephyr_data()[KEY_BOOTLOADER]
|
||||
if bootloader in (
|
||||
BOOTLOADER_ADAFRUIT,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
):
|
||||
hex_file = west_out / "zephyr.hex"
|
||||
dfu_package = build_dir / "firmware.zip"
|
||||
genpkg_cmd = [
|
||||
str(paths["python_executable"]),
|
||||
"-m",
|
||||
"nordicsemi.__main__",
|
||||
"dfu",
|
||||
"genpkg",
|
||||
]
|
||||
if bootloader in _GENPKG_PARAMS:
|
||||
dev_type, sd_req = _GENPKG_PARAMS[bootloader]
|
||||
genpkg_cmd += ["--dev-type", dev_type, "--sd-req", sd_req]
|
||||
genpkg_cmd += ["--application", str(hex_file), str(dfu_package)]
|
||||
if not run_command_ok(genpkg_cmd, env=env, stream_output=True):
|
||||
raise EsphomeError("Failed to create adafruit DFU package")
|
||||
|
||||
return True
|
||||
|
||||
@@ -54,22 +54,15 @@ def _get_toolchain_path(version: str) -> Path:
|
||||
return _get_tools_path() / "toolchains" / version
|
||||
|
||||
|
||||
# onexc/dir_fd were added to shutil.rmtree in 3.12; the 3.11 branch uses onerror.
|
||||
_SITECUSTOMIZE = """\
|
||||
import os, stat, shutil, sys
|
||||
import os, stat, shutil
|
||||
_orig = shutil.rmtree
|
||||
def _handler(func, path, exc):
|
||||
os.chmod(path, stat.S_IWRITE); func(path)
|
||||
if sys.version_info >= (3, 12):
|
||||
def _rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
|
||||
if onerror is None and onexc is None:
|
||||
onexc = _handler
|
||||
return _orig(path, ignore_errors=ignore_errors, onerror=onerror, onexc=onexc, dir_fd=dir_fd)
|
||||
else:
|
||||
def _rmtree(path, ignore_errors=False, onerror=None):
|
||||
if onerror is None:
|
||||
onerror = _handler
|
||||
return _orig(path, ignore_errors=ignore_errors, onerror=onerror)
|
||||
def _rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
|
||||
if onerror is None and onexc is None:
|
||||
onexc = _handler
|
||||
return _orig(path, ignore_errors=ignore_errors, onerror=onerror, onexc=onexc, dir_fd=dir_fd)
|
||||
shutil.rmtree = _rmtree
|
||||
"""
|
||||
|
||||
@@ -111,10 +104,9 @@ def _get_version_str() -> str:
|
||||
|
||||
def get_build_paths() -> dict:
|
||||
version = _get_version_str()
|
||||
env_path = _get_python_env_path(version)
|
||||
return {
|
||||
"python_executable": get_python_env_executable_path(
|
||||
_get_python_env_path(version), "python"
|
||||
),
|
||||
"python_executable": get_python_env_executable_path(env_path, "python"),
|
||||
"framework_path": _get_framework_path(version),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
west==1.5.0
|
||||
ninja==1.13.0
|
||||
cmake==4.3.2
|
||||
adafruit-nrfutil @ git+https://github.com/adafruit/Adafruit_nRF52_nrfutil.git@7fdfe15feee5f304fb7d9b031721dcefa1f72b58
|
||||
|
||||
@@ -541,7 +541,7 @@ void OpenTherm::debug_error(OpenThermError &error) const {
|
||||
error.capture, error.bit_pos);
|
||||
}
|
||||
|
||||
float OpenthermData::f88() { return ((float) this->s16()) / 256.0; }
|
||||
float OpenthermData::f88() { return ((float) this->s16()) / 256.0f; }
|
||||
|
||||
void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); }
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ void opentherm::OpenthermOutput::write_state(float state) {
|
||||
#else
|
||||
bool zero_means_zero = false;
|
||||
#endif
|
||||
this->state =
|
||||
state < 0.003 && zero_means_zero ? 0.0 : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
|
||||
this->state = state < 0.003f && zero_means_zero
|
||||
? 0.0f
|
||||
: clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
|
||||
this->has_state_ = true;
|
||||
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user