Compare commits

..

3 Commits

Author SHA1 Message Date
Franck Nijhof 19280e03ad [core] Defer requests import in external_files and web_server_ota
Same pattern as framework_helpers: both modules imported requests at module
top level but only use it inside functions that perform actual network I/O
(downloading fonts/images, web_server OTA upload), never during config
validation. external_files is loaded by the font and audio_file components.

Defer the imports into the functions that use them, and update the tests to
patch requests.<method> directly (the modules no longer hold a requests
attribute). Adds regression tests guarding against re-introducing the
top-level imports.
2026-06-25 20:42:35 +00:00
Franck Nijhof 1ff519446b [core] Guard deferred requests import and fix download test patch targets
Add a regression test asserting framework_helpers does not import requests
at module import time, and update the download_from_mirrors tests to patch
requests.get directly (the module no longer holds a requests attribute now
that the import is deferred into the function).
2026-06-25 19:04:41 +00:00
Franck Nijhof ee1fffb062 [core] Defer requests import in framework_helpers
framework_helpers imported requests at module top level, but it is only
used by download_from_mirrors() to fetch toolchains during a build. The
module is loaded during config validation (via the esp-idf framework,
the default for esp32, and the host platform), so every such config paid
the ~85ms requests import cost even though validation never downloads
anything.

