mirror of
https://github.com/esphome/esphome.git
synced 2026-06-29 12:06:13 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19280e03ad | |||
| 1ff519446b | |||
| ee1fffb062 |
@@ -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
@@ -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') }}
|
||||
|
||||
@@ -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
@@ -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
@@ -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. "
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
CODEOWNERS = ["@Passific", "@koosoli", "@limengdu"]
|
||||
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)});
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ®isters) {
|
||||
return ModbusExceptionCode::ILLEGAL_FUNCTION;
|
||||
};
|
||||
virtual ServerResponseStatus on_modbus_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
|
||||
RegisterValues ®isters) {
|
||||
return this->on_modbus_read_registers(start_address, number_of_registers, registers);
|
||||
};
|
||||
virtual ServerResponseStatus on_modbus_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
|
||||
RegisterValues ®isters) {
|
||||
return this->on_modbus_read_registers(start_address, number_of_registers, registers);
|
||||
};
|
||||
virtual ServerResponseStatus on_modbus_write_registers(uint16_t start_address, const RegisterValues ®isters) {
|
||||
return ModbusExceptionCode::ILLEGAL_FUNCTION;
|
||||
};
|
||||
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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ®isters) {
|
||||
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 ®isters) {
|
||||
// registers holds the values to write in host byte order; its size is the register count.
|
||||
ESP_LOGV(TAG, "Received write registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%zX.",
|
||||
this->address_, start_address, registers.size());
|
||||
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,
|
||||
®isters](const std::function<bool(ServerRegister *, uint16_t register_offset)> &callback) -> bool {
|
||||
uint16_t register_offset = 0;
|
||||
for (uint32_t current_address = start_address; current_address < start_address + registers.size();) {
|
||||
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, ®isters](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([®isters](ServerRegister *server_register, uint16_t register_offset) {
|
||||
int64_t number = registers_to_number(registers.data() + register_offset, registers.size() - register_offset,
|
||||
server_register->value_type);
|
||||
return server_register->write_lambda(number);
|
||||
})) {
|
||||
ESP_LOGW(TAG, "A register write callback failed mid-sequence; earlier writes were already applied.");
|
||||
return ModbusExceptionCode::SERVICE_DEVICE_FAILURE;
|
||||
}
|
||||
|
||||
// Success: the caller builds the write response (an echo of the request header).
|
||||
return {};
|
||||
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 ®isters) 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 ®isters) 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ==================
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user