Defer the import into download_from_mirrors(). Measured roughly 70ms
(~16%) off esphome config wall time for an esp-idf/host config.
2026-06-25 18:51:48 +00:00
155 changed files with 1384 additions and 5112 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: venv
# yamllint disable-line rule:line-length
+11 -11
View File
@@ -39,7 +39,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: venv
# yamllint disable-line rule:line-length
@@ -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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
-1
View File
@@ -266,7 +266,6 @@ 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
+1 -1
View File
@@ -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.21
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.18
RUN \
platformio settings set enable_telemetry No \
+5 -22
View File
@@ -1488,29 +1488,12 @@ _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
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)
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)
for key in sorted(unmarked):
_LOGGER.warning(
"Field '%s' is being redacted by a legacy substring heuristic. "
+1 -1
View File
@@ -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.0f;
float meters = distance / 1000.0;
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
this->publish_state(meters);
} else {
+1 -1
View File
@@ -216,7 +216,7 @@ void AcDimmer::setup() {
}
void AcDimmer::write_state(float state) {
state = std::acos(1 - (2 * state)) / std::numbers::pi_v<float>; // RMS power compensation
state = std::acos(1 - (2 * state)) / std::numbers::pi; // 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_;
+5 -5
View File
@@ -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.0f);
this->position = ((float) this->decoder_->position_ / 100.0);
if (!this->invert_position_)
this->position = 1 - this->position;
if (this->position > 0.97f)
this->position = 1.0f;
if (this->position < 0.02f)
this->position = 0.0f;
if (this->position > 0.97)
this->position = 1.0;
if (this->position < 0.02)
this->position = 0.0;
this->publish_state();
}
+2 -2
View File
@@ -6,9 +6,9 @@
namespace esphome::anova {
float ftoc(float f) { return (f - 32.0f) * (5.0f / 9.0f); }
float ftoc(float f) { return (f - 32.0) * (5.0f / 9.0f); }
float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0f; }
float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0; }
AnovaPacket *AnovaCodec::clean_packet_() {
this->packet_.length = strlen((char *) this->packet_.data);
-2
View File
@@ -305,7 +305,6 @@ 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,
@@ -316,7 +315,6 @@ 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,13 +31,6 @@
#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
@@ -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.0f - prior) * prob_state_source_false);
return prob_state_source_true / (prior * prob_state_source_true + (1.0 - prior) * prob_state_source_false);
}
void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float value) {
+1 -1
View File
@@ -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.5f / 59 - 40;
value = (value - 64) * 12.5 / 59 - 40;
}
sensor->publish_state(value);
}
+1 -1
View File
@@ -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.4f * 256);
return this->power_reference_cal_ * 3600000 / (1638.4 * 256);
}
float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; }
+2 -2
View File
@@ -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.0f / 305978.0f) / 73989.0f;
this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0 / 305978.0) / 73989.0;
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.4f;
this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4;
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.0f;
median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0;
}
}
@@ -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.01f) {
if (fabsf(this->position - pos) < 0.01) {
// already at target
} else {
auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
+1 -1
View File
@@ -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.8f) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F));
clamp<float>(((this->target_temperature * 1.8) + 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.25f;
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
}
switch (this->resolution_) {
case 9:
@@ -96,8 +96,7 @@ class DeepSleepComponent final : public Component {
#endif
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
!defined(USE_ESP32_VARIANT_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,8 +56,7 @@ 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_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
!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
@@ -100,8 +99,7 @@ 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_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
!defined(USE_ESP32_VARIANT_ESP32C6) && !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) {
@@ -124,9 +122,9 @@ void DeepSleepComponent::deep_sleep_() {
}
#endif
// 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)
// 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)
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
@@ -156,8 +154,7 @@ 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_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
!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);
+1 -1
View File
@@ -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.1f) {
if (val < 0.1) {
this->publish_state(NAN);
} else {
this->publish_state(val * 100);
+1 -1
View File
@@ -9,7 +9,7 @@ namespace esphome::demo {
class DemoSwitch final : public switch_::Switch, public Component {
public:
void setup() override {
bool initial = random_float() < 0.5f;
bool initial = random_float() < 0.5;
this->publish_state(initial);
}
+2 -2
View File
@@ -10,9 +10,9 @@ class DemoTextSensor final : public text_sensor::TextSensor, public PollingCompo
public:
void update() override {
float val = random_float();
if (val < 0.33f) {
if (val < 0.33) {
this->publish_state("foo");
} else if (val < 0.66f) {
} else if (val < 0.66) {
this->publish_state("bar");
} else {
this->publish_state("foobar");
+27 -27
View File
@@ -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 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
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.15f, max1 / 0.15f);
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
this->cmd_ = buf;
} else if (min3 < 0 || max3 < 0) {
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->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 = 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.15f, max1 / 0.15f, min2 / 0.15f,
max2 / 0.15f);
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
max2 / 0.15);
this->cmd_ = buf;
} else if (min4 < 0 || max4 < 0) {
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->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 = 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.15f, max1 / 0.15f, min2 / 0.15f,
max2 / 0.15f, min3 / 0.15f, max3 / 0.15f);
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);
this->cmd_ = buf;
} else {
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;
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;
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.15f, max1 / 0.15f,
min2 / 0.15f, max2 / 0.15f, min3 / 0.15f, max3 / 0.15f, min4 / 0.15f, max4 / 0.15f);
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);
this->cmd_ = buf;
}
+8 -8
View File
@@ -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 * 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;
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;
// 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 : tanf(float(draw_progress) * std::numbers::pi_v<float> / 100); // slope
float tan_a = (progress == 50) ? 65535 : tan(float(draw_progress) * M_PI / 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_v<float> / 180;
float rotation_radians = rotation_degrees * std::numbers::pi / 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_v<float> / edges : 0.0f;
rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0;
float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi_v<float> + rotation_radians;
float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + 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;
}
+1 -1
View File
@@ -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.0f; }
float get_setup_priority() const override { return setup_priority::BUS - 1.0; }
bool reset_device();
int reset_int() override;
+4 -4
View File
@@ -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.5f;
if (mic_gain <= 33.0f) {
mic_gain += 0.5;
if (mic_gain <= 33.0) {
return (uint8_t) (mic_gain / 3);
}
if (mic_gain < 36.0f) {
if (mic_gain < 36.0) {
return 12;
}
if (mic_gain < 37.0f) {
if (mic_gain < 37.0) {
return 13;
}
return 14;
+4 -4
View File
@@ -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.5f;
if (mic_gain <= 33.0f) {
mic_gain += 0.5;
if (mic_gain <= 33.0) {
return (uint8_t) mic_gain / 3;
}
if (mic_gain < 36.0f) {
if (mic_gain < 36.0) {
return 12;
}
if (mic_gain < 37.0f) {
if (mic_gain < 37.0) {
return 13;
}
return 14;
+1 -7
View File
@@ -173,14 +173,8 @@ 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 |= ES8388_DACCONTROL3_DAC_MUTE;
} else {
value &= ~ES8388_DACCONTROL3_DAC_MUTE;
value = 0x3C;
}
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
-1
View File
@@ -38,7 +38,6 @@ 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;
-4
View File
@@ -1102,8 +1102,6 @@ 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]
@@ -1187,8 +1185,6 @@ def final_validate(config):
)
)
final_validate_pins(full_config)
if (
config[CONF_FLASH_SIZE] == "32MB"
and "ota" in full_config
+1 -15
View File
@@ -18,7 +18,6 @@ from esphome.const import (
PLATFORM_ESP32,
)
from esphome.core import CORE
from esphome.types import ConfigType
from . import boards
from .const import (
@@ -51,11 +50,7 @@ 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_final_validate_pins,
esp32_s3_validate_gpio_pin,
esp32_s3_validate_supports,
)
from .gpio_esp32_s3 import 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)
@@ -101,7 +96,6 @@ 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 = {
@@ -151,7 +145,6 @@ _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,
@@ -268,10 +261,3 @@ 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)
+7 -38
View File
@@ -2,15 +2,8 @@ import logging
from typing import Any
import esphome.config_validation as cv
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
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
_ESP32S3_SPI_PSRAM_PINS = {
26: "SPICS1",
@@ -45,9 +38,11 @@ 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]})"
)
# 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 _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,
)
if value in (22, 23, 24, 25):
# These pins are not exposed in GPIO mux (reason unknown)
@@ -76,29 +71,3 @@ 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,6 +28,9 @@ 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) {
@@ -201,6 +204,11 @@ 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_) {
@@ -303,9 +311,7 @@ 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) {
if (this->current_send_packet_->callback_ != nullptr) {
this->current_send_packet_->callback_(packet->packet_.sent.status);
}
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
}
+2 -2
View File
@@ -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.0f - (float) (y - yn) / (ym - yn)));
int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (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.0f - v)) - thick / 2 + y_offset;
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - 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.0f / (float) rpm);
ms_per_step = (uint16_t) (3000.0 / (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.0f / (float) rpm);
ms_per_step = (uint16_t) (3000.0 / (float) rpm);
buffer_[0] = mode;
buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw
+1 -1
View File
@@ -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.49f) ? 1 : 0;
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 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.49f) ? 1 : 0;
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
}
if (out_data->ac_power == 0) {
// If AC is off - no presets allowed
+1 -3
View File
@@ -2,8 +2,6 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <numbers>
namespace esphome::hmc5883l {
static const char *const TAG = "hmc5883l";
@@ -128,7 +126,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 / std::numbers::pi_v<float>;
float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
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,9 +55,7 @@ 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.0f) * 200.0f) - 50.0f;
}
float HONEYWELLABPSensor::countstotemperatures_(const int counts) { return (((float) counts / 2047.0) * 200.0) - 50.0; }
// Pressure value from the most recent reading in units
float HONEYWELLABPSensor::read_pressure_() {
@@ -71,9 +69,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.0f);
this->pressure_sensor_->publish_state(read_pressure_() * 1.0);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(read_temperature_() * 1.0f);
this->temperature_sensor_->publish_state(read_temperature_() * 1.0);
}
}
@@ -55,7 +55,7 @@ void HrxlMaxsonarWrComponent::check_buffer_() {
millimeters = millimeters * 10;
}
float meters = float(millimeters) / 1000.0f;
float meters = float(millimeters) / 1000.0;
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.0f) {
if (volume > 0.0) {
this->audio_dac_->set_mute_off();
}
this->audio_dac_->set_volume(volume);
+1 -1
View File
@@ -119,7 +119,7 @@ void INA219Component::setup() {
}
this->calibration_lsb_ = lsb;
auto calibration = uint32_t(0.04096f / (0.000001f * lsb * this->shunt_resistance_ohm_));
auto calibration = uint32_t(0.04096f / (0.000001 * 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();
+1 -1
View File
@@ -70,7 +70,7 @@ void INA226Component::setup() {
this->calibration_lsb_ = lsb;
auto calibration = uint32_t(0.00512f / (lsb * this->shunt_resistance_ohm_ / 1000000.0f));
auto calibration = uint32_t(0.00512 / (lsb * this->shunt_resistance_ohm_ / 1000000.0f));
ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration);
-1
View File
@@ -1 +0,0 @@
CODEOWNERS = ["@Passific", "@koosoli", "@limengdu"]
-433
View File
@@ -1,433 +0,0 @@
"""
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
-373
View File
@@ -1,373 +0,0 @@
#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
-168
View File
@@ -1,168 +0,0 @@
#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
@@ -315,14 +315,14 @@ class LightColorValues {
if (this->color_temperature_ <= 0) {
return this->color_temperature_;
}
return 1000000.0f / this->color_temperature_;
return 1000000.0 / 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.0f / color_temperature;
this->color_temperature_ = 1000000.0 / color_temperature;
}
/// Get the cold white property of these light color values. In range 0.0 to 1.0.
+1 -1
View File
@@ -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.5f) * 2;
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
float v = LightTransformer::smoothed_progress(p);
return LightColorValues::lerp(start, end, v);
+1 -1
View File
@@ -75,7 +75,7 @@ void LTR390Component::read_als_() {
uint32_t als = *val;
if (this->light_sensor_ != nullptr) {
float lux = ((0.6f * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_;
float lux = ((0.6 * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_;
this->light_sensor_->publish_state(lux);
}
+6 -6
View File
@@ -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.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;
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;
} else {
ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
lux = 0.0f;
+6 -6
View File
@@ -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.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);
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);
} else {
ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
lux = 0.0f;
+1 -1
View File
@@ -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.25f * (float) (raw_voltage >> 4)) / 1000.0f;
float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0;
this->voltage_sensor_->publish_state(voltage);
this->status_clear_warning();
}
+1 -1
View File
@@ -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.000f * this->reference_voltage_; // in V
return float(digital_value) / 4096.000 * this->reference_voltage_; // in V
}
} // namespace esphome::mcp3204
@@ -29,9 +29,7 @@ void Mcp4461Wiper::write_state(float state) {
}
}
float Mcp4461Wiper::read_state() {
return (static_cast<float>(this->parent_->get_wiper_level_(this->wiper_)) / 256.0f);
}
float Mcp4461Wiper::read_state() { return (static_cast<float>(this->parent_->get_wiper_level_(this->wiper_)) / 256.0); }
float Mcp4461Wiper::update_state() {
this->state_ = this->read_state();
+1 -2
View File
@@ -24,8 +24,7 @@ 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) {
constexpr uint16_t max_value = (1U << MCP4725_RES) - 1;
const uint16_t value = (uint16_t) roundf(state * max_value);
const uint16_t value = (uint16_t) round(state * (pow(2, MCP4725_RES) - 1));
this->write_byte_16(64, value << 4);
}
+3 -4
View File
@@ -4,11 +4,10 @@
#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;
+1 -4
View File
@@ -100,7 +100,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
if (!friendly_name_empty) {
txt_count++; // friendly_name
}
#if defined(USE_ESP8266) || defined(USE_ESP32) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_NRF52)
#if defined(USE_ESP8266) || defined(USE_ESP32) || defined(USE_RP2040) || defined(USE_LIBRETINY)
txt_count++; // platform
#endif
#if defined(USE_WIFI) || defined(USE_ETHERNET) || defined(USE_OPENTHREAD)
@@ -141,9 +141,6 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)});
#elif defined(USE_LIBRETINY)
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(lt_cpu_get_model_name())});
#elif defined(USE_NRF52)
MDNS_STATIC_CONST_CHAR(PLATFORM_NRF52, "nRF52");
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_NRF52)});
#endif
txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
+3 -10
View File
@@ -2,20 +2,13 @@
#if defined(USE_ZEPHYR) && defined(USE_MDNS)
#include "mdns_component.h"
#include "esphome/core/log.h"
namespace esphome::mdns {
#ifdef USE_MDNS_STORE_SERVICES
// Zephyr has no local IP mDNS responder. When a consumer (ex. the OpenThread SRP
// client) enables service storage, it reads the compiled records via get_services()
// and advertises them itself. We only need to compile and store the records here.
static void register_zephyr(MDNSComponent *, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &) {}
static const char *const TAG = "mdns.zephyr";
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_zephyr); }
#else
// No responder and nothing consuming the records, so skip the boot-time compile.
void MDNSComponent::setup() {}
#endif
void MDNSComponent::setup() { ESP_LOGW(TAG, "mDNS is not implemented for Zephyr"); }
void MDNSComponent::on_shutdown() {}
+11 -11
View File
@@ -71,10 +71,10 @@ void MICS4514Component::update() {
float co = 0.0f;
if (red_f > 3.4f) {
co = 0.0;
} else if (red_f < 0.01f) {
} else if (red_f < 0.01) {
co = 1000.0;
} else {
co = 4.2f / powf(red_f, 1.2f);
co = 4.2 / pow(red_f, 1.2);
}
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.164f * powf(ox_f, 0.975f);
nitrogendioxide = 0.164 * pow(ox_f, 0.975);
}
this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide);
}
if (this->methane_sensor_ != nullptr) {
float methane = 0.0f;
if (red_f > 0.9f || red_f < 0.5f) { // outside the range->unlikely
if (red_f > 0.9f || red_f < 0.5) { // outside the range->unlikely
methane = 0.0;
} else {
methane = 630 / powf(red_f, 4.4f);
methane = 630 / pow(red_f, 4.4);
}
this->methane_sensor_->publish_state(methane);
}
if (this->ethanol_sensor_ != nullptr) {
float ethanol = 0.0f;
if (red_f > 1.0f || red_f < 0.02f) { // outside the range->unlikely
if (red_f > 1.0f || red_f < 0.02) { // outside the range->unlikely
ethanol = 0.0;
} else {
ethanol = 1.52f / powf(red_f, 1.55f);
ethanol = 1.52 / pow(red_f, 1.55);
}
this->ethanol_sensor_->publish_state(ethanol);
}
if (this->hydrogen_sensor_ != nullptr) {
float hydrogen = 0.0f;
if (red_f > 0.9f || red_f < 0.02f) { // outside the range->unlikely
if (red_f > 0.9f || red_f < 0.02) { // outside the range->unlikely
hydrogen = 0.0;
} else {
hydrogen = 0.85f / powf(red_f, 1.75f);
hydrogen = 0.85 / pow(red_f, 1.75);
}
this->hydrogen_sensor_->publish_state(hydrogen);
}
if (this->ammonia_sensor_ != nullptr) {
float ammonia = 0.0f;
if (red_f > 0.98f || red_f < 0.2532f) { // outside the ammonia range->unlikely
if (red_f > 0.98f || red_f < 0.2532) { // outside the ammonia range->unlikely
ammonia = 0.0;
} else {
ammonia = 0.9f / powf(red_f, 4.6f);
ammonia = 0.9 / pow(red_f, 4.6);
}
this->ammonia_sensor_->publish_state(ammonia);
}
+1 -3
View File
@@ -1,8 +1,6 @@
#include "mmc5603.h"
#include "esphome/core/log.h"
#include <numbers>
namespace esphome::mmc5603 {
static const char *const TAG = "mmc5603";
@@ -145,7 +143,7 @@ void MMC5603Component::update() {
const float z = 0.00625 * (raw_z - 524288);
const float heading = atan2f(0.0f - x, y) * 180.0f / std::numbers::pi_v<float>;
const float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
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)
+1
View File
@@ -124,6 +124,7 @@ 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))
+41 -136
View File
@@ -92,10 +92,14 @@ int32_t Modbus::tx_delay_remaining() {
int32_t ModbusClientHub::tx_delay_remaining() {
const uint32_t now = millis();
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_))});
// 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_))});
}
bool Modbus::tx_blocked() {
@@ -254,7 +258,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);
this->process_modbus_client_frame_(address, function_code, data, data_len);
return true;
}
@@ -317,8 +321,10 @@ 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) {
if (this->find_device_(address) != nullptr) {
ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address);
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->expecting_peer_response_ == address) {
@@ -332,124 +338,31 @@ void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t funct
this->expecting_peer_response_ = 0;
}
ModbusServerDevice *ModbusServerHub::find_device_(uint8_t address) {
void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data,
uint16_t len) {
bool found = false;
for (auto *device : this->devices_) {
if (device->get_address() == address) {
return device;
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);
}
}
}
return nullptr;
}
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) {
if (!found) {
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);
}
}
@@ -487,6 +400,7 @@ 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;
}
@@ -502,7 +416,8 @@ void ModbusClientHub::send_next_frame_() {
ModbusDeviceCommand &command = this->tx_buffer_.front();
if (this->send_frame_(command.frame)) {
this->waiting_for_response_ = std::move(command);
if (!this->last_send_was_broadcast_)
this->waiting_for_response_ = std::move(command);
} else {
if (command.device)
command.device->on_modbus_not_sent();
@@ -540,27 +455,17 @@ float Modbus::get_setup_priority() const {
return setup_priority::BUS - 1.0f;
}
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));
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);
return;
}
uint8_t raw_frame[MAX_RAW_SIZE];
raw_frame[0] = address;
raw_frame[1] = function_code;
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);
std::memcpy(raw_frame + 2, payload.data(), payload.size());
this->send_raw_(raw_frame, len);
}
// Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload.
+28 -33
View File
@@ -63,6 +63,7 @@ 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};
@@ -129,22 +130,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);
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 process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len);
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_;
@@ -199,41 +200,35 @@ 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:
virtual ~ModbusServerDevice() = default;
ModbusServerDevice() = default;
// Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five).
ModbusServerDevice(ModbusServerHub *parent, uint8_t address) : parent_(parent), address_(address) {}
virtual ~ModbusServerDevice() = default;
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; }
uint8_t get_address() const { return this->address_; }
virtual ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers,
RegisterValues &registers) {
return ModbusExceptionCode::ILLEGAL_FUNCTION;
};
virtual ServerResponseStatus on_modbus_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
RegisterValues &registers) {
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 &registers) {
return this->on_modbus_read_registers(start_address, number_of_registers, registers);
};
virtual ServerResponseStatus on_modbus_write_registers(uint16_t start_address, const RegisterValues &registers) {
return ModbusExceptionCode::ILLEGAL_FUNCTION;
};
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);
}
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 256 - CRC(2) = 254
static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 255 - CRC(2) = 254
static constexpr uint16_t MAX_FRAME_SIZE = 256;
/// End of Modbus definitions
} // namespace esphome::modbus
+42 -32
View File
@@ -101,19 +101,53 @@ static size_t required_payload_size(SensorValueType sensor_value_type) {
}
}
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));
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;
}
}
int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset,
int64_t payload_to_number(const std::vector<uint8_t> &data, 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) > size) {
if (static_cast<size_t>(offset) > data.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), size);
static_cast<unsigned int>(offset), data.size());
if (error_return)
*error_return = true;
return value;
@@ -124,9 +158,10 @@ int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sens
return value;
}
if (size - offset < required_size) {
if (data.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), size, required_size);
static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), data.size(),
required_size);
if (error_return)
*error_return = true;
return value;
@@ -179,31 +214,6 @@ int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sens
return value;
}
int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type,
bool *error_return) {
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) {
+8 -61
View File
@@ -224,77 +224,24 @@ template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
return 0;
}
// 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);
/** Append the Modbus register words for value to data.
* Works with any container exposing push_back(uint16_t) (e.g. std::vector or StaticVector).
/** 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
*/
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;
}
}
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
/** Convert a raw response payload to a number.
/** Convert vector<uint8_t> response payload to 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 uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset,
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);
/** 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,18 +3,26 @@
#include "esphome/core/log.h"
namespace esphome::modbus_server {
using modbus::ModbusFunctionCode;
using modbus::ModbusExceptionCode;
using modbus::helpers::registers_to_number;
using modbus::helpers::payload_to_number;
static const char *const TAG = "modbus_server";
modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t start_address,
uint16_t number_of_registers,
modbus::RegisterValues &registers) {
void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers) {
ESP_LOGV(TAG,
"Received read holding/input registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%X.",
this->address_, start_address, number_of_registers);
"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);
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_) {
@@ -28,7 +36,10 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t sta
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)));
modbus::helpers::number_to_payload(registers, value, server_register->value_type);
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());
current_address += server_register->register_count;
found = true;
break;
@@ -42,37 +53,92 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t sta
"Could not match any register to address 0x%02X, but default allowed. "
"Returning default value: %" PRIu16 ".",
current_address, this->server_courtesy_response_.register_value);
registers.push_back(this->server_courtesy_response_.register_value);
sixteen_bit_response.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);
return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS;
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
}
}
return {};
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);
}
modbus::ServerResponseStatus ModbusServer::on_modbus_write_registers(uint16_t start_address,
const modbus::RegisterValues &registers) {
// 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());
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;
auto for_each_register =
[this, start_address,
&registers](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();) {
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;) {
bool ok = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
ok = callback(server_register, register_offset);
ok = callback(server_register, offset);
current_address += server_register->register_count;
register_offset += server_register->register_count;
offset += server_register->register_count * sizeof(uint16_t);
break;
}
}
@@ -84,41 +150,36 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_write_registers(uint16_t st
return true;
};
// 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, &registers](ServerRegister *server_register, uint16_t register_offset) -> bool {
if (server_register->write_lambda == nullptr) {
return false; // unwritable -> ILLEGAL_DATA_ADDRESS
}
// 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;
registers_to_number(registers.data() + register_offset, registers.size() - register_offset,
server_register->value_type, &error);
int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF, &error);
if (error) {
precheck = ModbusExceptionCode::ILLEGAL_DATA_VALUE; // request doesn't supply the full value
return false;
} else {
return server_register->write_lambda(number);
}
return true;
})) {
ESP_LOGW(TAG, "Write request rejected before applying any register. Sending exception response.");
return precheck;
ESP_LOGW(TAG, "Could not write all registers. Sending exception response.");
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
return;
}
// 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([&registers](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 {};
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);
}
void ModbusServer::dump_config() {
@@ -98,11 +98,9 @@ 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
modbus::ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers,
modbus::RegisterValues &registers) final;
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
modbus::ServerResponseStatus on_modbus_write_registers(uint16_t start_address,
const modbus::RegisterValues &registers) final;
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) 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;
+3 -3
View File
@@ -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.0f;
altitude = float(alt) / 65536.0;
this->altitude_->publish_state(altitude);
} else {
uint32_t p = encode_uint32(0, buffer[0], buffer[1], buffer[2]);
pressure = float(p) / 6400.0f;
pressure = float(p) / 6400.0;
if (this->pressure_ != nullptr)
this->pressure_->publish_state(pressure);
}
int16_t t = encode_uint16(buffer[3], buffer[4]);
float temperature = float(t) / 256.0f;
float temperature = float(t) / 256.0;
if (this->temperature_ != nullptr)
this->temperature_->publish_state(temperature);
+2 -2
View File
@@ -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.1f;
root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1;
// current_temp_step
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1f;
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1;
// temperature units are always coerced to Celsius internally
root[MQTT_TEMPERATURE_UNIT] = "C";
+1 -1
View File
@@ -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.9f); // LSB to 1 LSB = 3.9mg = 0.0039g
const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9); // 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
-6
View File
@@ -221,12 +221,6 @@ 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)
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.0f) {
if (brightness < 0 || brightness > 1.0) {
ESP_LOGD(TAG, "Brightness out of bounds (0-1.0)");
return;
}
+9 -164
View File
@@ -4,7 +4,6 @@ import asyncio
import logging
from pathlib import Path
import re
import shutil
import subprocess
from esphome import pins
@@ -487,16 +486,6 @@ 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:
@@ -505,122 +494,17 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
mcumgr_device = host
else:
if not CORE.using_toolchain_platformio:
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":
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"])
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: PYOCD upload
return True # Handled: platformio 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
# Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths
from .ble_logger import is_mac_address
@@ -778,43 +662,4 @@ 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
+3 -2
View File
@@ -111,9 +111,10 @@ 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(env_path, "python"),
"python_executable": get_python_env_executable_path(
_get_python_env_path(version), "python"
),
"framework_path": _get_framework_path(version),
}
@@ -1,4 +1,3 @@
west==1.5.0
ninja==1.13.0
cmake==4.3.2
adafruit-nrfutil @ git+https://github.com/adafruit/Adafruit_nRF52_nrfutil.git@7fdfe15feee5f304fb7d9b031721dcefa1f72b58
+1 -1
View File
@@ -541,7 +541,7 @@ void OpenTherm::debug_error(OpenThermError &error) const {
error.capture, error.bit_pos);
}
float OpenthermData::f88() { return ((float) this->s16()) / 256.0f; }
float OpenthermData::f88() { return ((float) this->s16()) / 256.0; }
void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); }
@@ -12,9 +12,8 @@ void opentherm::OpenthermOutput::write_state(float state) {
#else
bool zero_means_zero = false;
#endif
this->state = state < 0.003f && zero_means_zero
? 0.0f
: clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
this->state =
state < 0.003 && zero_means_zero ? 0.0 : 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);
}
+1 -1
View File
@@ -85,7 +85,7 @@ class OpenThreadSrpComponent final : public Component {
public:
void set_mdns(esphome::mdns::MDNSComponent *mdns);
// This has to run after the mdns component or else no services are available to advertise
float get_setup_priority() const override { return this->mdns_->get_setup_priority() - 1.0f; }
float get_setup_priority() const override { return this->mdns_->get_setup_priority() - 1.0; }
void setup() override;
static void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrpClientService *services,
const otSrpClientService *removed_services, void *context);
+6 -3
View File
@@ -1,7 +1,10 @@
#include "pid_autotuner.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <numbers>
#ifndef M_PI
#define M_PI 3.1415926535897932384626433
#endif
namespace esphome::pid {
@@ -123,7 +126,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce
float osc_ampl = this->amplitude_detector_.get_mean_oscillation_amplitude();
float d = (this->relay_function_.output_positive - this->relay_function_.output_negative) / 2.0f;
ESP_LOGVV(TAG, " Relay magnitude: %f", d);
this->ku_ = 4.0f * d / (std::numbers::pi_v<float> * osc_ampl);
this->ku_ = 4.0f * d / float(M_PI * osc_ampl);
this->pu_ = this->frequency_detector_.get_mean_oscillation_period();
this->state_ = AUTOTUNE_SUCCEEDED;
@@ -297,7 +300,7 @@ bool PIDAutotuner::OscillationFrequencyDetector::is_increase_decrease_symmetrica
min_interval = std::min(min_interval, interval);
}
float ratio = min_interval / float(max_interval);
return ratio >= 0.66f;
return ratio >= 0.66;
}
// ================== OscillationAmplitudeDetector ==================
+1 -2
View File
@@ -3,7 +3,6 @@
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cmath>
#include <numbers>
namespace esphome::qmc5883l {
@@ -174,7 +173,7 @@ void QMC5883LComponent::read_sensor_() {
const float y = int16_t(raw[1]) * mg_per_bit * 0.1f;
const float z = int16_t(raw[2]) * mg_per_bit * 0.1f;
float heading = atan2f(0.0f - x, y) * 180.0f / std::numbers::pi_v<float>;
float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
float temp = NAN;
if (this->temperature_sensor_ != nullptr) {
+1 -1
View File
@@ -276,7 +276,7 @@ void QMP6988Component::write_oversampling_temperature_(QMP6988Oversampling overs
void QMP6988Component::calculate_altitude_(float pressure, float temp) {
float altitude;
altitude = (powf((101325 / pressure), 1 / 5.257f) - 1) * (temp + 273.15f) / 0.0065f;
altitude = (pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065;
this->qmp6988_data_.altitude = altitude;
}
+1 -2
View File
@@ -4,7 +4,6 @@
#include <cinttypes>
#include <cmath>
#include <numbers>
namespace esphome::rd03d {
@@ -234,7 +233,7 @@ void RD03DComponent::publish_target_(uint8_t target_num, int16_t x, int16_t y, i
// Angle is measured from the Y axis (radar forward direction)
if (target.angle != nullptr) {
if (valid) {
float angle = std::atan2(static_cast<float>(x), static_cast<float>(y)) * 180.0f / std::numbers::pi_v<float>;
float angle = std::atan2(static_cast<float>(x), static_cast<float>(y)) * 180.0f / M_PI;
target.angle->publish_state(angle);
} else {
target.angle->publish_state(NAN);
@@ -95,7 +95,7 @@ void RuntimeStatsCollector::log_stats_() {
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0f);
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
}
}
+1 -1
View File
@@ -86,7 +86,7 @@ void Servo::write(float value) {
void Servo::internal_write(float value) {
value = clamp(value, -1.0f, 1.0f);
float level;
if (value < 0.0f) {
if (value < 0.0) {
level = std::lerp(this->idle_level_, this->min_level_, -value);
} else {
level = std::lerp(this->idle_level_, this->max_level_, value);
@@ -207,7 +207,7 @@ bool ShellyDimmer::upgrade_firmware_() {
uint16_t ShellyDimmer::convert_brightness_(float brightness) {
// Special case for zero as only zero means turn off completely.
if (brightness == 0.0f) {
if (brightness == 0.0) {
return 0;
}
-6
View File
@@ -149,7 +149,6 @@ CONFIG_SCHEMA = cv.Schema(
ln882x=IMPLEMENTATION_LWIP_SOCKETS,
rtl87xx=IMPLEMENTATION_LWIP_SOCKETS,
host=IMPLEMENTATION_BSD_SOCKETS,
nrf52=IMPLEMENTATION_BSD_SOCKETS,
): cv.one_of(
IMPLEMENTATION_LWIP_TCP,
IMPLEMENTATION_LWIP_SOCKETS,
@@ -169,11 +168,6 @@ async def to_code(config):
cg.add_define("USE_SOCKET_IMPL_LWIP_SOCKETS")
elif impl == IMPLEMENTATION_BSD_SOCKETS:
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
if CORE.using_zephyr:
from esphome.components.zephyr import zephyr_add_prj_conf
zephyr_add_prj_conf("NET_SOCKETS", True)
zephyr_add_prj_conf("POSIX_API", True)
# ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket()
# and FreeRTOS task notifications — enable fast select to bypass lwip_select().
# Only when not using lwip_tcp, which does not provide select() support.
@@ -22,13 +22,11 @@ BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) {
if (flags >= 0)
::fcntl(this->fd_, F_SETFD, flags | FD_CLOEXEC);
#endif
// Guard structure matches socket_ready_fd(): non-HOST platforms (nRF52/OpenThread)
// do not register fds with the esphome select loop, so monitor_loop is a no-op there.
if (!monitor_loop)
return;
#ifdef USE_LWIP_FAST_SELECT
this->cached_sock_ = hook_fd_for_fast_select(this->fd_);
#elif defined(USE_HOST)
#else
this->loop_monitored_ = wake_register_fd(this->fd_);
#endif
}
@@ -47,7 +45,7 @@ int BSDSocketImpl::close() {
// touch an unrelated socket's pcb. No per-socket callback unhook is needed —
// all LwIP sockets share the same static event_callback.
this->cached_sock_ = nullptr;
#elif defined(USE_HOST)
#else
if (this->loop_monitored_) {
wake_unregister_fd(this->fd_);
}
+1 -27
View File
@@ -76,7 +76,7 @@ class BSDSocketImpl {
#endif
}
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) {
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR)
#if defined(USE_ESP32) || defined(USE_HOST)
return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len);
#else
return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
@@ -85,19 +85,6 @@ class BSDSocketImpl {
ssize_t readv(const struct iovec *iov, int iovcnt) {
#if defined(USE_ESP32)
return ::lwip_readv(this->fd_, iov, iovcnt);
#elif defined(USE_ZEPHYR)
// Zephyr does not provide readv(); emulate with a read() loop. Stream sockets only:
// on a datagram socket each read() would consume a separate datagram, not scatter one.
ssize_t total = 0;
for (int i = 0; i < iovcnt; i++) {
ssize_t n = ::read(this->fd_, iov[i].iov_base, iov[i].iov_len);
if (n < 0)
return total > 0 ? total : n;
total += n;
if (static_cast<size_t>(n) < iov[i].iov_len)
break;
}
return total;
#else
return ::readv(this->fd_, iov, iovcnt);
#endif
@@ -113,19 +100,6 @@ class BSDSocketImpl {
ssize_t writev(const struct iovec *iov, int iovcnt) {
#if defined(USE_ESP32)
return ::lwip_writev(this->fd_, iov, iovcnt);
#elif defined(USE_ZEPHYR)
// Zephyr does not provide writev(); emulate with a write() loop. Stream sockets only:
// on a datagram socket each write() would emit a separate datagram, not gather one.
ssize_t total = 0;
for (int i = 0; i < iovcnt; i++) {
ssize_t n = ::write(this->fd_, iov[i].iov_base, iov[i].iov_len);
if (n < 0)
return total > 0 ? total : n;
total += n;
if (static_cast<size_t>(n) < iov[i].iov_len)
break; // partial write: stop so caller resumes from the correct stream offset
}
return total;
#else
return ::writev(this->fd_, iov, iovcnt);
#endif
-6
View File
@@ -158,9 +158,7 @@ using socklen_t = uint32_t;
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#ifndef USE_ZEPHYR
#include <sys/uio.h>
#endif
#include <unistd.h>
#ifdef USE_HOST
@@ -169,10 +167,6 @@ using socklen_t = uint32_t;
#include <netinet/ip.h>
#include <netinet/tcp.h>
#endif // USE_HOST
#ifdef USE_ZEPHYR
#include <arpa/inet.h>
#include <netinet/in.h>
#endif // USE_ZEPHYR
#ifdef USE_ARDUINO
// arduino-esp32 declares a global var called INADDR_NONE which is replaced
+2 -50
View File
@@ -12,19 +12,9 @@
namespace esphome::socket {
#ifdef USE_HOST
// Host: ready when the wake select() loop has flagged this fd (or it isn't monitored).
// Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets).
// Checks if the host wake select() loop has marked this fd as ready.
bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || wake_fd_ready(fd); }
#elif defined(USE_ZEPHYR)
// Zephyr (nRF52): fd monitoring isn't wired into the esphome select loop
// (wake_register_fd is USE_HOST-only), so loop_monitored is always false. Always
// return true — the caller handles EAGAIN/EWOULDBLOCK on read.
//
// Cost (known trade-off, not an oversight): loop-monitored sockets (API, web_server)
// are read every loop() iteration and bail on EAGAIN; there is no event-driven wake,
// so the main loop busy-polls at loop frequency and cannot idle between packets.
// TODO: wire Zephyr fds into an event-driven wake source (e.g. zsock_poll/k_poll) so
// the loop can sleep between packets on battery/OpenThread targets.
bool socket_ready_fd(int /*fd*/, bool /*loop_monitored*/) { return true; }
#endif
// Platform-specific inet_ntop wrappers
@@ -50,19 +40,6 @@ static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t
return lwip_inet_ntop(AF_INET6, addr, buf, size);
}
#endif
#elif defined(USE_ZEPHYR)
// Zephyr BSD sockets — use Zephyr native address formatting via POSIX-subset wrappers.
// <zephyr/net/socket.h> is already included transitively through <sys/socket.h>.
static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
return zsock_inet_ntop(AF_INET, addr, buf, size);
}
// IPv6 is always enabled on nRF52 (config validation enforces enable_ipv6=True),
// but the guard is retained for consistency with other platform blocks.
#if USE_NETWORK_IPV6
static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) {
return zsock_inet_ntop(AF_INET6, addr, buf, size);
}
#endif
#else
// BSD sockets (host, ESP32-IDF)
static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) {
@@ -91,15 +68,6 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s
esphome_inet_ntop4(&addr->sin6_addr.s6_addr[12], buf.data(), buf.size()) != nullptr) {
return strlen(buf.data());
}
#elif defined(USE_ZEPHYR)
// Format IPv4-mapped IPv6 addresses as regular IPv4. Zephyr uses the standard POSIX
// s6_addr layout (not the LWIP union) but provides no IN6_IS_ADDR_V4MAPPED macro, so
// detect the ::ffff:0:0/96 prefix directly on the address words.
if (addr->sin6_addr.s6_addr32[0] == 0 && addr->sin6_addr.s6_addr32[1] == 0 &&
addr->sin6_addr.s6_addr32[2] == htonl(0xFFFF) &&
esphome_inet_ntop4(&addr->sin6_addr.s6_addr32[3], buf.data(), buf.size()) != nullptr) {
return strlen(buf.data());
}
#elif !defined(USE_SOCKET_IMPL_LWIP_TCP)
// Format IPv4-mapped IPv6 addresses as regular IPv4 (LWIP layout)
if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 &&
@@ -149,19 +117,11 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
server->sin6_port = htons(port);
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#if defined(USE_ZEPHYR)
// Zephyr BSD sockets: use native address conversion
if (zsock_inet_pton(AF_INET6, ip_address, &server->sin6_addr) != 1) {
errno = EINVAL;
return 0;
}
#else
// Use standard inet_pton for BSD sockets
if (inet_pton(AF_INET6, ip_address, &server->sin6_addr) != 1) {
errno = EINVAL;
return 0;
}
#endif
#else
// Use LWIP-specific functions
ip6_addr_t ip6;
@@ -178,15 +138,7 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
auto *server = reinterpret_cast<sockaddr_in *>(addr);
memset(server, 0, sizeof(sockaddr_in));
server->sin_family = AF_INET;
#if defined(USE_ZEPHYR)
// Zephyr BSD sockets: use native address conversion
if (zsock_inet_pton(AF_INET, ip_address, &server->sin_addr) != 1) {
errno = EINVAL;
return 0;
}
#else
server->sin_addr.s_addr = inet_addr(ip_address);
#endif
server->sin_port = htons(port);
return sizeof(sockaddr_in);
}
+2 -2
View File
@@ -60,11 +60,11 @@ inline struct lwip_sock *hook_fd_for_fast_select(int fd) {
}
return sock;
}
#elif defined(USE_HOST) || defined(USE_ZEPHYR)
#elif defined(USE_HOST)
/// Shared ready() helper for fd-based socket implementations.
/// Checks if the Application's select() loop has marked this fd as ready.
bool socket_ready_fd(int fd, bool loop_monitored);
#endif // USE_LWIP_FAST_SELECT
#endif
// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd
// declared above, while the impl headers are included before those declarations.
@@ -121,7 +121,7 @@ void SoundLevelComponent::loop() {
if (this->sample_count_ == samples_in_window) {
// Processed enough samples for the measurement window, compute and publish the sensor values
if (this->peak_sensor_ != nullptr) {
const float peak_db = 10.0f * log10f(static_cast<float>(this->squared_peak_) / MAX_SAMPLE_SQUARED_DENOMINATOR);
const float peak_db = 10.0f * log10(static_cast<float>(this->squared_peak_) / MAX_SAMPLE_SQUARED_DENOMINATOR);
this->peak_sensor_->publish_state(peak_db);
this->squared_peak_ = 0; // reset accumulator
+1 -1
View File
@@ -224,7 +224,7 @@ bool SPA06Component::soft_reset_() {
}
// Temperature conversion formula. See datasheet pg. 14
float SPA06Component::convert_temperature_(const float &t_raw_sc) { return this->c0_ * 0.5f + this->c1_ * t_raw_sc; }
float SPA06Component::convert_temperature_(const float &t_raw_sc) { return this->c0_ * 0.5 + this->c1_ * t_raw_sc; }
// Pressure conversion formula. See datasheet pg. 14
float SPA06Component::convert_pressure_(const float &p_raw_sc, const float &t_raw_sc) {
float p2_raw_sc = p_raw_sc * p_raw_sc;
@@ -612,7 +612,7 @@ void SpeakerMediaPlayer::set_volume_(float volume, bool publish) {
}
// Turn on the mute state if the volume is effectively zero, off otherwise
if (volume < 0.001f) {
if (volume < 0.001) {
this->set_mute_state_(true);
} else {
this->set_mute_state_(false);
@@ -831,7 +831,7 @@ void SpeakerSourceMediaPlayer::set_volume_(float volume, bool publish) {
// Turn on the mute state if the volume is effectively zero, off otherwise.
// Pass publish=false to avoid saving twice.
if (volume < 0.001f) {
if (volume < 0.001) {
this->set_mute_state_(true, false);
} else {
this->set_mute_state_(false, false);

Some files were not shown because too many files have changed in this diff Show More