mirror of
https://github.com/esphome/esphome.git
synced 2026-07-09 08:25:36 +00:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36250682b0 | |||
| 4f75647f63 | |||
| eec770d622 | |||
| d7b21a84a3 | |||
| f05243bd9d | |||
| 35cb28edfe | |||
| 1363f661e6 | |||
| 8af499b591 | |||
| 1a57d9bc2f | |||
| 9768380856 | |||
| 676f26919e | |||
| 29d3a3a498 | |||
| 77b76ac48a | |||
| 0b5835284a | |||
| 15df477472 | |||
| be0ee73847 | |||
| a241c9e622 | |||
| 2f433c78bd | |||
| e39c474577 | |||
| a62e3fe4fc | |||
| 7d6b9bee19 | |||
| ab6bda50e4 | |||
| 3d195d748c | |||
| 16cf4fb5e8 | |||
| 70503442f4 | |||
| 594b269dba | |||
| 8157c721a5 | |||
| 9af557de6d | |||
| 1f4136e76f | |||
| c8dffcc9b8 | |||
| 44fbb7f5a9 | |||
| eb01d43feb | |||
| 7891fd5cf1 | |||
| 4ee9cc432b | |||
| 42ff10afe5 | |||
| 6b3df66bdc | |||
| 968878a62d | |||
| daf3f4d2f1 | |||
| 52e8c50f45 | |||
| 0a4d9b430f | |||
| 0759a3c681 | |||
| 8921e3bb3f | |||
| 52f80618d4 | |||
| 876c8c4c2a | |||
| 41458d72e0 | |||
| 49d3df2698 | |||
| 792f2e8363 | |||
| 42c9fdc87e | |||
| 5f6bbb98ce | |||
| 4e0509435a | |||
| a03de7cea2 | |||
| 95b5ab7e78 | |||
| 3ac0939f55 | |||
| 191d3bc7e4 | |||
| a186f6fea9 | |||
| aea88aef5e | |||
| 433bbdb016 | |||
| 4137d93cbf | |||
| 6a5919ee87 | |||
| b753ee4e94 | |||
| c26ea52620 | |||
| 39a69385fb | |||
| a34836c290 | |||
| 01ac223913 | |||
| 7198c912c7 | |||
| 9f5121e271 | |||
| a1e3ec7118 | |||
| d3bae21d13 | |||
| 9d138e73c9 | |||
| e23a6bf59f |
@@ -39,7 +39,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_test.txt pre-commit
|
||||
pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
|
||||
pip install -e .
|
||||
|
||||
pylint:
|
||||
@@ -108,6 +108,34 @@ jobs:
|
||||
script/generate-esp32-boards.py --check
|
||||
script/generate-rp2040-boards.py --check
|
||||
|
||||
import-time:
|
||||
name: Check import esphome.__main__ time
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.import-time == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Check import time against budget and write waterfall HAR
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
script/check_import_time.py --check --har importtime.har
|
||||
- name: Upload waterfall HAR
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: import-time-waterfall
|
||||
path: importtime.har
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
strategy:
|
||||
@@ -176,6 +204,7 @@ jobs:
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
import-time: ${{ steps.determine.outputs.import-time }}
|
||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||
@@ -219,6 +248,7 @@ jobs:
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
@@ -339,7 +369,7 @@ jobs:
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
|
||||
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
|
||||
@@ -146,5 +146,6 @@ sdkconfig.*
|
||||
|
||||
/components
|
||||
/managed_components
|
||||
/dependencies.lock
|
||||
|
||||
api-docs/
|
||||
|
||||
@@ -347,6 +347,7 @@ esphome/components/modbus_controller/select/* @martgras @stegm
|
||||
esphome/components/modbus_controller/sensor/* @martgras
|
||||
esphome/components/modbus_controller/switch/* @martgras
|
||||
esphome/components/modbus_controller/text_sensor/* @martgras
|
||||
esphome/components/modbus_server/* @exciton
|
||||
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
||||
esphome/components/mopeka_pro_check/* @spbrogan
|
||||
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
||||
|
||||
@@ -597,7 +597,7 @@ async def component_resume_action_to_code(
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, comp)
|
||||
if CONF_UPDATE_INTERVAL in config:
|
||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int)
|
||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
|
||||
cg.add(var.set_update_interval(template_))
|
||||
return var
|
||||
|
||||
|
||||
@@ -62,7 +62,12 @@ void Animation::set_frame(int frame) {
|
||||
}
|
||||
|
||||
void Animation::update_data_start_() {
|
||||
const uint32_t image_size = this->get_width_stride() * this->height_;
|
||||
uint32_t image_size = this->get_width_stride() * this->height_;
|
||||
// RGB565 with an alpha channel stores the alpha plane immediately after the RGB
|
||||
// plane within each frame, so the per-frame stride includes the alpha bytes.
|
||||
if (this->type_ == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
image_size += static_cast<uint32_t>(this->width_) * this->height_;
|
||||
}
|
||||
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
|
||||
}
|
||||
|
||||
|
||||
@@ -1419,6 +1419,8 @@ enum LockState {
|
||||
LOCK_STATE_JAMMED = 3;
|
||||
LOCK_STATE_LOCKING = 4;
|
||||
LOCK_STATE_UNLOCKING = 5;
|
||||
LOCK_STATE_OPENING = 6;
|
||||
LOCK_STATE_OPEN = 7;
|
||||
}
|
||||
enum LockCommand {
|
||||
LOCK_UNLOCK = 0;
|
||||
@@ -1637,7 +1639,7 @@ message BluetoothLEAdvertisementResponse {
|
||||
|
||||
message BluetoothLERawAdvertisement {
|
||||
option (inline_encode) = true;
|
||||
uint64 address = 1 [(force) = true];
|
||||
uint64 address = 1 [(force) = true, (mac_address) = true];
|
||||
sint32 rssi = 2 [(force) = true];
|
||||
uint32 address_type = 3 [(max_value) = 4];
|
||||
|
||||
|
||||
@@ -110,4 +110,10 @@ extend google.protobuf.FieldOptions {
|
||||
// length varint calculations and direct byte writes, since the length
|
||||
// varint is guaranteed to be 1 byte.
|
||||
optional uint32 max_data_length = 50018;
|
||||
|
||||
// mac_address: Field is a 48-bit MAC address stored in a uint64.
|
||||
// Emits encode_varint_raw_48bit which has a 7-byte fast path that avoids
|
||||
// the per-byte loop when the upper bits are non-zero (the common case
|
||||
// for real MAC addresses, since OUIs occupy the top 24 bits).
|
||||
optional bool mac_address = 50019 [default=false];
|
||||
}
|
||||
|
||||
@@ -2352,7 +2352,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
|
||||
uint8_t *len_pos = pos;
|
||||
ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
|
||||
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
|
||||
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
|
||||
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi));
|
||||
if (sub_msg.address_type) {
|
||||
@@ -2373,7 +2373,7 @@ BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
size += 2;
|
||||
size += ProtoSize::calc_uint64_force(1, sub_msg.address);
|
||||
size += ProtoSize::calc_uint64_48bit_force(1, sub_msg.address);
|
||||
size += ProtoSize::calc_sint32_force(1, sub_msg.rssi);
|
||||
size += sub_msg.address_type ? 2 : 0;
|
||||
size += 2 + sub_msg.data_len;
|
||||
|
||||
@@ -181,6 +181,8 @@ enum LockState : uint32_t {
|
||||
LOCK_STATE_JAMMED = 3,
|
||||
LOCK_STATE_LOCKING = 4,
|
||||
LOCK_STATE_UNLOCKING = 5,
|
||||
LOCK_STATE_OPENING = 6,
|
||||
LOCK_STATE_OPEN = 7,
|
||||
};
|
||||
enum LockCommand : uint32_t {
|
||||
LOCK_UNLOCK = 0,
|
||||
|
||||
@@ -487,6 +487,10 @@ template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState v
|
||||
return ESPHOME_PSTR("LOCK_STATE_LOCKING");
|
||||
case enums::LOCK_STATE_UNLOCKING:
|
||||
return ESPHOME_PSTR("LOCK_STATE_UNLOCKING");
|
||||
case enums::LOCK_STATE_OPENING:
|
||||
return ESPHOME_PSTR("LOCK_STATE_OPENING");
|
||||
case enums::LOCK_STATE_OPEN:
|
||||
return ESPHOME_PSTR("LOCK_STATE_OPEN");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API
|
||||
void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements
|
||||
switch (msg_type) {
|
||||
@@ -706,5 +707,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif // USE_API
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -30,6 +30,11 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c
|
||||
|
||||
APIServer::APIServer() { global_api_server = this; }
|
||||
|
||||
// Custom deleter defined here so `delete` sees the complete APIConnection type.
|
||||
// This prevents libc++ from emitting an "incomplete type" error when other
|
||||
// translation units only have the forward declaration of APIConnection.
|
||||
void APIServer::APIConnectionDeleter::operator()(APIConnection *p) const { delete p; }
|
||||
|
||||
void APIServer::socket_failed_(const LogString *msg) {
|
||||
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
||||
this->destroy_socket_();
|
||||
|
||||
@@ -193,7 +193,13 @@ class APIServer final : public Component,
|
||||
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
|
||||
// to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the
|
||||
// APIConnection but cannot reset/move the slot and break the count invariant.
|
||||
using APIConnectionPtr = std::unique_ptr<APIConnection>;
|
||||
// Custom deleter is defined out-of-line in api_server.cpp so libc++ does not
|
||||
// eagerly instantiate `delete static_cast<APIConnection *>(p)` here, where
|
||||
// only the forward declaration of APIConnection is visible (incomplete type).
|
||||
struct APIConnectionDeleter {
|
||||
void operator()(APIConnection *p) const;
|
||||
};
|
||||
using APIConnectionPtr = std::unique_ptr<APIConnection, APIConnectionDeleter>;
|
||||
class ActiveClientsView {
|
||||
const APIConnectionPtr *begin_;
|
||||
const APIConnectionPtr *end_;
|
||||
@@ -292,7 +298,7 @@ class APIServer final : public Component,
|
||||
uint32_t last_connected_{0};
|
||||
|
||||
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
|
||||
std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{};
|
||||
std::array<APIConnectionPtr, MAX_API_CONNECTIONS> clients_{};
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
// Shared proto write buffer for all connections.
|
||||
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
|
||||
|
||||
@@ -342,6 +342,32 @@ class ProtoEncode {
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
/// Encode a 48-bit MAC address (stored in a uint64) as varint.
|
||||
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
|
||||
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
|
||||
/// with no per-byte branch. Falls back to the general loop otherwise.
|
||||
/// Caller must guarantee value fits in 48 bits (checked in debug builds).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint64_t value) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
|
||||
#endif
|
||||
// 7-byte varint holds 49 bits (7 * 7), so a 48-bit value needs all 7 bytes
|
||||
// whenever bit 42 or higher is set (i.e. value >= 1 << (48 - 6)).
|
||||
if (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6))) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 7);
|
||||
pos[0] = static_cast<uint8_t>(value | 0x80);
|
||||
pos[1] = static_cast<uint8_t>((value >> 7) | 0x80);
|
||||
pos[2] = static_cast<uint8_t>((value >> 14) | 0x80);
|
||||
pos[3] = static_cast<uint8_t>((value >> 21) | 0x80);
|
||||
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
|
||||
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
|
||||
pos[6] = static_cast<uint8_t>(value >> 42);
|
||||
pos += 7;
|
||||
return;
|
||||
}
|
||||
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t type) {
|
||||
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
|
||||
@@ -817,6 +843,14 @@ class ProtoSize {
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
return field_id_size + varint(value);
|
||||
}
|
||||
/// 48-bit MAC address variant: matches encode_varint_raw_48bit's fast path.
|
||||
/// When any of the top 6 of 48 bits is set the encoded varint is 7 bytes;
|
||||
/// otherwise fall back to the general size calculation.
|
||||
/// Caller must guarantee value fits in 48 bits (encoder asserts in debug).
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_48bit_force(uint32_t field_id_size,
|
||||
uint64_t value) {
|
||||
return field_id_size + (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6)) ? 7 : varint(value));
|
||||
}
|
||||
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
|
||||
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
|
||||
}
|
||||
|
||||
@@ -50,29 +50,31 @@ void MultiClickTriggerBase::on_state_(bool state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (*this->at_index_ == this->timing_count_) {
|
||||
// at_index_ has a value here (the !has_value() branch above returns).
|
||||
size_t at_index = *this->at_index_;
|
||||
if (at_index == this->timing_count_) {
|
||||
this->trigger_();
|
||||
return;
|
||||
}
|
||||
|
||||
MultiClickTriggerEvent evt = this->timing_[*this->at_index_];
|
||||
MultiClickTriggerEvent evt = this->timing_[at_index];
|
||||
|
||||
if (evt.max_length != 4294967294UL) {
|
||||
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT
|
||||
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, at_index, evt.min_length, evt.max_length); // NOLINT
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
this->schedule_is_not_valid_(evt.max_length);
|
||||
} else if (*this->at_index_ + 1 != this->timing_count_) {
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
} else if (at_index + 1 != this->timing_count_) {
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
|
||||
this->is_valid_ = false;
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||
}
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
this->at_index_ = at_index + 1;
|
||||
}
|
||||
void MultiClickTriggerBase::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
|
||||
@@ -78,43 +78,43 @@ void BME680Component::setup() {
|
||||
}
|
||||
|
||||
// Read calibration
|
||||
uint8_t cal1[25];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF1, cal1, 25)) {
|
||||
uint8_t coeff1[25];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF1, coeff1, 25)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint8_t cal2[16];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF2, cal2, 16)) {
|
||||
uint8_t coeff2[16];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF2, coeff2, 16)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->calibration_.t1 = cal2[9] << 8 | cal2[8];
|
||||
this->calibration_.t2 = cal1[2] << 8 | cal1[1];
|
||||
this->calibration_.t3 = cal1[3];
|
||||
this->calibration_.t1 = coeff2[9] << 8 | coeff2[8];
|
||||
this->calibration_.t2 = coeff1[2] << 8 | coeff1[1];
|
||||
this->calibration_.t3 = coeff1[3];
|
||||
|
||||
this->calibration_.h1 = cal2[2] << 4 | (cal2[1] & 0x0F);
|
||||
this->calibration_.h2 = cal2[0] << 4 | cal2[1] >> 4;
|
||||
this->calibration_.h3 = cal2[3];
|
||||
this->calibration_.h4 = cal2[4];
|
||||
this->calibration_.h5 = cal2[5];
|
||||
this->calibration_.h6 = cal2[6];
|
||||
this->calibration_.h7 = cal2[7];
|
||||
this->calibration_.h1 = coeff2[2] << 4 | (coeff2[1] & 0x0F);
|
||||
this->calibration_.h2 = coeff2[0] << 4 | coeff2[1] >> 4;
|
||||
this->calibration_.h3 = coeff2[3];
|
||||
this->calibration_.h4 = coeff2[4];
|
||||
this->calibration_.h5 = coeff2[5];
|
||||
this->calibration_.h6 = coeff2[6];
|
||||
this->calibration_.h7 = coeff2[7];
|
||||
|
||||
this->calibration_.p1 = cal1[6] << 8 | cal1[5];
|
||||
this->calibration_.p2 = cal1[8] << 8 | cal1[7];
|
||||
this->calibration_.p3 = cal1[9];
|
||||
this->calibration_.p4 = cal1[12] << 8 | cal1[11];
|
||||
this->calibration_.p5 = cal1[14] << 8 | cal1[13];
|
||||
this->calibration_.p6 = cal1[16];
|
||||
this->calibration_.p7 = cal1[15];
|
||||
this->calibration_.p8 = cal1[20] << 8 | cal1[19];
|
||||
this->calibration_.p9 = cal1[22] << 8 | cal1[21];
|
||||
this->calibration_.p10 = cal1[23];
|
||||
this->calibration_.p1 = coeff1[6] << 8 | coeff1[5];
|
||||
this->calibration_.p2 = coeff1[8] << 8 | coeff1[7];
|
||||
this->calibration_.p3 = coeff1[9];
|
||||
this->calibration_.p4 = coeff1[12] << 8 | coeff1[11];
|
||||
this->calibration_.p5 = coeff1[14] << 8 | coeff1[13];
|
||||
this->calibration_.p6 = coeff1[16];
|
||||
this->calibration_.p7 = coeff1[15];
|
||||
this->calibration_.p8 = coeff1[20] << 8 | coeff1[19];
|
||||
this->calibration_.p9 = coeff1[22] << 8 | coeff1[21];
|
||||
this->calibration_.p10 = coeff1[23];
|
||||
|
||||
this->calibration_.gh1 = cal2[14];
|
||||
this->calibration_.gh2 = cal2[12] << 8 | cal2[13];
|
||||
this->calibration_.gh3 = cal2[15];
|
||||
this->calibration_.gh1 = coeff2[14];
|
||||
this->calibration_.gh2 = coeff2[12] << 8 | coeff2[13];
|
||||
this->calibration_.gh3 = coeff2[15];
|
||||
|
||||
uint8_t temp_var = 0;
|
||||
if (!this->read_byte(0x02, &temp_var)) {
|
||||
|
||||
@@ -193,11 +193,14 @@ def _validate_ex1_wakeup_mode(value):
|
||||
|
||||
|
||||
def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
|
||||
if not CORE.is_bk72xx:
|
||||
return value
|
||||
max_duration = core.TimePeriod(hours=36)
|
||||
if value > max_duration:
|
||||
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
|
||||
if CORE.is_bk72xx:
|
||||
max_duration = core.TimePeriod(hours=36)
|
||||
if value > max_duration:
|
||||
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
|
||||
elif CORE.using_zephyr:
|
||||
max_duration = core.TimePeriod(days=49)
|
||||
if value > max_duration:
|
||||
raise cv.Invalid("sleep duration cannot be more than 49 days on Zephyr")
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -9,18 +9,11 @@ static const char *const TAG = "deep_sleep";
|
||||
// 5 seconds for deep sleep to ensure clean disconnect from Home Assistant
|
||||
static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000;
|
||||
|
||||
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void DeepSleepComponent::setup() {
|
||||
#ifdef USE_ZEPHYR
|
||||
k_sem_init(&this->wakeup_sem_, 0, 1);
|
||||
#endif
|
||||
global_has_deep_sleep = true;
|
||||
this->schedule_sleep_();
|
||||
// It can be used from another thread for waking up the device.
|
||||
// It should be called as last item in setup.
|
||||
global_deep_sleep.store(this);
|
||||
}
|
||||
|
||||
void DeepSleepComponent::schedule_sleep_() {
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <atomic>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_sleep.h>
|
||||
#endif
|
||||
@@ -15,10 +13,6 @@
|
||||
#include "esphome/core/time.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <zephyr/kernel.h>
|
||||
#endif
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
@@ -125,9 +119,6 @@ class DeepSleepComponent : public Component {
|
||||
|
||||
void prevent_deep_sleep();
|
||||
void allow_deep_sleep();
|
||||
#ifdef USE_ZEPHYR
|
||||
void wakeup();
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Returns nullopt if no run duration is set. Otherwise, returns the run
|
||||
@@ -167,9 +158,6 @@ class DeepSleepComponent : public Component {
|
||||
optional<uint32_t> run_duration_;
|
||||
bool next_enter_deep_sleep_{false};
|
||||
bool prevent_{false};
|
||||
#ifdef USE_ZEPHYR
|
||||
k_sem wakeup_sem_;
|
||||
#endif
|
||||
};
|
||||
|
||||
extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -256,8 +244,5 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ
|
||||
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
|
||||
};
|
||||
|
||||
extern std::atomic<DeepSleepComponent *>
|
||||
global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace deep_sleep
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
#include "deep_sleep_component.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/wake.h"
|
||||
#include <zephyr/sys/poweroff.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/stats/stats.h>
|
||||
#include <zephyr/pm/pm.h>
|
||||
|
||||
namespace esphome::deep_sleep {
|
||||
|
||||
static const char *const TAG = "deep_sleep";
|
||||
|
||||
void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); }
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
void DeepSleepComponent::dump_config_platform_() {}
|
||||
@@ -19,9 +15,8 @@ void DeepSleepComponent::dump_config_platform_() {}
|
||||
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
|
||||
|
||||
void DeepSleepComponent::deep_sleep_() {
|
||||
k_timeout_t sleep_duration = K_FOREVER;
|
||||
if (this->sleep_duration_.has_value()) {
|
||||
sleep_duration = K_USEC(*this->sleep_duration_);
|
||||
esphome::internal::wakeable_delay(static_cast<uint32_t>(*this->sleep_duration_ / 1000));
|
||||
} else {
|
||||
#ifndef USE_ZIGBEE
|
||||
// the device can be woken up through one of the following signals:
|
||||
@@ -33,11 +28,12 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
//
|
||||
// The system is reset when it wakes up from System OFF mode.
|
||||
sys_poweroff();
|
||||
#else
|
||||
esphome::internal::wakeable_delay(UINT32_MAX);
|
||||
#endif
|
||||
}
|
||||
// It might wake up immediately if k_sem_give was called again after wake up
|
||||
int ret = k_sem_take(&this->wakeup_sem_, sleep_duration);
|
||||
if (ret == 0) {
|
||||
const bool woke = esphome::wake_request_take();
|
||||
if (woke) {
|
||||
ESP_LOGD(TAG, "Woken up by another thread");
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Timeout expired (normal sleep)");
|
||||
|
||||
@@ -29,7 +29,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component {
|
||||
protected:
|
||||
void control(const AlarmControlPanelCall &call) override {
|
||||
auto state = call.get_state().value_or(ACP_STATE_DISARMED);
|
||||
auto code = call.get_code();
|
||||
const auto &code = call.get_code();
|
||||
switch (state) {
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
if (this->get_requires_code_to_arm()) {
|
||||
|
||||
@@ -104,8 +104,9 @@ int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
|
||||
if (this->is_full()) {
|
||||
ESP_LOGE(TAG, "Command queue is full");
|
||||
return -1;
|
||||
} else if (this->is_empty())
|
||||
} else if (this->is_empty()) {
|
||||
front_++;
|
||||
}
|
||||
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
|
||||
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
|
||||
return 1;
|
||||
|
||||
@@ -1724,15 +1724,16 @@ async def to_code(config):
|
||||
CORE.relative_internal_path(".espressif")
|
||||
)
|
||||
|
||||
# Both ESP-IDF and ESP32 Arduino builds generate IDF app metadata. Keep
|
||||
# volatile build path/time data out of the binary so equivalent projects can
|
||||
# produce reproducible outputs and downstream tooling can reuse artifacts.
|
||||
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
# Strip volatile build path/time metadata from PlatformIO-managed
|
||||
# ESP-IDF builds so equivalent projects can produce reproducible
|
||||
# outputs and downstream tooling can safely reuse artifacts.
|
||||
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
|
||||
|
||||
# Wrap std::__throw_* functions to abort immediately, eliminating ~3KB of
|
||||
# exception class overhead. See throw_stubs.cpp for implementation.
|
||||
|
||||
@@ -22,7 +22,7 @@ extern "C" __attribute__((weak)) void initArduino() {}
|
||||
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { vPortYield(); }
|
||||
// yield(), delay(), micros(), millis_64() inlined in hal.h.
|
||||
// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
|
||||
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
|
||||
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
|
||||
@@ -37,15 +37,6 @@ uint32_t IRAM_ATTR HOT millis() {
|
||||
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
|
||||
#endif
|
||||
}
|
||||
// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is
|
||||
// safe because the two are never cross-compared: millis() values are only used for
|
||||
// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while
|
||||
// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME),
|
||||
// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly,
|
||||
// so the Scheduler is internally consistent on the esp_timer clock.
|
||||
uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); }
|
||||
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
void arch_restart() {
|
||||
esp_restart();
|
||||
|
||||
@@ -18,6 +18,12 @@ struct NVSData {
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// open() runs from app_main() before the logger is initialized, so any failure
|
||||
// must be deferred until after global_logger is set. This is emitted from the
|
||||
// first make_preference() call, which runs from the generated setup() after
|
||||
// log->pre_setup() has run at EARLY_INIT priority.
|
||||
static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
@@ -70,12 +76,14 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
}
|
||||
|
||||
void ESP32Preferences::open() {
|
||||
// Runs from app_main() before the logger is initialized; any logging here
|
||||
// must be deferred. See s_open_err and make_preference() below.
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err == 0)
|
||||
return;
|
||||
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
|
||||
s_open_err = err;
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
nvs_flash_init();
|
||||
@@ -87,6 +95,14 @@ void ESP32Preferences::open() {
|
||||
}
|
||||
|
||||
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
|
||||
if (s_open_err != ESP_OK) {
|
||||
if (this->nvs_handle == 0) {
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err));
|
||||
}
|
||||
s_open_err = ESP_OK;
|
||||
}
|
||||
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->nvs_handle = this->nvs_handle;
|
||||
pref->key = type;
|
||||
|
||||
@@ -104,7 +104,7 @@ ESPBTUUID ESPBTUUID::as_128bit() const {
|
||||
} else {
|
||||
uuid32 = this->uuid_.uuid.uuid16;
|
||||
}
|
||||
for (uint8_t i = 0; i < this->uuid_.len; i++) {
|
||||
for (uint16_t i = 0; i < this->uuid_.len; i++) {
|
||||
data[12 + i] = ((uuid32 >> i * 8) & 0xFF);
|
||||
}
|
||||
return ESPBTUUID::from_raw(data);
|
||||
|
||||
@@ -216,6 +216,7 @@ void ESP32TouchComponent::setup() {
|
||||
// Do initial oneshot scans to populate baseline values
|
||||
for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) {
|
||||
err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS);
|
||||
App.feed_wdt(); // 3 scans with 2s timeout might exceed WDT, so feed it here to be safe
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err));
|
||||
}
|
||||
|
||||
@@ -314,6 +314,11 @@ async def to_code(config):
|
||||
for symbol in ("vprintf", "printf", "fprintf"):
|
||||
cg.add_build_flag(f"-Wl,--wrap={symbol}")
|
||||
|
||||
# Wrap Arduino's millis() so all callers (including Arduino libraries and ISR
|
||||
# handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply
|
||||
# implementation in the Arduino ESP8266 core.
|
||||
cg.add_build_flag("-Wl,--wrap=millis")
|
||||
|
||||
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
|
||||
|
||||
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
|
||||
@@ -15,11 +15,75 @@ extern "C" {
|
||||
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { ::yield(); }
|
||||
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
|
||||
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
|
||||
void HOT delay(uint32_t ms) { ::delay(ms); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
|
||||
// yield(), micros(), millis_64() inlined in hal.h.
|
||||
// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit
|
||||
// multiplies on the LX106). Tracks a running ms counter from 32-bit
|
||||
// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis
|
||||
// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g.
|
||||
// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static
|
||||
// state against ISR re-entry; the critical section is bounded (≤10 while-loop
|
||||
// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on
|
||||
// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level
|
||||
// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis().
|
||||
//
|
||||
// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles
|
||||
// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a
|
||||
// >71 min block would trip the watchdog long before it could matter here.
|
||||
static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000;
|
||||
static constexpr uint32_t US_PER_MS = 1000;
|
||||
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
// Struct packs the three statics so the compiler loads one base address
|
||||
// instead of three separate literal pool entries (saves ~8 bytes IRAM).
|
||||
static struct {
|
||||
uint32_t cache;
|
||||
uint32_t remainder;
|
||||
uint32_t last_us;
|
||||
} state = {0, 0, 0};
|
||||
uint32_t ps = xt_rsil(15);
|
||||
uint32_t now_us = system_get_time();
|
||||
uint32_t delta = now_us - state.last_us;
|
||||
state.last_us = now_us;
|
||||
state.remainder += delta;
|
||||
if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) {
|
||||
// Rare path: large gap (WiFi scan, boot, long block). Constant-time
|
||||
// conversion keeps the critical section bounded.
|
||||
uint32_t ms = state.remainder / US_PER_MS;
|
||||
state.cache += ms;
|
||||
// Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a
|
||||
// second __umodsi3 call on the LX106 (no hardware divide).
|
||||
state.remainder -= ms * US_PER_MS;
|
||||
} else {
|
||||
// Common path: small gap. At most ~10 iterations since remainder was
|
||||
// < threshold (10 ms) on entry and delta adds at most one more threshold
|
||||
// before exiting this branch.
|
||||
while (state.remainder >= US_PER_MS) {
|
||||
state.cache++;
|
||||
state.remainder -= US_PER_MS;
|
||||
}
|
||||
}
|
||||
uint32_t result = state.cache;
|
||||
xt_wsr_ps(ps);
|
||||
return result;
|
||||
}
|
||||
// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object
|
||||
// call to the original millis() that --wrap can't intercept, so calling ::delay()
|
||||
// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still
|
||||
// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and
|
||||
// WiFi run correctly. Theoretically less power-efficient than Arduino's
|
||||
// os_timer-based delay() for long waits, but nearly all ESPHome delays are short
|
||||
// (sensor/I²C/SPI settling in the 1–100 ms range) where the difference is
|
||||
// negligible.
|
||||
void HOT delay(uint32_t ms) {
|
||||
if (ms == 0) {
|
||||
optimistic_yield(1000);
|
||||
return;
|
||||
}
|
||||
uint32_t start = millis();
|
||||
while (millis() - start < ms) {
|
||||
optimistic_yield(1000);
|
||||
}
|
||||
}
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
void arch_restart() {
|
||||
system_restart();
|
||||
@@ -78,4 +142,12 @@ extern "C" void resetPins() { // NOLINT
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator.
|
||||
// Requires -Wl,--wrap=millis in build flags (added by __init__.py).
|
||||
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); }
|
||||
// Note: Arduino's init() registers a 60-second overflow timer for micros64().
|
||||
// We leave it running — wrapping init() as a no-op would break micros64()'s
|
||||
// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s).
|
||||
|
||||
#endif // USE_ESP8266
|
||||
|
||||
@@ -292,6 +292,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
bool update_started = false;
|
||||
size_t total = 0;
|
||||
uint32_t last_progress = 0;
|
||||
uint32_t last_data_ms = 0;
|
||||
uint8_t buf[OTA_BUFFER_SIZE];
|
||||
char *sbuf = reinterpret_cast<char *>(buf);
|
||||
size_t ota_size;
|
||||
@@ -350,8 +351,18 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
// Acknowledge MD5 OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||
|
||||
// Track when we last received data so a silently-vanished peer (no FIN/RST
|
||||
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
|
||||
// can't wedge the device indefinitely. Without this, the loop only exits
|
||||
// on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP
|
||||
// TCP keepalive isn't enabled here.
|
||||
last_data_ms = millis();
|
||||
while (total < ota_size) {
|
||||
// TODO: timeout check
|
||||
if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
|
||||
ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
size_t remaining = ota_size - total;
|
||||
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
|
||||
ssize_t read = this->client_->read(buf, requested);
|
||||
@@ -369,6 +380,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
last_data_ms = millis();
|
||||
error_code = this->backend_->write(buf, read);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGW(TAG, "Flash write err %d", error_code);
|
||||
|
||||
@@ -35,7 +35,7 @@ void EZOSensor::update() {
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
|
||||
auto ezo_command = make_unique<EzoCommand>();
|
||||
ezo_command->command = "R";
|
||||
ezo_command->command_type = EzoCommandType::EZO_READ;
|
||||
ezo_command->delay_ms = 900;
|
||||
@@ -162,7 +162,7 @@ void EZOSensor::loop() {
|
||||
}
|
||||
|
||||
void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) {
|
||||
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
|
||||
auto ezo_command = make_unique<EzoCommand>();
|
||||
ezo_command->command = command;
|
||||
ezo_command->command_type = command_type;
|
||||
ezo_command->delay_ms = delay_ms;
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace feedback {
|
||||
namespace esphome::feedback {
|
||||
|
||||
static const char *const TAG = "feedback.cover";
|
||||
|
||||
static constexpr uint32_t DIRECTION_CHANGE_TIMEOUT_ID = 1;
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
void FeedbackCover::setup() {
|
||||
@@ -37,7 +38,7 @@ void FeedbackCover::setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
this->last_recompute_time_ = this->start_dir_time_ = millis();
|
||||
this->last_recompute_time_ = this->start_dir_time_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
CoverTraits FeedbackCover::get_traits() {
|
||||
@@ -135,7 +136,7 @@ void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop
|
||||
#endif
|
||||
|
||||
void FeedbackCover::endstop_reached_(bool open_endstop) {
|
||||
const uint32_t now = millis();
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
|
||||
|
||||
@@ -174,7 +175,7 @@ void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool
|
||||
if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
|
||||
#endif
|
||||
{
|
||||
auto now = millis();
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->current_operation = operation;
|
||||
this->start_dir_time_ = this->last_recompute_time_ = now;
|
||||
this->publish_state();
|
||||
@@ -306,7 +307,7 @@ void FeedbackCover::control(const CoverCall &call) {
|
||||
|
||||
void FeedbackCover::stop_prev_trigger_() {
|
||||
if (this->direction_change_waittime_.has_value()) {
|
||||
this->cancel_timeout("direction_change");
|
||||
this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
|
||||
}
|
||||
if (this->prev_command_trigger_ != nullptr) {
|
||||
this->prev_command_trigger_->stop_action();
|
||||
@@ -377,7 +378,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
|
||||
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
|
||||
this->set_timeout("direction_change", *this->direction_change_waittime_,
|
||||
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, *this->direction_change_waittime_,
|
||||
[this, dir]() { this->start_direction_(dir); });
|
||||
|
||||
} else {
|
||||
@@ -395,7 +396,7 @@ void FeedbackCover::recompute_position_() {
|
||||
if (this->current_operation == COVER_OPERATION_IDLE)
|
||||
return;
|
||||
|
||||
const uint32_t now = millis();
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
float dir;
|
||||
float action_dur;
|
||||
float min_pos;
|
||||
@@ -451,5 +452,4 @@ void FeedbackCover::recompute_position_() {
|
||||
this->last_recompute_time_ = now;
|
||||
}
|
||||
|
||||
} // namespace feedback
|
||||
} // namespace esphome
|
||||
} // namespace esphome::feedback
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#endif
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace feedback {
|
||||
namespace esphome::feedback {
|
||||
|
||||
class FeedbackCover : public cover::Cover, public Component {
|
||||
public:
|
||||
@@ -85,5 +84,4 @@ class FeedbackCover : public cover::Cover, public Component {
|
||||
uint32_t update_interval_{1000};
|
||||
};
|
||||
|
||||
} // namespace feedback
|
||||
} // namespace esphome
|
||||
} // namespace esphome::feedback
|
||||
|
||||
@@ -210,8 +210,9 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
|
||||
#ifdef USE_WIFI
|
||||
else if (this->send_wifi_signal_ &&
|
||||
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
|
||||
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
|
||||
SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) {
|
||||
this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
|
||||
}
|
||||
#endif
|
||||
} break;
|
||||
default:
|
||||
|
||||
@@ -462,7 +462,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
this->request_headers_.push_back({key, value});
|
||||
}
|
||||
|
||||
void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); }
|
||||
void add_collect_header(const char *value) { this->lower_case_collect_headers_.emplace_back(value); }
|
||||
|
||||
void init_json(size_t count) { this->json_.init(count); }
|
||||
void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.push_back({key, value}); }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "i2c_bus.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
struct device;
|
||||
struct device; // NOLINT(readability-identifier-naming) - forward decl of Zephyr's device type
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
|
||||
@@ -744,21 +744,28 @@ async def write_image(config, all_frames=False):
|
||||
if frame_count <= 1:
|
||||
_LOGGER.warning("Image file %s has no animation frames", path)
|
||||
|
||||
total_rows = height * frame_count
|
||||
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
||||
if byte_order := config.get(CONF_BYTE_ORDER):
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
# Encode each frame with its own encoder and concatenate. This keeps every
|
||||
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
|
||||
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
|
||||
# correct without needing to know the total frame count.
|
||||
byte_order = config.get(CONF_BYTE_ORDER)
|
||||
combined_data: list[int] = []
|
||||
encoder: ImageEncoder | None = None
|
||||
for frame_index in range(frame_count):
|
||||
image.seek(frame_index)
|
||||
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
|
||||
if byte_order is not None:
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
encoder.end_row()
|
||||
encoder.end_image()
|
||||
encoder.end_image()
|
||||
combined_data.extend(encoder.data)
|
||||
|
||||
rhs = [HexInt(x) for x in encoder.data]
|
||||
rhs = [HexInt(x) for x in combined_data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
image_type = get_image_type_enum(type)
|
||||
trans_value = get_transparency_enum(encoder.transparency)
|
||||
|
||||
@@ -41,12 +41,12 @@ bool InkbirdIbstH1Mini::parse_device(const esp32_ble_tracker::ESPBTDevice &devic
|
||||
ESP_LOGVV(TAG, "parse_device(): service_data is expected to be empty");
|
||||
return false;
|
||||
}
|
||||
auto mnf_datas = device.get_manufacturer_datas();
|
||||
const auto &mnf_datas = device.get_manufacturer_datas();
|
||||
if (mnf_datas.size() != 1) {
|
||||
ESP_LOGVV(TAG, "parse_device(): manufacturer_datas is expected to have a single element");
|
||||
return false;
|
||||
}
|
||||
auto mnf_data = mnf_datas[0];
|
||||
const auto &mnf_data = mnf_datas[0];
|
||||
if (mnf_data.uuid.get_uuid().len != ESP_UUID_LEN_16) {
|
||||
ESP_LOGVV(TAG, "parse_device(): manufacturer data element is expected to have uuid of length 16");
|
||||
return false;
|
||||
|
||||
@@ -39,7 +39,8 @@ bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
|
||||
}
|
||||
|
||||
JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape) false positives with
|
||||
// ArduinoJson
|
||||
if (data == nullptr || len == 0) {
|
||||
ESP_LOGE(TAG, "No data to parse");
|
||||
return JsonObject(); // return unbound object
|
||||
@@ -63,7 +64,7 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||
}
|
||||
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
||||
return JsonObject(); // return unbound object
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape)
|
||||
}
|
||||
|
||||
SerializationBuffer<> JsonBuilder::serialize() {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/time_64.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
|
||||
@@ -15,32 +14,7 @@ void loop();
|
||||
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { ::yield(); }
|
||||
// Inline the tick read so esphome::millis() matches MillisInternal::get()'s fast
|
||||
// path instead of going through the Arduino core's out-of-line ::millis() wrapper.
|
||||
//
|
||||
// RTL87xx / LN882x (1 kHz): xTaskGetTickCount() is already ms. IRAM_ATTR + ISR
|
||||
// dispatch are needed because ISR handlers (e.g. rotary_encoder) call millis().
|
||||
//
|
||||
// BK72xx (500 Hz): ticks * portTICK_PERIOD_MS (== 2). IRAM_ATTR and ISR dispatch
|
||||
// are both unnecessary — the SDK masks FIQ + IRQ during flash writes (see hal.h),
|
||||
// so no ISR runs while flash is stalled.
|
||||
#if defined(USE_RTL87XX) || defined(USE_LN882X)
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
|
||||
return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount();
|
||||
}
|
||||
#elif defined(USE_BK72XX)
|
||||
uint32_t HOT millis() {
|
||||
static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick");
|
||||
return xTaskGetTickCount() * portTICK_PERIOD_MS;
|
||||
}
|
||||
#else
|
||||
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
|
||||
#endif
|
||||
uint64_t millis_64() { return Millis64Impl::compute(millis()); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
|
||||
void HOT delay(uint32_t ms) { ::delay(ms); }
|
||||
// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h.
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
|
||||
|
||||
void arch_init() {
|
||||
|
||||
@@ -4,15 +4,25 @@ from esphome.components.binary_sensor import (
|
||||
new_binary_sensor,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_STATE
|
||||
|
||||
from ..defines import CONF_WIDGET
|
||||
from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lvgl_static
|
||||
from ..types import LV_EVENT, lv_pseudo_button_t
|
||||
from ..defines import CONF_WIDGET, LV_OBJ_FLAG, LvConstant
|
||||
from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static
|
||||
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t
|
||||
from ..widgets import Widget, get_widgets, wait_for_widgets
|
||||
|
||||
STATE_PRESSED = "PRESSED"
|
||||
STATE_CHECKED = "CHECKED"
|
||||
|
||||
BS_STATE = LvConstant(
|
||||
"LV_STATE_",
|
||||
STATE_PRESSED,
|
||||
STATE_CHECKED,
|
||||
)
|
||||
CONFIG_SCHEMA = binary_sensor_schema(BinarySensor).extend(
|
||||
{
|
||||
cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
|
||||
cv.Optional(CONF_STATE, default=STATE_PRESSED): BS_STATE.one_of,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -22,16 +32,23 @@ async def to_code(config):
|
||||
widget = await get_widgets(config, CONF_WIDGET)
|
||||
widget = widget[0]
|
||||
assert isinstance(widget, Widget)
|
||||
state = await BS_STATE.process(config[CONF_STATE])
|
||||
await wait_for_widgets()
|
||||
async with LambdaContext(EVENT_ARG) as pressed_ctx:
|
||||
pressed_ctx.add(sensor.publish_state(widget.is_pressed()))
|
||||
is_pressed = str(state) == str(LV_STATE.PRESSED)
|
||||
test_expr = widget.is_pressed() if is_pressed else widget.is_checked()
|
||||
async with LambdaContext(EVENT_ARG) as test_ctx:
|
||||
test_ctx.add(sensor.publish_state(test_expr))
|
||||
async with LvContext() as ctx:
|
||||
ctx.add(sensor.publish_initial_state(widget.is_pressed()))
|
||||
ctx.add(sensor.publish_initial_state(test_expr))
|
||||
if is_pressed:
|
||||
events = [LV_EVENT.PRESSED, LV_EVENT.RELEASED]
|
||||
widget.add_flag(LV_OBJ_FLAG.CLICKABLE)
|
||||
else:
|
||||
events = [LV_EVENT.VALUE_CHANGED, UPDATE_EVENT]
|
||||
ctx.add(
|
||||
lvgl_static.add_event_cb(
|
||||
widget.obj,
|
||||
await pressed_ctx.get_lambda(),
|
||||
LV_EVENT.PRESSED,
|
||||
LV_EVENT.RELEASED,
|
||||
await test_ctx.get_lambda(),
|
||||
*events,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from operator import itemgetter
|
||||
|
||||
from esphome import config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import (
|
||||
@@ -11,6 +13,7 @@ from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning
|
||||
from .helpers import add_lv_use
|
||||
from .lv_validation import lv_color, lv_percentage, opacity
|
||||
from .lvcode import lv
|
||||
from .types import lv_color_t, lv_gradient_t, lv_opa_t
|
||||
@@ -50,6 +53,7 @@ GRADIENT_SCHEMA = cv.ensure_list(
|
||||
|
||||
|
||||
async def gradients_to_code(config):
|
||||
add_lv_use("gradient")
|
||||
max_stops = 2
|
||||
if any(CONF_DITHER in x for x in config.get(CONF_GRADIENTS, ())):
|
||||
add_warning(
|
||||
@@ -58,7 +62,7 @@ async def gradients_to_code(config):
|
||||
for gradient in config.get(CONF_GRADIENTS, ()):
|
||||
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
|
||||
idbase = gradient[CONF_ID].id
|
||||
stops = gradient[CONF_STOPS]
|
||||
stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION))
|
||||
max_stops = max(max_stops, len(stops))
|
||||
if gradient[CONF_DIRECTION].startswith("VER"):
|
||||
lv.grad_vertical_init(var)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
@@ -85,6 +86,22 @@ def grid_free_space(value):
|
||||
|
||||
grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space)
|
||||
|
||||
|
||||
def grid_dimension(value):
|
||||
"""
|
||||
Validator for a grid `rows` or `columns` value.
|
||||
Accepts either a positive integer (interpreted as that many cells of equal
|
||||
`LV_GRID_FR(1)` size) or a non-empty list of grid specs.
|
||||
"""
|
||||
if isinstance(value, int):
|
||||
value = cv.int_range(min=1)(value)
|
||||
return ["LV_GRID_FR(1)"] * value
|
||||
result = cv.Schema([grid_spec])(value)
|
||||
if not result:
|
||||
raise cv.Invalid("Grid dimension list must contain at least one entry")
|
||||
return result
|
||||
|
||||
|
||||
GRID_CELL_SCHEMA = {
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
@@ -184,7 +201,16 @@ class DirectionalLayout(FlexLayout):
|
||||
|
||||
|
||||
class GridLayout(Layout):
|
||||
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$")
|
||||
# Match shorthand grid layout strings: "NxM", "Nx" or "xM".
|
||||
# At least one of the two numbers must be present; this is enforced after matching.
|
||||
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)?\s*x\s*(\d+)?\s*$")
|
||||
|
||||
@staticmethod
|
||||
def _match_shorthand(layout):
|
||||
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
|
||||
if match is None or (match.group(1) is None and match.group(2) is None):
|
||||
return None
|
||||
return match
|
||||
|
||||
def get_type(self):
|
||||
return TYPE_GRID
|
||||
@@ -192,7 +218,7 @@ class GridLayout(Layout):
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
if isinstance(layout, str):
|
||||
if GridLayout._GRID_LAYOUT_REGEX.match(layout):
|
||||
if GridLayout._match_shorthand(layout):
|
||||
return (
|
||||
cv.string,
|
||||
{
|
||||
@@ -213,59 +239,107 @@ class GridLayout(Layout):
|
||||
|
||||
if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_GRID:
|
||||
return None, {}
|
||||
x_default = (
|
||||
"center" if isinstance(layout.get(CONF_GRID_ROWS), int) else cv.UNDEFINED
|
||||
)
|
||||
y_default = (
|
||||
"center" if isinstance(layout.get(CONF_GRID_COLUMNS), int) else cv.UNDEFINED
|
||||
)
|
||||
x_align = layout.get(CONF_GRID_CELL_X_ALIGN, x_default)
|
||||
y_align = layout.get(CONF_GRID_CELL_Y_ALIGN, y_default)
|
||||
return (
|
||||
{
|
||||
cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True),
|
||||
cv.Required(CONF_GRID_ROWS): [grid_spec],
|
||||
cv.Required(CONF_GRID_COLUMNS): [grid_spec],
|
||||
cv.Optional(CONF_GRID_ROWS): grid_dimension,
|
||||
cv.Optional(CONF_GRID_COLUMNS): grid_dimension,
|
||||
cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_PAD_ROW): padding,
|
||||
cv.Optional(CONF_PAD_COLUMN): padding,
|
||||
cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean,
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
},
|
||||
{
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1),
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1),
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN, default=x_align): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN, default=y_align): grid_alignments,
|
||||
},
|
||||
)
|
||||
|
||||
def validate(self, config: dict):
|
||||
"""
|
||||
Validate the grid layout.
|
||||
The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns".
|
||||
The `layout:` key may be a dictionary with `rows` and/or `columns` keys, or a
|
||||
shorthand string in the format "<rows>x<columns>", "<rows>x" or "x<columns>".
|
||||
Either dimension may be omitted, in which case it will be calculated from the
|
||||
other dimension and the number of configured widgets.
|
||||
Either all cells must have a row and column,
|
||||
or none, in which case the grid layout is auto-generated.
|
||||
:param config:
|
||||
:return: The config updated with auto-generated values
|
||||
"""
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
widgets = config.get(CONF_WIDGETS, [])
|
||||
num_widgets = len(widgets)
|
||||
if isinstance(layout, str):
|
||||
# If the layout is a string, assume it is in the format "rows x columns", implying
|
||||
# a grid layout with the specified number of rows and columns each with CONTENT sizing.
|
||||
# Shorthand string: "<rows>x<columns>", "<rows>x" or "x<columns>".
|
||||
# Each dimension defaults to LV_GRID_FR(1). A missing dimension is
|
||||
# calculated from the other dimension and the number of widgets.
|
||||
layout = layout.strip()
|
||||
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
|
||||
if match:
|
||||
rows = int(match.group(1))
|
||||
cols = int(match.group(2))
|
||||
layout = {
|
||||
CONF_TYPE: TYPE_GRID,
|
||||
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
|
||||
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
|
||||
}
|
||||
config[CONF_LAYOUT] = layout
|
||||
else:
|
||||
match = GridLayout._match_shorthand(layout)
|
||||
if not match:
|
||||
raise cv.Invalid(
|
||||
f"Invalid grid layout format: {config}, expected 'rows x columns'",
|
||||
f"Invalid grid layout format: {layout!r}, expected "
|
||||
"'<rows>x<columns>', '<rows>x' or 'x<columns>'",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
rows_int = int(match.group(1)) if match.group(1) is not None else None
|
||||
cols_int = int(match.group(2)) if match.group(2) is not None else None
|
||||
for label, val in (("row", rows_int), ("column", cols_int)):
|
||||
if val is not None and val < 1:
|
||||
raise cv.Invalid(
|
||||
f"Invalid grid layout {layout!r}: {label} count must be "
|
||||
"at least 1",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
if rows_int is not None and cols_int is not None:
|
||||
rows = rows_int
|
||||
cols = cols_int
|
||||
elif rows_int is not None:
|
||||
rows = rows_int
|
||||
cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1
|
||||
else:
|
||||
cols = cols_int
|
||||
rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1
|
||||
layout = {
|
||||
CONF_TYPE: TYPE_GRID,
|
||||
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
|
||||
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
|
||||
}
|
||||
config[CONF_LAYOUT] = layout
|
||||
# should be guaranteed to be a dict at this point
|
||||
assert isinstance(layout, dict)
|
||||
assert layout.get(CONF_TYPE).lower() == TYPE_GRID
|
||||
rows_list = layout.get(CONF_GRID_ROWS)
|
||||
cols_list = layout.get(CONF_GRID_COLUMNS)
|
||||
if rows_list is None and cols_list is None:
|
||||
raise cv.Invalid(
|
||||
"Grid layout requires at least one of 'rows' or 'columns' to be "
|
||||
"specified",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
if rows_list is None:
|
||||
cols = len(cols_list)
|
||||
rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1
|
||||
layout[CONF_GRID_ROWS] = ["LV_GRID_FR(1)"] * rows
|
||||
elif cols_list is None:
|
||||
rows = len(rows_list)
|
||||
cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1
|
||||
layout[CONF_GRID_COLUMNS] = ["LV_GRID_FR(1)"] * cols
|
||||
allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False)
|
||||
rows = len(layout[CONF_GRID_ROWS])
|
||||
columns = len(layout[CONF_GRID_COLUMNS])
|
||||
@@ -379,7 +453,8 @@ def append_layout_schema(schema, config: dict):
|
||||
textwrap.dedent(
|
||||
"""
|
||||
Invalid 'layout' value
|
||||
layout choices are 'horizontal', 'vertical', '<rows>x<cols>',
|
||||
layout choices are 'horizontal', 'vertical',
|
||||
'<rows>x<cols>', '<rows>x', 'x<cols>',
|
||||
or a dictionary with a 'type' key
|
||||
"""
|
||||
),
|
||||
|
||||
@@ -864,6 +864,32 @@ void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_en
|
||||
}
|
||||
#endif // USE_LVGL_SCALE
|
||||
|
||||
#ifdef USE_LVGL_GRADIENT
|
||||
/**
|
||||
*
|
||||
* @param dsc The gradient descriptor containing the color stops
|
||||
* @param pos The current position to calculate the color for
|
||||
* @return The color for the given position
|
||||
*/
|
||||
|
||||
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos) {
|
||||
if (dsc->stops_count == 0)
|
||||
return lv_color_black();
|
||||
if (dsc->stops_count == 1 || pos <= dsc->stops[0].frac)
|
||||
return dsc->stops[0].color;
|
||||
if (pos >= dsc->stops[dsc->stops_count - 1].frac)
|
||||
return dsc->stops[dsc->stops_count - 1].color;
|
||||
int i = 1;
|
||||
while (i < dsc->stops_count && dsc->stops[i].frac < pos)
|
||||
i++;
|
||||
auto *stop1 = &dsc->stops[i - 1];
|
||||
auto *stop2 = &dsc->stops[i];
|
||||
int32_t range = stop2->frac - stop1->frac;
|
||||
int32_t offset = pos - stop1->frac;
|
||||
return lv_color_mix(stop2->color, stop1->color, range == 0 ? 0 : (offset * 255) / range);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
|
||||
LV_TRACE_OBJ_CREATE("begin");
|
||||
LV_UNUSED(class_p);
|
||||
|
||||
@@ -115,6 +115,16 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images
|
||||
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL_GRADIENT
|
||||
/**
|
||||
*
|
||||
* @param dsc The gradient descriptor containing the color stops
|
||||
* @param pos The current position to calculate the color for
|
||||
* @return The color for the given position
|
||||
*/
|
||||
|
||||
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos);
|
||||
#endif
|
||||
// Parent class for things that wrap an LVGL object
|
||||
class LvCompound {
|
||||
public:
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
from collections.abc import Callable
|
||||
import difflib
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import KEY_METADATA
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FROM, CONF_ID, CONF_TO
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj, VariableDeclarationExpression, add_global
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import (
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
VariableDeclarationExpression,
|
||||
add_global,
|
||||
)
|
||||
from esphome.loader import get_component
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
MULTI_CONF = True
|
||||
DOMAIN = "mapping"
|
||||
|
||||
mapping_ns = cg.esphome_ns.namespace("mapping")
|
||||
mapping_class = mapping_ns.class_("Mapping")
|
||||
|
||||
CONF_DEFAULT_VALUE = "default_value"
|
||||
CONF_ENTRIES = "entries"
|
||||
CONF_CLASS = "class"
|
||||
|
||||
@@ -22,11 +31,18 @@ class IndexType:
|
||||
Represents a type of index in a map.
|
||||
"""
|
||||
|
||||
def __init__(self, validator, data_type, conversion):
|
||||
def __init__(
|
||||
self, validator: Callable, data_type: MockObj, conversion: Callable = None
|
||||
) -> None:
|
||||
self.validator = validator
|
||||
self.data_type = data_type
|
||||
self.conversion = conversion
|
||||
|
||||
async def convert_value(self, value):
|
||||
if self.conversion:
|
||||
return self.conversion(value)
|
||||
return await cg.get_variable(value)
|
||||
|
||||
|
||||
INDEX_TYPES = {
|
||||
"int": IndexType(cv.int_, cg.int_, int),
|
||||
@@ -38,6 +54,12 @@ INDEX_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
class MappingMetaData:
|
||||
def __init__(self, from_: IndexType, to_: IndexType) -> None:
|
||||
self.from_ = from_
|
||||
self.to_ = to_
|
||||
|
||||
|
||||
def to_schema(value):
|
||||
"""
|
||||
Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name.
|
||||
@@ -60,7 +82,7 @@ BASE_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def get_object_type(to_):
|
||||
def get_object_type(to_) -> MockObjClass | None:
|
||||
"""
|
||||
Get the object type from a string. Possible formats:
|
||||
xxx The name of a component which defines INSTANCE_TYPE
|
||||
@@ -81,25 +103,60 @@ def get_object_type(to_):
|
||||
return None
|
||||
|
||||
|
||||
def get_all_mapping_metadata() -> dict[str, MappingMetaData]:
|
||||
"""Get all mapping metadata."""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
|
||||
|
||||
|
||||
def get_mapping_metadata(mapping_id: str) -> MappingMetaData:
|
||||
"""Get mapping metadata by ID for use by other components."""
|
||||
return get_all_mapping_metadata()[mapping_id]
|
||||
|
||||
|
||||
def add_metadata(
|
||||
mapping_id: ID,
|
||||
from_: IndexType,
|
||||
to_: IndexType,
|
||||
) -> None:
|
||||
get_all_mapping_metadata()[mapping_id.id] = MappingMetaData(from_, to_)
|
||||
|
||||
|
||||
def map_schema(config):
|
||||
config = BASE_SCHEMA(config)
|
||||
if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict):
|
||||
raise cv.Invalid("an entries list is required for a map")
|
||||
raise cv.Invalid("an entries dictionary is required for a mapping")
|
||||
entries = config[CONF_ENTRIES]
|
||||
if len(entries) == 0:
|
||||
raise cv.Invalid("Map must have at least one entry")
|
||||
raise cv.Invalid("A mapping must have at least one entry")
|
||||
to_ = config[CONF_TO]
|
||||
if to_ in INDEX_TYPES:
|
||||
value_type = INDEX_TYPES[to_].validator
|
||||
value_type = INDEX_TYPES[to_]
|
||||
else:
|
||||
value_type = get_object_type(to_)
|
||||
if value_type is None:
|
||||
object_type = get_object_type(to_)
|
||||
if object_type is None:
|
||||
matches = difflib.get_close_matches(to_, CORE.id_classes)
|
||||
raise cv.Invalid(
|
||||
f"No known mappable class name matches '{to_}'; did you mean one of {', '.join(matches)}?"
|
||||
)
|
||||
value_type = cv.use_id(value_type)
|
||||
config[CONF_ENTRIES] = {k: value_type(v) for k, v in entries.items()}
|
||||
validator = cv.use_id(object_type)
|
||||
value_type = IndexType(validator, object_type)
|
||||
config[CONF_ENTRIES] = {k: value_type.validator(v) for k, v in entries.items()}
|
||||
if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None:
|
||||
config[CONF_DEFAULT_VALUE] = value_type.validator(default_value)
|
||||
unexpected_keys = config.keys() - {
|
||||
CONF_ENTRIES,
|
||||
CONF_TO,
|
||||
CONF_FROM,
|
||||
CONF_ID,
|
||||
CONF_DEFAULT_VALUE,
|
||||
}
|
||||
if unexpected_keys:
|
||||
errors = [
|
||||
cv.Invalid(f"Unexpected key '{k}'", path=[k]) for k in unexpected_keys
|
||||
]
|
||||
raise cv.MultipleInvalid(errors)
|
||||
|
||||
add_metadata(config[CONF_ID], INDEX_TYPES[config[CONF_FROM]], value_type)
|
||||
return config
|
||||
|
||||
|
||||
@@ -107,29 +164,19 @@ CONFIG_SCHEMA = map_schema
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
entries = config[CONF_ENTRIES]
|
||||
from_ = config[CONF_FROM]
|
||||
to_ = config[CONF_TO]
|
||||
index_conversion = INDEX_TYPES[from_].conversion
|
||||
index_type = INDEX_TYPES[from_].data_type
|
||||
if to_ in INDEX_TYPES:
|
||||
value_conversion = INDEX_TYPES[to_].conversion
|
||||
value_type = INDEX_TYPES[to_].data_type
|
||||
entries = {
|
||||
index_conversion(key): value_conversion(value)
|
||||
for key, value in entries.items()
|
||||
}
|
||||
else:
|
||||
entries = {
|
||||
index_conversion(key): await cg.get_variable(value)
|
||||
for key, value in entries.items()
|
||||
}
|
||||
value_type = get_object_type(to_)
|
||||
if list(entries.values())[0].op != ".":
|
||||
value_type = value_type.operator("ptr")
|
||||
varid = config[CONF_ID]
|
||||
metadata = get_mapping_metadata(varid.id)
|
||||
entries = {
|
||||
metadata.from_.conversion(key): await metadata.to_.convert_value(value)
|
||||
for key, value in config[CONF_ENTRIES].items()
|
||||
}
|
||||
value_type = metadata.to_.data_type
|
||||
# entries guaranteed to be non-empty here.
|
||||
value_0 = list(entries.values())[0]
|
||||
if isinstance(value_0, MockObj) and value_0.op != ".":
|
||||
value_type = value_type.operator("ptr")
|
||||
varid.type = mapping_class.template(
|
||||
index_type,
|
||||
metadata.from_.data_type,
|
||||
value_type,
|
||||
)
|
||||
var = MockObj(varid, ".")
|
||||
@@ -139,4 +186,6 @@ async def to_code(config):
|
||||
|
||||
for key, value in entries.items():
|
||||
cg.add(var.set(key, value))
|
||||
if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None:
|
||||
cg.add(var.set_default_value(await metadata.to_.convert_value(default_value)))
|
||||
return var
|
||||
|
||||
@@ -40,6 +40,9 @@ template<typename K, typename V> class Mapping {
|
||||
if (it != this->map_.end()) {
|
||||
return V{it->second};
|
||||
}
|
||||
if (this->default_value_.has_value()) {
|
||||
return this->default_value_.value();
|
||||
}
|
||||
if constexpr (std::is_pointer_v<K>) {
|
||||
esph_log_e(TAG, "Key '%p' not found in mapping", key);
|
||||
} else if constexpr (std::is_same_v<K, std::string>) {
|
||||
@@ -69,11 +72,17 @@ template<typename K, typename V> class Mapping {
|
||||
if (it != this->map_.end()) {
|
||||
return it->second.c_str(); // safe since value remains in map
|
||||
}
|
||||
if (this->default_value_.has_value()) {
|
||||
return this->default_value_.value();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void set_default_value(const V &default_value) { this->default_value_ = default_value; }
|
||||
|
||||
protected:
|
||||
std::map<key_t, value_t, std::less<key_t>, RAMAllocator<std::pair<key_t, value_t>>> map_;
|
||||
std::optional<V> default_value_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::mapping
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, Lambda, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.cpp_generator import LambdaExpression
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -61,6 +62,28 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _require_network_interface(config: ConfigType) -> ConfigType:
|
||||
"""Require a network interface for mDNS on Arduino/LEAmDNS platforms.
|
||||
|
||||
On ESP8266 and RP2040 the C++ implementation needs at least one IP state
|
||||
listener (WiFi on ESP8266; WiFi or Ethernet on RP2040) to arm its polling
|
||||
window. Reject at config time rather than silently producing a component
|
||||
that never initializes.
|
||||
"""
|
||||
if config.get(CONF_DISABLED) or not (CORE.is_esp8266 or CORE.is_rp2040):
|
||||
return config
|
||||
full_config = fv.full_config.get()
|
||||
has_wifi = "wifi" in full_config
|
||||
has_ethernet = CORE.is_rp2040 and "ethernet" in full_config
|
||||
if not (has_wifi or has_ethernet):
|
||||
options = "'wifi'" if CORE.is_esp8266 else "'wifi' or 'ethernet'"
|
||||
raise cv.Invalid(
|
||||
"mdns on this platform requires a network interface — "
|
||||
f"add a {options} component to your configuration."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -74,6 +97,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _require_network_interface
|
||||
|
||||
|
||||
def mdns_txt_record(key: str, value: str) -> cg.RawExpression:
|
||||
"""Create a mDNS TXT record.
|
||||
|
||||
@@ -169,6 +195,19 @@ async def to_code(config):
|
||||
elif CORE.is_rp2040:
|
||||
cg.add_library("LEAmDNS", None)
|
||||
|
||||
# Subscribe to the network IP state listener(s) so MDNS.update() is only
|
||||
# scheduled during the probe+announce phase. Same on_ip_state() override
|
||||
# serves both WiFi and Ethernet (signatures match).
|
||||
if CORE.is_esp8266 or CORE.is_rp2040:
|
||||
if "wifi" in CORE.config:
|
||||
from esphome.components import wifi
|
||||
|
||||
wifi.request_wifi_ip_state_listener()
|
||||
if CORE.is_rp2040 and "ethernet" in CORE.config:
|
||||
from esphome.components import ethernet
|
||||
|
||||
ethernet.request_ethernet_ip_state_listener()
|
||||
|
||||
if CORE.is_esp32:
|
||||
add_idf_component(name="espressif/mdns", ref="1.11.0")
|
||||
|
||||
|
||||
@@ -5,6 +5,22 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
// On ESP8266 and RP2040 the scheduler-backed MDNS.update() polling window is armed by
|
||||
// IP state listener events on whichever network interface is configured.
|
||||
#if (defined(USE_ESP8266) || defined(USE_RP2040)) && \
|
||||
((defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)) || \
|
||||
(defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS)))
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#define USE_MDNS_EVENT_DRIVEN_POLLING
|
||||
#if defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#define USE_MDNS_WIFI_LISTENER
|
||||
#endif
|
||||
#if defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS)
|
||||
#include "esphome/components/ethernet/ethernet_component.h"
|
||||
#define USE_MDNS_ETHERNET_LISTENER
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome::mdns {
|
||||
|
||||
@@ -40,33 +56,40 @@ struct MDNSService {
|
||||
FixedVector<MDNSTXTRecord> txt_records;
|
||||
};
|
||||
|
||||
class MDNSComponent final : public Component {
|
||||
class MDNSComponent final : public Component
|
||||
#ifdef USE_MDNS_WIFI_LISTENER
|
||||
,
|
||||
public wifi::WiFiIPStateListener
|
||||
#endif
|
||||
#ifdef USE_MDNS_ETHERNET_LISTENER
|
||||
,
|
||||
public ethernet::EthernetIPStateListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
// Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040).
|
||||
//
|
||||
// On these platforms, MDNS.update() calls _process(true) which only manages timer-driven
|
||||
// state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS
|
||||
// packets are handled independently via the lwIP onRx UDP callback and are NOT affected
|
||||
// by how often update() is called.
|
||||
//
|
||||
// The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1).
|
||||
// Announcement intervals are 1000ms and cache TTL checks are on the order of seconds
|
||||
// to minutes. A 50ms polling interval provides sufficient resolution for all timers
|
||||
// while completely removing mDNS from the per-iteration loop list.
|
||||
//
|
||||
// In steady state (after the ~8 second boot probe/announce phase completes), update()
|
||||
// checks timers that are set to never expire, making every call pure overhead.
|
||||
//
|
||||
// Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this
|
||||
// interval is safe in production.
|
||||
//
|
||||
// By using set_interval() instead of overriding loop(), the component is excluded from
|
||||
// the main loop list via has_overridden_loop(), eliminating all per-iteration overhead
|
||||
// including virtual dispatch.
|
||||
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
|
||||
// LEAmDNS has meaningful work only during the probe+announce phase (3×250ms probes +
|
||||
// 8×1000ms announces, ~9s). Afterwards every internal timer is resetToNeverExpires()
|
||||
// and update() becomes pure overhead. We arm a bounded polling window from IP state
|
||||
// listener events so update() runs only during that phase.
|
||||
static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50;
|
||||
// Must exceed LEAmDNS's longest restart-to-announce-complete path:
|
||||
// MDNS_PROBE_DELAY (250ms) × MDNS_PROBE_COUNT (3) = 750ms probing
|
||||
// + MDNS_ANNOUNCE_DELAY (1000ms) × MDNS_ANNOUNCE_COUNT (8) = 8000ms announcing
|
||||
// + rand() % MDNS_PROBE_DELAY jitter on first probe (0–250ms)
|
||||
// + debounced schedule_function() hop when statusChangeCB fires on ESP8266
|
||||
// ≈ 9s nominal. 15s gives ~6s margin to absorb main-loop blocking (long
|
||||
// component setup, WiFi scan, flash writes) that could stretch the deadlines
|
||||
// between our polls. If LEAmDNS ever extends its phase (upstream library
|
||||
// update) this constant needs to grow. Constants defined in LEAmDNS_Priv.h
|
||||
// (ESP8266 core 3.1.2 / arduino-pico 5.5.1).
|
||||
static constexpr uint32_t MDNS_POLL_WINDOW_MS = 15000;
|
||||
static constexpr uint32_t MDNS_POLL_ID = 0;
|
||||
static constexpr uint32_t MDNS_POLL_STOP_ID = 1;
|
||||
#endif
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
#ifdef USE_MDNS_EXTRA_SERVICES
|
||||
@@ -87,7 +110,17 @@ class MDNSComponent final : public Component {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
|
||||
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
|
||||
const network::IPAddress &dns2) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
|
||||
/// Arm a fresh MDNS_POLL_WINDOW_MS polling window. Idempotent — re-arming replaces
|
||||
/// the previous window via the scheduler's atomic cancel-and-add on matching IDs.
|
||||
void start_polling_window_();
|
||||
#endif
|
||||
/// Helper to set up services and MAC buffers, then call platform-specific registration
|
||||
using PlatformRegisterFn = void (*)(MDNSComponent *, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &);
|
||||
|
||||
@@ -130,8 +163,8 @@ class MDNSComponent final : public Component {
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
|
||||
#endif
|
||||
#ifdef USE_RP2040
|
||||
bool was_connected_{false};
|
||||
#if defined(USE_RP2040) && defined(USE_MDNS_EVENT_DRIVEN_POLLING)
|
||||
// RP2040 defers MDNS.begin() until the first IP-up event; this tracks that.
|
||||
bool initialized_{false};
|
||||
#endif
|
||||
void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "mdns_component.h"
|
||||
// wifi_component.h is pulled in transitively by mdns_component.h when
|
||||
// USE_MDNS_WIFI_LISTENER is defined.
|
||||
|
||||
namespace esphome::mdns {
|
||||
|
||||
@@ -36,15 +38,36 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
|
||||
void MDNSComponent::start_polling_window_() {
|
||||
// uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add.
|
||||
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
|
||||
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
|
||||
}
|
||||
#endif
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
this->setup_buffers_and_register_(register_esp8266);
|
||||
// Schedule MDNS.update() via set_interval() instead of overriding loop().
|
||||
// This removes the component from the per-iteration loop list entirely,
|
||||
// eliminating virtual dispatch overhead on every main loop cycle.
|
||||
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
|
||||
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
|
||||
#ifdef USE_MDNS_WIFI_LISTENER
|
||||
// LEAmDNS's own LwipIntf::statusChangeCB drives _restart() on netif changes; we just
|
||||
// arm the window around the initial probe/announce and each reconnect. Unconditional
|
||||
// here is safe: setup_priority::AFTER_CONNECTION guarantees the network is up.
|
||||
wifi::global_wifi_component->add_ip_state_listener(this);
|
||||
this->start_polling_window_();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_MDNS_WIFI_LISTENER
|
||||
void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &,
|
||||
const network::IPAddress &) {
|
||||
// IP listener only fires on acquisition (not loss), so any notification is a fresh
|
||||
// IP worth re-arming for. start_polling_window_() is idempotent.
|
||||
if (ips[0].is_set()) {
|
||||
this->start_polling_window_();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void MDNSComponent::on_shutdown() {
|
||||
MDNS.close();
|
||||
delay(10);
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "mdns_component.h"
|
||||
// wifi_component.h / ethernet_component.h are pulled in transitively by
|
||||
// mdns_component.h when their respective listener defines are active.
|
||||
|
||||
// Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty.
|
||||
// Save and restore our definition around the include to avoid a redefinition warning.
|
||||
#pragma push_macro("IRAM_ATTR")
|
||||
#undef IRAM_ATTR
|
||||
#include <ESP8266mDNS.h>
|
||||
@@ -20,10 +21,7 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
|
||||
MDNS.begin(App.get_name().c_str());
|
||||
|
||||
for (const auto &service : services) {
|
||||
// Strip the leading underscore from the proto and service_type. While it is
|
||||
// part of the wire protocol to have an underscore, and for example ESP-IDF
|
||||
// expects the underscore to be there, the ESP8266 implementation always adds
|
||||
// the underscore itself.
|
||||
// ESP8266mDNS always adds the leading underscore itself, so strip it here.
|
||||
auto *proto = MDNS_STR_ARG(service.proto);
|
||||
while (*proto == '_') {
|
||||
proto++;
|
||||
@@ -40,34 +38,58 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
|
||||
}
|
||||
}
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
// RP2040's LEAmDNS library registers a LwipIntf::stateUpCB() callback to restart
|
||||
// mDNS when the network interface reconnects. However, stateUpCB() is stubbed out
|
||||
// in arduino-pico's LwipIntfCB.cpp because the original ESP8266 implementation used
|
||||
// schedule_function() which doesn't exist in arduino-pico, and the callback can't
|
||||
// safely run directly since netif status callbacks fire from IRQ context
|
||||
// (PICO_CYW43_ARCH_THREADSAFE_BACKGROUND) while _restart() allocates UDP sockets.
|
||||
//
|
||||
// Workaround: defer MDNS.begin() and service registration until the network is
|
||||
// connected (has an IP), then call notifyAPChange() on subsequent reconnects to
|
||||
// restart mDNS probing and announcing — all from main loop context so it's
|
||||
// thread-safe.
|
||||
this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() {
|
||||
bool connected = network::is_connected();
|
||||
if (connected && !this->was_connected_) {
|
||||
if (!this->initialized_) {
|
||||
this->setup_buffers_and_register_(register_rp2040);
|
||||
this->initialized_ = true;
|
||||
} else {
|
||||
MDNS.notifyAPChange();
|
||||
}
|
||||
}
|
||||
this->was_connected_ = connected;
|
||||
if (this->initialized_) {
|
||||
MDNS.update();
|
||||
}
|
||||
});
|
||||
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
|
||||
void MDNSComponent::start_polling_window_() {
|
||||
// uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add.
|
||||
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
|
||||
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
|
||||
}
|
||||
#endif
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
|
||||
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
|
||||
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
|
||||
// one on_ip_state() override serves both.
|
||||
#ifdef USE_MDNS_WIFI_LISTENER
|
||||
wifi::global_wifi_component->add_ip_state_listener(this);
|
||||
// AFTER_CONNECTION priority means the network may already be up; the listener only
|
||||
// fires on subsequent changes, so seed the current state.
|
||||
{
|
||||
const auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses();
|
||||
if (ips[0].is_set()) {
|
||||
this->on_ip_state(ips, wifi::global_wifi_component->get_dns_address(0),
|
||||
wifi::global_wifi_component->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MDNS_ETHERNET_LISTENER
|
||||
ethernet::global_eth_component->add_ip_state_listener(this);
|
||||
if (ethernet::global_eth_component->is_connected()) {
|
||||
const auto ips = ethernet::global_eth_component->get_ip_addresses();
|
||||
if (ips[0].is_set()) {
|
||||
this->on_ip_state(ips, network::IPAddress{}, network::IPAddress{});
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
|
||||
void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &,
|
||||
const network::IPAddress &) {
|
||||
// Listener only fires on IP acquisition (not loss); every event is a fresh IP.
|
||||
if (!ips[0].is_set()) {
|
||||
return;
|
||||
}
|
||||
if (!this->initialized_) {
|
||||
this->setup_buffers_and_register_(register_rp2040);
|
||||
this->initialized_ = true;
|
||||
} else {
|
||||
MDNS.notifyAPChange();
|
||||
}
|
||||
this->start_polling_window_();
|
||||
}
|
||||
#endif
|
||||
|
||||
void MDNSComponent::on_shutdown() {
|
||||
MDNS.close();
|
||||
|
||||
@@ -3,11 +3,8 @@ import binascii
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import modbus
|
||||
from esphome.components.const import CONF_ENABLED
|
||||
from esphome.components.modbus.helpers import (
|
||||
CPP_TYPE_REGISTER_MAP,
|
||||
MODBUS_REGISTER_TYPE,
|
||||
SENSOR_VALUE_TYPE,
|
||||
TYPE_REGISTER_MAP,
|
||||
ModbusRegisterType,
|
||||
)
|
||||
@@ -29,11 +26,10 @@ from .const import (
|
||||
CONF_ON_OFFLINE,
|
||||
CONF_ON_ONLINE,
|
||||
CONF_REGISTER_COUNT,
|
||||
CONF_REGISTER_LAST_ADDRESS,
|
||||
CONF_REGISTER_TYPE,
|
||||
CONF_REGISTER_VALUE,
|
||||
CONF_RESPONSE_SIZE,
|
||||
CONF_SERVER_COURTESY_RESPONSE,
|
||||
CONF_SERVER_REGISTERS,
|
||||
CONF_SKIP_UPDATES,
|
||||
CONF_VALUE_TYPE,
|
||||
)
|
||||
@@ -42,9 +38,6 @@ CODEOWNERS = ["@martgras"]
|
||||
|
||||
AUTO_LOAD = ["modbus"]
|
||||
|
||||
CONF_READ_LAMBDA = "read_lambda"
|
||||
CONF_WRITE_LAMBDA = "write_lambda"
|
||||
CONF_SERVER_REGISTERS = "server_registers"
|
||||
MULTI_CONF = True
|
||||
|
||||
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
|
||||
@@ -53,30 +46,9 @@ ModbusController = modbus_controller_ns.class_(
|
||||
)
|
||||
|
||||
SensorItem = modbus_controller_ns.struct("SensorItem")
|
||||
ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse")
|
||||
ServerRegister = modbus_controller_ns.struct("ServerRegister")
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ENABLED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
|
||||
}
|
||||
)
|
||||
|
||||
ModbusServerRegisterSchema = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ServerRegister),
|
||||
cv.Required(CONF_ADDRESS): cv.positive_int,
|
||||
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
|
||||
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -85,12 +57,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_COMMAND_THROTTLE, default="0ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_SERVER_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
|
||||
cv.Optional(CONF_SERVER_COURTESY_RESPONSE): cv.invalid(
|
||||
"This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/"
|
||||
),
|
||||
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
|
||||
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
|
||||
cv.Optional(
|
||||
CONF_SERVER_REGISTERS,
|
||||
): cv.ensure_list(ModbusServerRegisterSchema),
|
||||
): cv.invalid(
|
||||
"This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/"
|
||||
),
|
||||
cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_ONLINE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_OFFLINE): automation.validate_automation({}),
|
||||
@@ -142,11 +118,9 @@ def validate_modbus_register(config):
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
if CONF_SERVER_COURTESY_RESPONSE in config or CONF_SERVER_REGISTERS in config:
|
||||
return modbus.final_validate_modbus_device("modbus_controller", role="server")(
|
||||
config
|
||||
)
|
||||
return config
|
||||
return modbus.final_validate_modbus_device("modbus_controller", role="client")(
|
||||
config
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
@@ -228,53 +202,8 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
|
||||
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
|
||||
if server_courtesy_response := config.get(CONF_SERVER_COURTESY_RESPONSE):
|
||||
cg.add(
|
||||
var.set_server_courtesy_response(
|
||||
cg.StructInitializer(
|
||||
ServerCourtesyResponse,
|
||||
("enabled", server_courtesy_response[CONF_ENABLED]),
|
||||
(
|
||||
"register_last_address",
|
||||
server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
|
||||
),
|
||||
("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
|
||||
)
|
||||
)
|
||||
)
|
||||
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
|
||||
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
|
||||
if CONF_SERVER_REGISTERS in config:
|
||||
for server_register in config[CONF_SERVER_REGISTERS]:
|
||||
server_register_var = cg.new_Pvariable(
|
||||
server_register[CONF_ID],
|
||||
server_register[CONF_ADDRESS],
|
||||
server_register[CONF_VALUE_TYPE],
|
||||
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
|
||||
)
|
||||
cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]]
|
||||
cg.add(
|
||||
server_register_var.set_read_lambda(
|
||||
cg.TemplateArguments(cpp_type),
|
||||
await cg.process_lambda(
|
||||
server_register[CONF_READ_LAMBDA],
|
||||
[(cg.uint16, "address")],
|
||||
return_type=cpp_type,
|
||||
),
|
||||
)
|
||||
)
|
||||
if CONF_WRITE_LAMBDA in server_register:
|
||||
cg.add(
|
||||
server_register_var.set_write_lambda(
|
||||
cg.TemplateArguments(cpp_type),
|
||||
await cg.process_lambda(
|
||||
server_register[CONF_WRITE_LAMBDA],
|
||||
parameters=[(cg.uint16, "address"), (cpp_type, "x")],
|
||||
return_type=cg.bool_,
|
||||
),
|
||||
)
|
||||
)
|
||||
cg.add(var.add_server_register(server_register_var))
|
||||
await register_modbus_device(var, config)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ CONF_REGISTER_TYPE = "register_type"
|
||||
CONF_REGISTER_VALUE = "register_value"
|
||||
CONF_RESPONSE_SIZE = "response_size"
|
||||
CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response"
|
||||
CONF_SERVER_REGISTERS = "server_registers"
|
||||
CONF_SKIP_UPDATES = "skip_updates"
|
||||
CONF_USE_WRITE_MULTIPLE = "use_write_multiple"
|
||||
CONF_VALUE_TYPE = "value_type"
|
||||
|
||||
@@ -112,167 +112,6 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_
|
||||
}
|
||||
}
|
||||
|
||||
void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
|
||||
uint16_t number_of_registers) {
|
||||
ESP_LOGD(TAG,
|
||||
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
|
||||
"0x%X.",
|
||||
this->address_, function_code, start_address, number_of_registers);
|
||||
|
||||
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
|
||||
ESP_LOGW(TAG, "Invalid number of registers %d. 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_) {
|
||||
if (server_register->address == current_address) {
|
||||
if (!server_register->read_lambda) {
|
||||
break;
|
||||
}
|
||||
int64_t value = server_register->read_lambda();
|
||||
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
|
||||
server_register->address, static_cast<size_t>(server_register->value_type),
|
||||
server_register->register_count, server_register->format_value(value).c_str());
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
if (this->server_courtesy_response_.enabled &&
|
||||
(current_address <= this->server_courtesy_response_.register_last_address)) {
|
||||
ESP_LOGD(TAG,
|
||||
"Could not match any register to address 0x%02X, but default allowed. "
|
||||
"Returning default value: %d.",
|
||||
current_address, 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);
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> response;
|
||||
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, start_address, number_of_registers, response.size(), response.data());
|
||||
}
|
||||
|
||||
void ModbusController::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) {
|
||||
uint16_t number_of_registers;
|
||||
uint16_t payload_offset;
|
||||
|
||||
if (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 %d. 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 %d bytes is not 2 times the number of registers (%d). 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 (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, offset);
|
||||
current_address += server_register->register_count;
|
||||
offset += server_register->register_count * sizeof(uint16_t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// check all registers are writable before writing to any of them:
|
||||
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
|
||||
return server_register->write_lambda != nullptr;
|
||||
})) {
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
|
||||
return;
|
||||
}
|
||||
|
||||
// Actually write to the registers:
|
||||
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
|
||||
int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
|
||||
return server_register->write_lambda(number);
|
||||
})) {
|
||||
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
|
||||
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);
|
||||
}
|
||||
|
||||
SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
|
||||
auto reg_it = std::find_if(
|
||||
std::begin(this->register_ranges_), std::end(this->register_ranges_),
|
||||
@@ -472,14 +311,8 @@ void ModbusController::dump_config() {
|
||||
"ModbusController:\n"
|
||||
" Address: 0x%02X\n"
|
||||
" Max Command Retries: %d\n"
|
||||
" Offline Skip Updates: %d\n"
|
||||
" Server Courtesy Response:\n"
|
||||
" Enabled: %s\n"
|
||||
" Register Last Address: 0x%02X\n"
|
||||
" Register Value: %d",
|
||||
this->address_, this->max_cmd_retries_, this->offline_skip_updates_,
|
||||
this->server_courtesy_response_.enabled ? "true" : "false",
|
||||
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
|
||||
" Offline Skip Updates: %d\n",
|
||||
this->address_, this->max_cmd_retries_, this->offline_skip_updates_);
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGCONFIG(TAG, "sensormap");
|
||||
@@ -493,11 +326,6 @@ void ModbusController::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
|
||||
it.start_address, it.register_count, it.skip_updates);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "server registers");
|
||||
for (auto &r : this->server_registers_) {
|
||||
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
|
||||
static_cast<uint8_t>(r->value_type), r->register_count);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -120,82 +120,6 @@ class SensorItem {
|
||||
bool force_new_range{false};
|
||||
};
|
||||
|
||||
struct ServerCourtesyResponse {
|
||||
bool enabled{false};
|
||||
uint16_t register_last_address{0xFFFF};
|
||||
uint16_t register_value{0};
|
||||
};
|
||||
|
||||
class ServerRegister {
|
||||
using ReadLambda = std::function<int64_t()>;
|
||||
using WriteLambda = std::function<bool(int64_t value)>;
|
||||
|
||||
public:
|
||||
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
|
||||
this->address = address;
|
||||
this->value_type = value_type;
|
||||
this->register_count = register_count;
|
||||
}
|
||||
|
||||
template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
|
||||
this->read_lambda = [this, user_read_lambda]() -> int64_t {
|
||||
T user_value = user_read_lambda(this->address);
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
return bit_cast<uint32_t>(user_value);
|
||||
} else {
|
||||
return static_cast<int64_t>(user_value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
|
||||
this->write_lambda = [this, user_write_lambda](int64_t number) {
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
float float_value = bit_cast<float>(static_cast<uint32_t>(number));
|
||||
return user_write_lambda(this->address, float_value);
|
||||
}
|
||||
return user_write_lambda(this->address, static_cast<T>(number));
|
||||
};
|
||||
}
|
||||
|
||||
// Formats a raw value into a string representation based on the value type for debugging
|
||||
std::string format_value(int64_t value) const {
|
||||
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
|
||||
// plus null terminator = 43, rounded to 44 for 4-byte alignment
|
||||
char buf[44];
|
||||
switch (this->value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::U_QWORD_R:
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
|
||||
return buf;
|
||||
case SensorValueType::S_WORD:
|
||||
case SensorValueType::S_DWORD:
|
||||
case SensorValueType::S_DWORD_R:
|
||||
case SensorValueType::S_QWORD:
|
||||
case SensorValueType::S_QWORD_R:
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
case SensorValueType::FP32_R:
|
||||
case SensorValueType::FP32:
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
||||
return buf;
|
||||
default:
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t address{0};
|
||||
SensorValueType value_type{SensorValueType::RAW};
|
||||
uint8_t register_count{0};
|
||||
ReadLambda read_lambda;
|
||||
WriteLambda write_lambda;
|
||||
};
|
||||
|
||||
// ModbusController::create_register_ranges_ tries to optimize register range
|
||||
// for this the sensors must be ordered by register_type, start_address and bitmask
|
||||
class SensorItemsComparator {
|
||||
@@ -367,16 +291,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
||||
void queue_command(const ModbusCommandItem &command);
|
||||
/// Registers a sensor with the controller. Called by esphomes code generator
|
||||
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
|
||||
/// 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 response was parsed without errors
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
/// called when a modbus error response was received
|
||||
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
|
||||
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
|
||||
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
|
||||
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
|
||||
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
|
||||
/// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
|
||||
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
|
||||
/// default delegate called by process_modbus_data when a response for a write response has retrieved from the
|
||||
@@ -413,12 +331,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
||||
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
|
||||
/// get how many times a command will be (re)sent if no response is received
|
||||
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
|
||||
/// 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;
|
||||
}
|
||||
/// Get the server courtesy response object
|
||||
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
|
||||
|
||||
protected:
|
||||
/// parse sensormap_ and create range of sequential addresses
|
||||
@@ -435,8 +347,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
||||
void dump_sensors_();
|
||||
/// Collection of all sensors for this component
|
||||
SensorSet sensorset_;
|
||||
/// Collection of all server registers for this component
|
||||
std::vector<ServerRegister *> server_registers_{};
|
||||
/// Continuous range of modbus registers
|
||||
std::vector<RegisterRange> register_ranges_{};
|
||||
/// Hold the pending requests to be sent
|
||||
@@ -461,9 +371,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
||||
CallbackManager<void(int, int)> online_callback_{};
|
||||
/// Server offline callback
|
||||
CallbackManager<void(int, int)> offline_callback_{};
|
||||
/// Server courtesy response
|
||||
ServerCourtesyResponse server_courtesy_response_{
|
||||
.enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
|
||||
};
|
||||
|
||||
/** Convert vector<uint8_t> response payload to float.
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import modbus
|
||||
from esphome.components.const import CONF_ENABLED
|
||||
from esphome.components.modbus.helpers import (
|
||||
CPP_TYPE_REGISTER_MAP,
|
||||
SENSOR_VALUE_TYPE,
|
||||
TYPE_REGISTER_MAP,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||
|
||||
from .const import (
|
||||
CONF_COURTESY_RESPONSE,
|
||||
CONF_READ_LAMBDA,
|
||||
CONF_REGISTER_LAST_ADDRESS,
|
||||
CONF_REGISTER_VALUE,
|
||||
CONF_REGISTERS,
|
||||
CONF_VALUE_TYPE,
|
||||
CONF_WRITE_LAMBDA,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@exciton"]
|
||||
|
||||
AUTO_LOAD = ["modbus"]
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
modbus_server_ns = cg.esphome_ns.namespace("modbus_server")
|
||||
ModbusServer = modbus_server_ns.class_(
|
||||
"ModbusServer", cg.Component, modbus.ModbusDevice
|
||||
)
|
||||
|
||||
ServerCourtesyResponse = modbus_server_ns.struct("ServerCourtesyResponse")
|
||||
ServerRegister = modbus_server_ns.struct("ServerRegister")
|
||||
|
||||
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ENABLED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
|
||||
}
|
||||
)
|
||||
|
||||
ModbusServerRegisterSchema = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ServerRegister),
|
||||
cv.Required(CONF_ADDRESS): cv.positive_int,
|
||||
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
|
||||
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ModbusServer),
|
||||
cv.Optional(CONF_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
|
||||
cv.Optional(
|
||||
CONF_REGISTERS,
|
||||
): cv.ensure_list(ModbusServerRegisterSchema),
|
||||
}
|
||||
).extend(modbus.modbus_device_schema(0x01)),
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
return modbus.final_validate_modbus_device("modbus_server", role="server")(config)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
if server_courtesy_response := config.get(CONF_COURTESY_RESPONSE):
|
||||
cg.add(
|
||||
var.set_server_courtesy_response(
|
||||
cg.StructInitializer(
|
||||
ServerCourtesyResponse,
|
||||
("enabled", server_courtesy_response[CONF_ENABLED]),
|
||||
(
|
||||
"register_last_address",
|
||||
server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
|
||||
),
|
||||
("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
|
||||
)
|
||||
)
|
||||
)
|
||||
if CONF_REGISTERS in config:
|
||||
for server_register in config[CONF_REGISTERS]:
|
||||
server_register_var = cg.new_Pvariable(
|
||||
server_register[CONF_ID],
|
||||
server_register[CONF_ADDRESS],
|
||||
server_register[CONF_VALUE_TYPE],
|
||||
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
|
||||
)
|
||||
cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]]
|
||||
cg.add(
|
||||
server_register_var.set_read_lambda(
|
||||
cg.TemplateArguments(cpp_type),
|
||||
await cg.process_lambda(
|
||||
server_register[CONF_READ_LAMBDA],
|
||||
[(cg.uint16, "address")],
|
||||
return_type=cpp_type,
|
||||
),
|
||||
)
|
||||
)
|
||||
if CONF_WRITE_LAMBDA in server_register:
|
||||
cg.add(
|
||||
server_register_var.set_write_lambda(
|
||||
cg.TemplateArguments(cpp_type),
|
||||
await cg.process_lambda(
|
||||
server_register[CONF_WRITE_LAMBDA],
|
||||
parameters=[(cg.uint16, "address"), (cpp_type, "x")],
|
||||
return_type=cg.bool_,
|
||||
),
|
||||
)
|
||||
)
|
||||
cg.add(var.add_server_register(server_register_var))
|
||||
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||
await cg.register_component(var, config)
|
||||
return await modbus.register_modbus_device(var, config)
|
||||
@@ -0,0 +1,7 @@
|
||||
CONF_REGISTER_LAST_ADDRESS = "register_last_address"
|
||||
CONF_REGISTER_VALUE = "register_value"
|
||||
CONF_VALUE_TYPE = "value_type"
|
||||
CONF_COURTESY_RESPONSE = "courtesy_response"
|
||||
CONF_READ_LAMBDA = "read_lambda"
|
||||
CONF_WRITE_LAMBDA = "write_lambda"
|
||||
CONF_REGISTERS = "registers"
|
||||
@@ -0,0 +1,192 @@
|
||||
#include "modbus_server.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::modbus_server {
|
||||
using modbus::ModbusFunctionCode;
|
||||
using modbus::ModbusExceptionCode;
|
||||
|
||||
static const char *const TAG = "modbus_server";
|
||||
|
||||
void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
|
||||
uint16_t number_of_registers) {
|
||||
ESP_LOGD(TAG,
|
||||
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
|
||||
"0x%X.",
|
||||
this->address_, function_code, start_address, number_of_registers);
|
||||
|
||||
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
|
||||
ESP_LOGW(TAG, "Invalid number of registers %d. 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_) {
|
||||
if (server_register->address == current_address) {
|
||||
if (!server_register->read_lambda) {
|
||||
break;
|
||||
}
|
||||
int64_t value = server_register->read_lambda();
|
||||
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
|
||||
server_register->address, static_cast<size_t>(server_register->value_type),
|
||||
server_register->register_count, server_register->format_value(value).c_str());
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
if (this->server_courtesy_response_.enabled &&
|
||||
(current_address <= this->server_courtesy_response_.register_last_address)) {
|
||||
ESP_LOGD(TAG,
|
||||
"Could not match any register to address 0x%02X, but default allowed. "
|
||||
"Returning default value: %d.",
|
||||
current_address, 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);
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> response;
|
||||
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, start_address, number_of_registers, response.size(), response.data());
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (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 %d. 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 %d bytes is not 2 times the number of registers (%d). 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 (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, offset);
|
||||
current_address += server_register->register_count;
|
||||
offset += server_register->register_count * sizeof(uint16_t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// check all registers are writable before writing to any of them:
|
||||
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
|
||||
return server_register->write_lambda != nullptr;
|
||||
})) {
|
||||
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
|
||||
return;
|
||||
}
|
||||
|
||||
// Actually write to the registers:
|
||||
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
|
||||
int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
|
||||
return server_register->write_lambda(number);
|
||||
})) {
|
||||
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
|
||||
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() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ModbusServer:\n"
|
||||
" Address: 0x%02X\n"
|
||||
" Server Courtesy Response:\n"
|
||||
" Enabled: %s\n"
|
||||
" Register Last Address: 0x%02X\n"
|
||||
" Register Value: %" PRIu16,
|
||||
this->address_, this->server_courtesy_response_.enabled ? "true" : "false",
|
||||
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGCONFIG(TAG, "server registers");
|
||||
for (auto &r : this->server_registers_) {
|
||||
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
|
||||
static_cast<uint8_t>(r->value_type), r->register_count);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus_server
|
||||
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
#include "esphome/components/modbus/modbus_helpers.h"
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::modbus_server {
|
||||
|
||||
using modbus::helpers::SensorValueType;
|
||||
|
||||
struct ServerCourtesyResponse {
|
||||
bool enabled{false};
|
||||
uint16_t register_last_address{0xFFFF};
|
||||
uint16_t register_value{0};
|
||||
};
|
||||
|
||||
class ServerRegister {
|
||||
using ReadLambda = std::function<int64_t()>;
|
||||
using WriteLambda = std::function<bool(int64_t value)>;
|
||||
|
||||
public:
|
||||
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
|
||||
this->address = address;
|
||||
this->value_type = value_type;
|
||||
this->register_count = register_count;
|
||||
}
|
||||
|
||||
template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
|
||||
this->read_lambda = [this, user_read_lambda]() -> int64_t {
|
||||
T user_value = user_read_lambda(this->address);
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
return bit_cast<uint32_t>(user_value);
|
||||
} else {
|
||||
return static_cast<int64_t>(user_value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
|
||||
this->write_lambda = [this, user_write_lambda](int64_t number) {
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
float float_value = bit_cast<float>(static_cast<uint32_t>(number));
|
||||
return user_write_lambda(this->address, float_value);
|
||||
}
|
||||
return user_write_lambda(this->address, static_cast<T>(number));
|
||||
};
|
||||
}
|
||||
|
||||
// Formats a raw value into a string representation based on the value type for debugging
|
||||
std::string format_value(int64_t value) const {
|
||||
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
|
||||
// plus null terminator = 43, rounded to 44 for 4-byte alignment
|
||||
char buf[44];
|
||||
switch (this->value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::U_QWORD_R:
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
|
||||
return buf;
|
||||
case SensorValueType::S_WORD:
|
||||
case SensorValueType::S_DWORD:
|
||||
case SensorValueType::S_DWORD_R:
|
||||
case SensorValueType::S_QWORD:
|
||||
case SensorValueType::S_QWORD_R:
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
case SensorValueType::FP32_R:
|
||||
case SensorValueType::FP32:
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
||||
return buf;
|
||||
default:
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t address{0};
|
||||
SensorValueType value_type{SensorValueType::RAW};
|
||||
uint8_t register_count{0};
|
||||
ReadLambda read_lambda;
|
||||
WriteLambda write_lambda;
|
||||
};
|
||||
|
||||
class ModbusServer : public Component, public modbus::ModbusDevice {
|
||||
public:
|
||||
void dump_config() override;
|
||||
|
||||
/// Not used for ModbusServer.
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override{};
|
||||
/// Registers a server register with the controller. Called by esphomes code generator
|
||||
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
|
||||
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
|
||||
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
|
||||
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;
|
||||
}
|
||||
/// Get the server courtesy response object
|
||||
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
|
||||
|
||||
protected:
|
||||
/// Collection of all server registers for this component
|
||||
std::vector<ServerRegister *> server_registers_{};
|
||||
/// Server courtesy response
|
||||
ServerCourtesyResponse server_courtesy_response_{
|
||||
.enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
|
||||
};
|
||||
|
||||
} // namespace esphome::modbus_server
|
||||
@@ -16,6 +16,13 @@ namespace esphome::nextion {
|
||||
static const char *const TAG = "nextion.upload.arduino";
|
||||
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
|
||||
|
||||
// Timeout for display acknowledgment during TFT upload (ms).
|
||||
// A single value is used for all chunks; the happy path returns as soon as
|
||||
// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
|
||||
// reports showed the previous 500ms steady-state value was too tight for
|
||||
// some firmware variants.
|
||||
static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
|
||||
|
||||
// Followed guide
|
||||
// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
|
||||
|
||||
@@ -80,14 +87,14 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) {
|
||||
recv_string.clear();
|
||||
this->write_array(buffer, buffer_size);
|
||||
App.feed_wdt();
|
||||
this->recv_ret_string_(recv_string, this->upload_first_chunk_sent_ ? 500 : 5000, true);
|
||||
this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
|
||||
this->content_length_ -= read_len;
|
||||
const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
|
||||
ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_,
|
||||
EspClass::getFreeHeap());
|
||||
this->upload_first_chunk_sent_ = true;
|
||||
if (recv_string.empty()) {
|
||||
ESP_LOGW(TAG, "No response from display during upload");
|
||||
ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
|
||||
allocator.deallocate(buffer, 4096);
|
||||
buffer = nullptr;
|
||||
return -1;
|
||||
@@ -324,7 +331,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
WiFiClient *Nextion::get_wifi_client_() {
|
||||
if (this->tft_url_.compare(0, 6, "https:") == 0) {
|
||||
if (this->tft_url_.starts_with("https:")) {
|
||||
if (this->wifi_client_secure_ == nullptr) {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
||||
this->wifi_client_secure_ = new BearSSL::WiFiClientSecure();
|
||||
|
||||
@@ -19,6 +19,13 @@ namespace esphome::nextion {
|
||||
static const char *const TAG = "nextion.upload.esp32";
|
||||
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
|
||||
|
||||
// Timeout for display acknowledgment during TFT upload (ms).
|
||||
// A single value is used for all chunks; the happy path returns as soon as
|
||||
// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
|
||||
// reports showed the previous 500ms steady-state value was too tight for
|
||||
// some firmware variants.
|
||||
static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
|
||||
|
||||
// Followed guide
|
||||
// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
|
||||
|
||||
@@ -96,7 +103,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
|
||||
recv_string.clear();
|
||||
this->write_array(buffer, buffer_size);
|
||||
App.feed_wdt();
|
||||
this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true);
|
||||
this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
|
||||
this->content_length_ -= read_len;
|
||||
const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
|
||||
#ifdef USE_PSRAM
|
||||
@@ -109,7 +116,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
|
||||
#endif
|
||||
upload_first_chunk_sent_ = true;
|
||||
if (recv_string.empty()) {
|
||||
ESP_LOGW(TAG, "No response from display during upload");
|
||||
ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
|
||||
allocator.deallocate(buffer, 4096);
|
||||
buffer = nullptr;
|
||||
return -1;
|
||||
|
||||
@@ -141,6 +141,22 @@ CONF_UICR_ERASE = "uicr_erase"
|
||||
VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3]
|
||||
|
||||
|
||||
_DFU_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate),
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _dfu_schema(value: bool | ConfigType) -> ConfigType:
|
||||
if isinstance(value, bool):
|
||||
if not value:
|
||||
raise cv.Invalid("Use 'dfu: true' or specify a configuration dict")
|
||||
return _DFU_SCHEMA({})
|
||||
return _DFU_SCHEMA(value)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
_detect_bootloader,
|
||||
set_core_data,
|
||||
@@ -150,12 +166,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
|
||||
),
|
||||
cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True),
|
||||
cv.Optional(CONF_DFU): cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate),
|
||||
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_DFU): _dfu_schema,
|
||||
cv.Optional(CONF_DCDC, default=True): cv.boolean,
|
||||
cv.Optional(CONF_REG0): cv.Schema(
|
||||
{
|
||||
@@ -321,8 +332,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
async def _dfu_to_code(dfu_config):
|
||||
cg.add_define("USE_NRF52_DFU")
|
||||
var = cg.new_Pvariable(dfu_config[CONF_ID])
|
||||
pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN])
|
||||
cg.add(var.set_reset_pin(pin))
|
||||
if CONF_RESET_PIN in dfu_config:
|
||||
pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN])
|
||||
cg.add(var.set_reset_pin(pin))
|
||||
zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True)
|
||||
await cg.register_component(var, dfu_config)
|
||||
|
||||
|
||||
@@ -2,24 +2,34 @@
|
||||
|
||||
#ifdef USE_NRF52_DFU
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/zephyr/cdc_acm.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace nrf52 {
|
||||
#include <hal/nrf_power.h>
|
||||
|
||||
namespace esphome::nrf52 {
|
||||
|
||||
static const char *const TAG = "dfu";
|
||||
|
||||
static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS
|
||||
static const uint8_t DFU_MAGIC_UF2_RESET = 0x57; // Adafruit nRF52 bootloader UF2 magic
|
||||
|
||||
void DeviceFirmwareUpdate::setup() {
|
||||
this->reset_pin_->setup();
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
}
|
||||
#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT)
|
||||
zephyr::global_cdc_acm->add_on_rate_callback([this](const device *, uint32_t rate) {
|
||||
if (rate == 1200) {
|
||||
volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C;
|
||||
(*dbl_reset_mem) = DFU_DBL_RESET_MAGIC;
|
||||
this->reset_pin_->digital_write(true);
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(true);
|
||||
} else {
|
||||
NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET;
|
||||
App.reboot();
|
||||
}
|
||||
}
|
||||
});
|
||||
#endif
|
||||
@@ -27,10 +37,13 @@ void DeviceFirmwareUpdate::setup() {
|
||||
|
||||
void DeviceFirmwareUpdate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DFU:");
|
||||
LOG_PIN(" RESET Pin: ", this->reset_pin_);
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
LOG_PIN(" RESET Pin: ", this->reset_pin_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Method: GPREGRET");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace nrf52
|
||||
} // namespace esphome
|
||||
} // namespace esphome::nrf52
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace nrf52 {
|
||||
namespace esphome::nrf52 {
|
||||
class DeviceFirmwareUpdate : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -14,10 +13,9 @@ class DeviceFirmwareUpdate : public Component {
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
GPIOPin *reset_pin_;
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace nrf52
|
||||
} // namespace esphome
|
||||
} // namespace esphome::nrf52
|
||||
|
||||
#endif
|
||||
|
||||
@@ -28,7 +28,7 @@ bool OnlineImage::validate_url_(const std::string &url) {
|
||||
ESP_LOGE(TAG, "URL is too long");
|
||||
return false;
|
||||
}
|
||||
if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) {
|
||||
if (!url.starts_with("http://") && !url.starts_with("https://")) {
|
||||
ESP_LOGE(TAG, "URL must start with http:// or https://");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -54,10 +54,16 @@ async def setup_output_platform_(obj, config):
|
||||
power_supply_ = await cg.get_variable(config[CONF_POWER_SUPPLY])
|
||||
cg.add(obj.set_power_supply(power_supply_))
|
||||
if CONF_MAX_POWER in config:
|
||||
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
|
||||
cg.add(obj.set_max_power(config[CONF_MAX_POWER]))
|
||||
if CONF_MIN_POWER in config:
|
||||
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
|
||||
cg.add(obj.set_min_power(config[CONF_MIN_POWER]))
|
||||
if CONF_ZERO_MEANS_ZERO in config:
|
||||
# Only emit when zero_means_zero is actually enabled. The schema defaults to False
|
||||
# so this key is always present; emitting unconditionally would force
|
||||
# USE_OUTPUT_FLOAT_POWER_SCALING on for every output, defeating the gate.
|
||||
if config.get(CONF_ZERO_MEANS_ZERO):
|
||||
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
|
||||
cg.add(obj.set_zero_means_zero(config[CONF_ZERO_MEANS_ZERO]))
|
||||
|
||||
|
||||
@@ -121,6 +127,7 @@ async def output_set_level_to_code(config, action_id, template_arg, args):
|
||||
synchronous=True,
|
||||
)
|
||||
async def output_set_min_power_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
template_ = await cg.templatable(config[CONF_MIN_POWER], args, cg.float_)
|
||||
@@ -140,6 +147,7 @@ async def output_set_min_power_to_code(config, action_id, template_arg, args):
|
||||
synchronous=True,
|
||||
)
|
||||
async def output_set_max_power_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
template_ = await cg.templatable(config[CONF_MAX_POWER], args, cg.float_)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/components/output/binary_output.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
|
||||
@@ -40,6 +41,7 @@ template<typename... Ts> class SetLevelAction : public Action<Ts...> {
|
||||
FloatOutput *output_;
|
||||
};
|
||||
|
||||
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
template<typename... Ts> class SetMinPowerAction : public Action<Ts...> {
|
||||
public:
|
||||
SetMinPowerAction(FloatOutput *output) : output_(output) {}
|
||||
@@ -63,6 +65,7 @@ template<typename... Ts> class SetMaxPowerAction : public Action<Ts...> {
|
||||
protected:
|
||||
FloatOutput *output_;
|
||||
};
|
||||
#endif // USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
|
||||
} // namespace output
|
||||
} // namespace esphome
|
||||
|
||||
@@ -7,13 +7,15 @@ namespace output {
|
||||
|
||||
static const char *const TAG = "output.float";
|
||||
|
||||
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
void FloatOutput::set_max_power(float max_power) {
|
||||
this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to MIN>=MAX>=1.0
|
||||
this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to min_power <= max <= 1.0
|
||||
}
|
||||
|
||||
void FloatOutput::set_min_power(float min_power) {
|
||||
this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0>=MIN>=MAX
|
||||
this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0 <= min <= max_power
|
||||
}
|
||||
#endif
|
||||
|
||||
void FloatOutput::set_level(float state) {
|
||||
state = clamp(state, 0.0f, 1.0f);
|
||||
@@ -26,8 +28,10 @@ void FloatOutput::set_level(float state) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
if (state != 0.0f || !this->zero_means_zero_) // regardless of min_power_, 0.0 means off
|
||||
state = (state * (this->max_power_ - this->min_power_)) + this->min_power_;
|
||||
#endif
|
||||
|
||||
if (this->is_inverted())
|
||||
state = 1.0f - state;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "binary_output.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace output {
|
||||
|
||||
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
#define LOG_FLOAT_OUTPUT(this) \
|
||||
LOG_BINARY_OUTPUT(this) \
|
||||
if (this->max_power_ != 1.0f) { \
|
||||
@@ -14,6 +16,9 @@ namespace output {
|
||||
if (this->min_power_ != 0.0f) { \
|
||||
ESP_LOGCONFIG(TAG, " Min Power: %.1f%%", this->min_power_ * 100.0f); \
|
||||
}
|
||||
#else
|
||||
#define LOG_FLOAT_OUTPUT(this) LOG_BINARY_OUTPUT(this)
|
||||
#endif
|
||||
|
||||
/** Base class for all output components that can output a variable level, like PWM.
|
||||
*
|
||||
@@ -22,14 +27,18 @@ namespace output {
|
||||
* makes using maths much easier and (in theory) supports all possible bit depths.
|
||||
*
|
||||
* If you want to create a FloatOutput yourself, you essentially just have to override write_state(float).
|
||||
* That method will be called for you with inversion and max-min power and offset to min power already applied.
|
||||
* That method will be called for you with inversion already applied. When USE_OUTPUT_FLOAT_POWER_SCALING is
|
||||
* enabled (set automatically by Python codegen if any output uses min_power/max_power/zero_means_zero or the
|
||||
* matching runtime actions), the value will additionally have max-min power scaling and offset to min_power
|
||||
* applied; otherwise only inversion is applied.
|
||||
*
|
||||
* This interface is compatible with BinaryOutput (and will automatically convert the binary states to floating
|
||||
* point states for you). Additionally, this class provides a way for users to set a minimum and/or maximum power
|
||||
* output
|
||||
* output (gated on USE_OUTPUT_FLOAT_POWER_SCALING).
|
||||
*/
|
||||
class FloatOutput : public BinaryOutput {
|
||||
public:
|
||||
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
/** Set the maximum power output of this component.
|
||||
*
|
||||
* All values are multiplied by max_power - min_power and offset to min_power to get the adjusted value.
|
||||
@@ -51,6 +60,32 @@ class FloatOutput : public BinaryOutput {
|
||||
* @param zero_means_zero True if a 0 state should mean 0 and not min_power.
|
||||
*/
|
||||
void set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; }
|
||||
#else
|
||||
// Compile-time guards for users calling these methods from lambdas (documented usage at
|
||||
// https://esphome.io/components/output/#output-set_min_power_action). When power scaling
|
||||
// is compiled out, these template stubs fail to compile with an actionable error pointing
|
||||
// at the user's lambda. Templating on a default-false bool means static_assert only fires
|
||||
// on instantiation (i.e. when the user actually calls the method), not on every parse.
|
||||
template<bool _use_output_float_power_scaling = false> void set_max_power(float max_power) {
|
||||
static_assert(_use_output_float_power_scaling,
|
||||
"set_max_power() requires USE_OUTPUT_FLOAT_POWER_SCALING. "
|
||||
"To enable it, add 'max_power: 100%' (or any value) to one output entry in your YAML — "
|
||||
"the codegen will then keep the scaling fields. "
|
||||
"See https://esphome.io/components/output/ for details.");
|
||||
}
|
||||
template<bool _use_output_float_power_scaling = false> void set_min_power(float min_power) {
|
||||
static_assert(_use_output_float_power_scaling,
|
||||
"set_min_power() requires USE_OUTPUT_FLOAT_POWER_SCALING. "
|
||||
"To enable it, add 'min_power: 0%' (or any value) to one output entry in your YAML — "
|
||||
"the codegen will then keep the scaling fields. "
|
||||
"See https://esphome.io/components/output/ for details.");
|
||||
}
|
||||
template<bool _use_output_float_power_scaling = false> void set_zero_means_zero(bool zero_means_zero) {
|
||||
static_assert(_use_output_float_power_scaling,
|
||||
"set_zero_means_zero() requires USE_OUTPUT_FLOAT_POWER_SCALING. "
|
||||
"To enable it, add 'zero_means_zero: true' to one output entry in your YAML.");
|
||||
}
|
||||
#endif
|
||||
|
||||
/** Set the level of this float output, this is called from the front-end.
|
||||
*
|
||||
@@ -69,20 +104,30 @@ class FloatOutput : public BinaryOutput {
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
|
||||
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
/// Get the maximum power output.
|
||||
float get_max_power() const { return this->max_power_; }
|
||||
|
||||
/// Get the minimum power output.
|
||||
float get_min_power() const { return this->min_power_; }
|
||||
#else
|
||||
/// Get the maximum power output.
|
||||
float get_max_power() const { return 1.0f; }
|
||||
|
||||
/// Get the minimum power output.
|
||||
float get_min_power() const { return 0.0f; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
/// Implement BinarySensor's write_enabled; this should never be called.
|
||||
void write_state(bool state) override;
|
||||
virtual void write_state(float state) = 0;
|
||||
|
||||
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
float max_power_{1.0f};
|
||||
float min_power_{0.0f};
|
||||
bool zero_means_zero_;
|
||||
bool zero_means_zero_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace output
|
||||
|
||||
@@ -433,13 +433,17 @@ void Pipsolar::handle_qpigs_(const char *message) {
|
||||
}
|
||||
|
||||
void Pipsolar::handle_qmod_(const char *message) {
|
||||
std::string mode;
|
||||
char device_mode = char(message[1]);
|
||||
if (this->last_qmod_) {
|
||||
this->last_qmod_->publish_state(message);
|
||||
}
|
||||
// QMOD response is "(M" where M is the device-mode character. Bail out if the
|
||||
// message is shorter than 2 chars (e.g. empty error response from
|
||||
// handle_poll_error_) — reading message[1] would otherwise be out of bounds.
|
||||
if (message[0] == '\0' || message[1] == '\0')
|
||||
return;
|
||||
if (this->device_mode_) {
|
||||
mode = device_mode;
|
||||
std::string mode;
|
||||
mode = char(message[1]);
|
||||
this->device_mode_->publish_state(mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) {
|
||||
if (!this->rd_start_time_.has_value()) {
|
||||
this->rd_start_time_ = millis();
|
||||
}
|
||||
const uint32_t rd_start_time = *this->rd_start_time_;
|
||||
|
||||
while (true) {
|
||||
if (this->is_read_ready()) {
|
||||
@@ -324,7 +325,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (millis() - *this->rd_start_time_ > 100) {
|
||||
if (millis() - rd_start_time > 100) {
|
||||
ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!");
|
||||
this->rd_ready_ = TIMEOUT;
|
||||
break;
|
||||
|
||||
@@ -13,11 +13,7 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { ::yield(); }
|
||||
uint64_t millis_64() { return micros_to_millis<uint64_t>(time_us_64()); }
|
||||
uint32_t HOT millis() { return micros_to_millis(time_us_64()); }
|
||||
void HOT delay(uint32_t ms) { ::delay(ms); }
|
||||
uint32_t HOT micros() { return ::micros(); }
|
||||
// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h.
|
||||
void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
void arch_restart() {
|
||||
watchdog_reboot(0, 0, 10);
|
||||
|
||||
@@ -110,7 +110,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
case STATE_INIT: {
|
||||
// While we were waiting for update to check for messages, this notifies a message
|
||||
// is available.
|
||||
bool message_available = message.compare(0, 6, "+CMTI:") == 0;
|
||||
bool message_available = message.starts_with("+CMTI:");
|
||||
if (!message_available) {
|
||||
if (message == "RING") {
|
||||
// Incoming call...
|
||||
@@ -120,7 +120,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
this->call_state_ = 6;
|
||||
this->call_disconnected_callback_.call();
|
||||
}
|
||||
} else if (message.compare(0, 6, "+CUSD:") == 0) {
|
||||
} else if (message.starts_with("+CUSD:")) {
|
||||
// Incoming USSD MESSAGE
|
||||
this->state_ = STATE_CHECK_USSD;
|
||||
}
|
||||
@@ -175,7 +175,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
break;
|
||||
case STATE_CHECK_USSD:
|
||||
ESP_LOGD(TAG, "Check ussd code: '%s'", message.c_str());
|
||||
if (message.compare(0, 6, "+CUSD:") == 0) {
|
||||
if (message.starts_with("+CUSD:")) {
|
||||
this->state_ = STATE_RECEIVED_USSD;
|
||||
this->ussd_ = "";
|
||||
size_t start = 10;
|
||||
@@ -196,8 +196,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
case STATE_CREG_WAIT: {
|
||||
// Response: "+CREG: 0,1" -- the one there means registered ok
|
||||
// "+CREG: -,-" means not registered ok
|
||||
bool registered =
|
||||
message.size() > 9 && message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5');
|
||||
bool registered = message.size() > 9 && message.starts_with("+CREG:") && (message[9] == '1' || message[9] == '5');
|
||||
if (registered) {
|
||||
if (!this->registered_) {
|
||||
ESP_LOGD(TAG, "Registered OK");
|
||||
@@ -223,7 +222,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
this->state_ = STATE_CSQ_RESPONSE;
|
||||
break;
|
||||
case STATE_CSQ_RESPONSE:
|
||||
if (message.compare(0, 5, "+CSQ:") == 0) {
|
||||
if (message.starts_with("+CSQ:")) {
|
||||
size_t comma = message.find(',', 6);
|
||||
if (comma != 6) {
|
||||
int rssi = parse_number<int>(message.substr(6, comma - 6)).value_or(0);
|
||||
@@ -243,7 +242,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
this->state_ = STATE_CHECK_SMS;
|
||||
break;
|
||||
case STATE_PARSE_SMS_RESPONSE:
|
||||
if (message.compare(0, 6, "+CMGL:") == 0 && this->parse_index_ == 0) {
|
||||
if (message.starts_with("+CMGL:") && this->parse_index_ == 0) {
|
||||
size_t start = 7;
|
||||
size_t end = message.find(',', start);
|
||||
uint8_t item = 0;
|
||||
@@ -278,7 +277,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
}
|
||||
break;
|
||||
case STATE_CHECK_CALL:
|
||||
if (message.compare(0, 6, "+CLCC:") == 0 && this->parse_index_ == 0) {
|
||||
if (message.starts_with("+CLCC:") && this->parse_index_ == 0) {
|
||||
this->expect_ack_ = true;
|
||||
size_t start = 7;
|
||||
size_t end = message.find(',', start);
|
||||
@@ -324,7 +323,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
/* Our recipient is set and the message body is in message
|
||||
kick ESPHome callback now
|
||||
*/
|
||||
if (ok || message.compare(0, 6, "+CMGL:") == 0) {
|
||||
if (ok || message.starts_with("+CMGL:")) {
|
||||
ESP_LOGD(TAG,
|
||||
"Received SMS from: %s\n"
|
||||
" %s",
|
||||
@@ -360,7 +359,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
}
|
||||
break;
|
||||
case STATE_SENDING_SMS_3:
|
||||
if (message.compare(0, 6, "+CMGS:") == 0) {
|
||||
if (message.starts_with("+CMGS:")) {
|
||||
ESP_LOGD(TAG, "SMS Sent OK: %s", message.c_str());
|
||||
this->send_pending_ = false;
|
||||
this->state_ = STATE_CHECK_SMS;
|
||||
@@ -383,7 +382,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
this->state_ = STATE_INIT;
|
||||
break;
|
||||
case STATE_PARSE_CLIP:
|
||||
if (message.compare(0, 6, "+CLIP:") == 0) {
|
||||
if (message.starts_with("+CLIP:")) {
|
||||
std::string caller_id;
|
||||
size_t start = 7;
|
||||
size_t end = message.find(',', start);
|
||||
|
||||
@@ -502,7 +502,7 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) {
|
||||
media_command.announce = false;
|
||||
}
|
||||
|
||||
auto media_url = call.get_media_url();
|
||||
const auto &media_url = call.get_media_url();
|
||||
if (media_url.has_value()) {
|
||||
media_command.url =
|
||||
new std::string(*media_url); // Must be manually deleted after receiving media_command from a queue
|
||||
|
||||
@@ -698,7 +698,7 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call
|
||||
}
|
||||
}
|
||||
|
||||
auto media_url = call.get_media_url();
|
||||
const auto &media_url = call.get_media_url();
|
||||
if (media_url.has_value()) {
|
||||
auto command = call.get_command();
|
||||
bool enqueue = command.has_value() && command.value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE;
|
||||
|
||||
@@ -451,7 +451,7 @@ class SPIDevice : public SPIClient {
|
||||
|
||||
uint8_t read_byte() { return this->delegate_->transfer(0); }
|
||||
|
||||
void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); }
|
||||
void read_array(uint8_t *data, size_t length) { this->delegate_->read_array(data, length); }
|
||||
|
||||
/**
|
||||
* Write a single data item, up to 32 bits.
|
||||
|
||||
@@ -897,11 +897,12 @@ void Sprinkler::resume() {
|
||||
}
|
||||
|
||||
if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) {
|
||||
const size_t paused_valve = *this->paused_valve_;
|
||||
const uint32_t resume_duration = *this->resume_duration_;
|
||||
// Resume only if valve has not been completed yet
|
||||
if (!this->valve_cycle_complete_(this->paused_valve_.value())) {
|
||||
ESP_LOGD(TAG, "Resuming valve %zu with %" PRIu32 " seconds remaining", this->paused_valve_.value_or(0),
|
||||
this->resume_duration_.value_or(0));
|
||||
this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value());
|
||||
if (!this->valve_cycle_complete_(paused_valve)) {
|
||||
ESP_LOGD(TAG, "Resuming valve %zu with %" PRIu32 " seconds remaining", paused_valve, resume_duration);
|
||||
this->fsm_request_(paused_valve, resume_duration);
|
||||
}
|
||||
this->reset_resume();
|
||||
} else {
|
||||
|
||||
@@ -68,13 +68,8 @@ void TLC5971::transfer_(uint8_t send) {
|
||||
uint8_t startbit = 0x80;
|
||||
|
||||
bool towrite, lastmosi = !(send & startbit);
|
||||
uint8_t bitdelay_us = (1000000 / 1000000) / 2;
|
||||
|
||||
for (uint8_t b = startbit; b != 0; b = b >> 1) {
|
||||
if (bitdelay_us) {
|
||||
delayMicroseconds(bitdelay_us);
|
||||
}
|
||||
|
||||
towrite = send & b;
|
||||
if ((lastmosi != towrite)) {
|
||||
this->data_pin_->digital_write(towrite);
|
||||
@@ -82,11 +77,6 @@ void TLC5971::transfer_(uint8_t send) {
|
||||
}
|
||||
|
||||
this->clock_pin_->digital_write(true);
|
||||
|
||||
if (bitdelay_us) {
|
||||
delayMicroseconds(bitdelay_us);
|
||||
}
|
||||
|
||||
this->clock_pin_->digital_write(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <cinttypes>
|
||||
#include <cstdint>
|
||||
#ifdef USE_ESP32
|
||||
#include <soc/soc_caps.h>
|
||||
#include "esp_idf_version.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#endif
|
||||
@@ -40,7 +39,7 @@ void WatchdogManager::set_timeout_(uint32_t timeout_ms) {
|
||||
#ifdef USE_ESP32
|
||||
esp_task_wdt_config_t wdt_config = {
|
||||
.timeout_ms = timeout_ms,
|
||||
.idle_core_mask = (1 << SOC_CPU_CORES_NUM) - 1,
|
||||
.idle_core_mask = (1U << CONFIG_FREERTOS_NUMBER_OF_CORES) - 1U,
|
||||
.trigger_panic = true,
|
||||
};
|
||||
esp_task_wdt_reconfigure(&wdt_config);
|
||||
|
||||
@@ -1099,9 +1099,9 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
auto eap_opt = ap.get_eap();
|
||||
const auto &eap_opt = ap.get_eap();
|
||||
if (eap_opt.has_value()) {
|
||||
EAPAuth eap_config = *eap_opt;
|
||||
const EAPAuth &eap_config = *eap_opt;
|
||||
// clang-format off
|
||||
ESP_LOGV(
|
||||
TAG,
|
||||
|
||||
@@ -313,10 +313,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
|
||||
// setup enterprise authentication if required
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
auto eap_opt = ap.get_eap();
|
||||
const auto &eap_opt = ap.get_eap();
|
||||
if (eap_opt.has_value()) {
|
||||
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
|
||||
EAPAuth eap = *eap_opt;
|
||||
const EAPAuth &eap = *eap_opt;
|
||||
ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
|
||||
if (ret) {
|
||||
ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed: %d", ret);
|
||||
|
||||
@@ -179,7 +179,10 @@ void WiFiComponent::wifi_pre_setup_() {
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
// cfg.nvs_enable = false;
|
||||
if (global_preferences->nvs_handle == 0) {
|
||||
ESP_LOGW(TAG, "starting wifi without nvs");
|
||||
cfg.nvs_enable = false;
|
||||
}
|
||||
err = esp_wifi_init(&cfg);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err));
|
||||
@@ -404,10 +407,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
|
||||
// setup enterprise authentication if required
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
auto eap_opt = ap.get_eap();
|
||||
const auto &eap_opt = ap.get_eap();
|
||||
if (eap_opt.has_value()) {
|
||||
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
|
||||
EAPAuth eap = *eap_opt;
|
||||
const EAPAuth &eap = *eap_opt;
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
|
||||
#else
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
#include "esphome/core/hal.h"
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
#include "esphome/components/deep_sleep/deep_sleep_component.h"
|
||||
#endif
|
||||
#include "esphome/core/wake.h"
|
||||
|
||||
extern "C" {
|
||||
#include <zboss_api.h>
|
||||
@@ -119,11 +117,7 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
|
||||
/* Set default response value. */
|
||||
p_device_cb_param->status = RET_OK;
|
||||
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
if (auto *ds = deep_sleep::global_deep_sleep.load()) {
|
||||
ds->wakeup();
|
||||
}
|
||||
#endif
|
||||
esphome::wake_loop_threadsafe();
|
||||
|
||||
// endpoints are enumerated from 1
|
||||
if (global_zigbee->callbacks_.size() >= endpoint) {
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include <vector>
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_LWIP_FAST_SELECT) && defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
#include <atomic> // for std::atomic_thread_fence in Application::loop()
|
||||
#endif
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
@@ -370,6 +374,9 @@ class Application {
|
||||
#elif defined(USE_ESP8266)
|
||||
/// Wake from ISR (ESP8266). No task_woken arg — no FreeRTOS. Caller must be IRAM_ATTR.
|
||||
static void IRAM_ATTR ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { esphome::wake_loop_isrsafe(); }
|
||||
#elif defined(USE_ZEPHYR)
|
||||
/// Wake from ISR (Zephyr). No task_woken arg — k_sem_give() handles ISR scheduling internally.
|
||||
static void wake_loop_isrsafe() { esphome::wake_loop_isrsafe(); }
|
||||
#endif
|
||||
|
||||
/// Wake from any context (ISR, thread, callback).
|
||||
@@ -377,12 +384,16 @@ class Application {
|
||||
|
||||
protected:
|
||||
friend Component;
|
||||
friend class Scheduler;
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
friend class runtime_stats::RuntimeStatsCollector;
|
||||
#endif
|
||||
friend void ::setup();
|
||||
friend void ::original_setup();
|
||||
|
||||
/// Freshen the cached loop component start time. Called by Scheduler before each dispatch.
|
||||
void set_loop_component_start_time_(uint32_t now) { this->loop_component_start_time_ = now; }
|
||||
|
||||
/// Walk all registered components looking for any whose component_state_
|
||||
/// has the given flag set. Used by Component::status_clear_*_slow_path_()
|
||||
/// (which is a friend) to decide whether to clear the corresponding bit on
|
||||
@@ -573,6 +584,15 @@ inline ESPHOME_ALWAYS_INLINE Application::ComponentPhaseGuard::ComponentPhaseGua
|
||||
}
|
||||
|
||||
inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
#if defined(USE_LWIP_FAST_SELECT) && defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
// Pairs with the TCP/IP thread's SYS_ARCH_UNPROTECT release on rcvevent so
|
||||
// subsequent Socket::ready() checks in this iter observe the published state
|
||||
// without a per-call memw. Wake is independent (xTaskNotifyGive/
|
||||
// ulTaskNotifyTake), so non-losing. Skipped on MULTI_NO_ATOMICS (e.g.
|
||||
// BK72xx) — that path keeps `volatile` in esphome_lwip_socket_has_data()
|
||||
// instead.
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
#endif
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
// Capture the start of the active (non-sleeping) portion of this iteration.
|
||||
// Used to derive main-loop overhead = active time − Σ(component time) −
|
||||
|
||||
@@ -273,18 +273,32 @@ template<typename... Ts> class WhileLoopContinuation : public Action<Ts...> {
|
||||
WhileAction<Ts...> *parent_;
|
||||
};
|
||||
|
||||
// Wraps a ContinuationAction when Enabled, empty otherwise.
|
||||
// Lets IfAction elide the else continuation when HasElse is false.
|
||||
template<bool Enabled, typename... Ts> struct OptionalContinuation {
|
||||
ContinuationAction<Ts...> action;
|
||||
explicit OptionalContinuation(Action<Ts...> *parent) : action(parent) {}
|
||||
};
|
||||
template<typename... Ts> struct OptionalContinuation<false, Ts...> {
|
||||
explicit OptionalContinuation(Action<Ts...> * /*parent*/) {}
|
||||
};
|
||||
|
||||
template<bool HasElse, typename... Ts> class IfAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {}
|
||||
|
||||
// Precondition: add_then/add_else must be called at most once per instance.
|
||||
// Codegen always batches the full action list into a single call. Calling
|
||||
// twice would re-append the same inline continuation pointer and form a
|
||||
// self-loop in the next_ chain.
|
||||
void add_then(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
this->then_.add_actions(actions);
|
||||
this->then_.add_action(new ContinuationAction<Ts...>(this));
|
||||
this->then_.add_action(&this->then_continuation_);
|
||||
}
|
||||
|
||||
void add_else(const std::initializer_list<Action<Ts...> *> &actions) requires(HasElse) {
|
||||
this->else_.add_actions(actions);
|
||||
this->else_.add_action(new ContinuationAction<Ts...>(this));
|
||||
this->else_.add_action(&this->else_continuation_.action);
|
||||
}
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
@@ -316,17 +330,20 @@ template<bool HasElse, typename... Ts> class IfAction : public Action<Ts...> {
|
||||
protected:
|
||||
Condition<Ts...> *condition_;
|
||||
ActionList<Ts...> then_;
|
||||
ContinuationAction<Ts...> then_continuation_{this};
|
||||
struct NoElse {};
|
||||
[[no_unique_address]] std::conditional_t<HasElse, ActionList<Ts...>, NoElse> else_;
|
||||
[[no_unique_address]] OptionalContinuation<HasElse, Ts...> else_continuation_{this};
|
||||
};
|
||||
|
||||
template<typename... Ts> class WhileAction : public Action<Ts...> {
|
||||
public:
|
||||
WhileAction(Condition<Ts...> *condition) : condition_(condition) {}
|
||||
|
||||
// Precondition: must be called at most once per instance (see IfAction::add_then).
|
||||
void add_then(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
this->then_.add_actions(actions);
|
||||
this->then_.add_action(new WhileLoopContinuation<Ts...>(this));
|
||||
this->then_.add_action(&this->loop_continuation_);
|
||||
}
|
||||
|
||||
friend class WhileLoopContinuation<Ts...>;
|
||||
@@ -354,6 +371,7 @@ template<typename... Ts> class WhileAction : public Action<Ts...> {
|
||||
protected:
|
||||
Condition<Ts...> *condition_;
|
||||
ActionList<Ts...> then_;
|
||||
WhileLoopContinuation<Ts...> loop_continuation_{this};
|
||||
};
|
||||
|
||||
// Implementation of WhileLoopContinuation::play
|
||||
@@ -386,9 +404,10 @@ template<typename... Ts> class RepeatAction : public Action<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint32_t, count)
|
||||
|
||||
// Precondition: must be called at most once per instance (see IfAction::add_then).
|
||||
void add_then(const std::initializer_list<Action<uint32_t, Ts...> *> &actions) {
|
||||
this->then_.add_actions(actions);
|
||||
this->then_.add_action(new RepeatLoopContinuation<Ts...>(this));
|
||||
this->then_.add_action(&this->loop_continuation_);
|
||||
}
|
||||
|
||||
friend class RepeatLoopContinuation<Ts...>;
|
||||
@@ -409,6 +428,7 @@ template<typename... Ts> class RepeatAction : public Action<Ts...> {
|
||||
|
||||
protected:
|
||||
ActionList<uint32_t, Ts...> then_;
|
||||
RepeatLoopContinuation<Ts...> loop_continuation_{this};
|
||||
};
|
||||
|
||||
// Implementation of RepeatLoopContinuation::play
|
||||
|
||||
@@ -792,6 +792,29 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
# Per-platform wake implementations — wake.h dispatches to exactly one of
|
||||
# these based on USE_*, so the others can be skipped at the source level
|
||||
# too. Header files next to each .cpp are always copied (the dispatcher
|
||||
# #include's them) but compile to empty TUs on the wrong platform anyway.
|
||||
"wake/wake_freertos.cpp": {
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
PlatformFramework.BK72XX_ARDUINO,
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
"wake/wake_esp8266.cpp": {
|
||||
PlatformFramework.ESP8266_ARDUINO,
|
||||
},
|
||||
"wake/wake_rp2040.cpp": {
|
||||
PlatformFramework.RP2040_ARDUINO,
|
||||
},
|
||||
"wake/wake_host.cpp": {
|
||||
PlatformFramework.HOST_NATIVE,
|
||||
},
|
||||
"wake/wake_zephyr.cpp": {
|
||||
PlatformFramework.NRF52_ZEPHYR,
|
||||
},
|
||||
# Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered
|
||||
# as they are only included when needed by the preprocessor
|
||||
}
|
||||
|
||||
+16
-1
@@ -17,8 +17,21 @@
|
||||
#define ESPHOME_DEBUG_SCHEDULER
|
||||
#define ESPHOME_DEBUG_API
|
||||
|
||||
// Default threading model for static analysis (ESP32 is multi-threaded with atomics)
|
||||
// Threading model for static analysis. Match what the real codegen picks per
|
||||
// platform (see esphome/components/<platform>/__init__.py ThreadModel.*):
|
||||
// USE_ESP8266 / USE_RP2040 / USE_NRF52 → SINGLE
|
||||
// USE_BK72XX (ARMv5TE, no LDREX/STREX) → MULTI_NO_ATOMICS
|
||||
// everything else (ESP32, host, RTL87XX, LN882X) → MULTI_ATOMICS
|
||||
// Without this the clang-tidy envs end up with USE_<single-threaded platform>
|
||||
// + MULTI_ATOMICS simultaneously, a combination that can never occur in a
|
||||
// real build.
|
||||
#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_NRF52)
|
||||
#define ESPHOME_THREAD_SINGLE
|
||||
#elif defined(USE_BK72XX)
|
||||
#define ESPHOME_THREAD_MULTI_NO_ATOMICS
|
||||
#else
|
||||
#define ESPHOME_THREAD_MULTI_ATOMICS
|
||||
#endif
|
||||
|
||||
// logger
|
||||
#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
@@ -83,6 +96,7 @@
|
||||
#define USE_LVGL_CHECKBOX
|
||||
#define USE_LVGL_DROPDOWN
|
||||
#define USE_LVGL_FONT
|
||||
#define USE_LVGL_GRADIENT
|
||||
#define USE_LVGL_IMAGE
|
||||
#define USE_LVGL_IMAGEBUTTON
|
||||
#define USE_LVGL_KEY_LISTENER
|
||||
@@ -132,6 +146,7 @@
|
||||
#define USE_NEXTION_WAVEFORM
|
||||
#define USE_NUMBER
|
||||
#define USE_OUTPUT
|
||||
#define USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
#define USE_POWER_SUPPLY
|
||||
#define USE_PREFERENCES_SYNC_EVERY_LOOP
|
||||
#define USE_QR_CODE
|
||||
|
||||
+22
-103
@@ -2,111 +2,34 @@
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/time_64.h"
|
||||
#include "esphome/core/time_conversion.h"
|
||||
|
||||
// Per-platform HAL bits (IRAM_ATTR / PROGMEM macros, in_isr_context(),
|
||||
// inline yield/delay/micros/millis/millis_64 wrappers, ESP8266 progmem
|
||||
// helpers) live under esphome/core/hal/ and are dispatched here based on
|
||||
// the active USE_* platform define. Each header guards its body with the
|
||||
// matching #ifdef USE_<platform> and re-enters namespace esphome {} so it
|
||||
// is safe to be re-included.
|
||||
#if defined(USE_ESP32)
|
||||
#include <esp_attr.h>
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
#include "esphome/core/hal/hal_esp32.h"
|
||||
#elif defined(USE_ESP8266)
|
||||
|
||||
#include <c_types.h>
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM ICACHE_RODATA_ATTR
|
||||
#endif
|
||||
|
||||
#elif defined(USE_RP2040)
|
||||
|
||||
#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical")))
|
||||
#define PROGMEM
|
||||
|
||||
#include "esphome/core/hal/hal_esp8266.h"
|
||||
#elif defined(USE_LIBRETINY)
|
||||
|
||||
// IRAM_ATTR places a function in executable RAM so it is callable from an
|
||||
// ISR even while flash is busy (XIP stall, OTA, logger flash write).
|
||||
// Each family uses a section its stock linker already routes to RAM:
|
||||
// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the
|
||||
// exception: its stock linker has no matching glob, so patch_linker.py
|
||||
// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link.
|
||||
//
|
||||
// BK72xx (all variants) are left as a no-op: their SDK wraps flash
|
||||
// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for
|
||||
// the duration of every write, so no ISR fires while flash is stalled and
|
||||
// the race IRAM_ATTR guards against cannot occur. The trade-off is that
|
||||
// interrupts are delayed (not dropped) by up to ~20 ms during a sector
|
||||
// erase, but that is an SDK-level choice and cannot be changed from this
|
||||
// layer.
|
||||
#if defined(USE_BK72XX)
|
||||
#define IRAM_ATTR
|
||||
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
|
||||
// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM).
|
||||
#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text")))
|
||||
#include "esphome/core/hal/hal_libretiny.h"
|
||||
#elif defined(USE_RP2040)
|
||||
#include "esphome/core/hal/hal_rp2040.h"
|
||||
#elif defined(USE_HOST)
|
||||
#include "esphome/core/hal/hal_host.h"
|
||||
#elif defined(USE_ZEPHYR)
|
||||
#include "esphome/core/hal/hal_zephyr.h"
|
||||
#else
|
||||
// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text.
|
||||
// LN882H: patch_linker.py.script injects *(.sram.text*) into
|
||||
// .flash_copysection (> RAM0 AT> FLASH).
|
||||
#define IRAM_ATTR __attribute__((noinline, section(".sram.text")))
|
||||
#endif
|
||||
#define PROGMEM
|
||||
|
||||
#else
|
||||
|
||||
#define IRAM_ATTR
|
||||
#define PROGMEM
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so
|
||||
// it is callable from Thumb code via interworking. The MRS CPSR instruction
|
||||
// is ARM-only and user code here may be built in Thumb, so in_isr_context()
|
||||
// defers to this port helper on BK72xx instead of reading CPSR inline.
|
||||
extern "C" uint32_t platform_is_in_interrupt_context(void);
|
||||
#error "hal.h: not implemented for this platform"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
/// always_inline so callers placed in IRAM keep the detection in IRAM.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() {
|
||||
#if defined(USE_ESP32)
|
||||
return xPortInIsrContext() != 0;
|
||||
#elif defined(USE_ESP8266)
|
||||
// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is
|
||||
// non-zero both in a real ISR and when user code masks interrupts. The
|
||||
// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule
|
||||
// which is ISR-safe) so this helper is unused on this platform.
|
||||
return false;
|
||||
#elif defined(USE_RP2040)
|
||||
uint32_t ipsr;
|
||||
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
|
||||
return ipsr != 0;
|
||||
#elif defined(USE_BK72XX)
|
||||
// BK72xx is ARM968E-S (ARM9); see extern declaration above.
|
||||
return platform_is_in_interrupt_context() != 0;
|
||||
#elif defined(USE_LIBRETINY)
|
||||
// Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number;
|
||||
// non-zero means we're in a handler.
|
||||
uint32_t ipsr;
|
||||
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
|
||||
return ipsr != 0;
|
||||
#else
|
||||
// Host and any future platform without an ISR concept.
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void yield();
|
||||
uint32_t millis();
|
||||
uint64_t millis_64();
|
||||
uint32_t micros();
|
||||
void delay(uint32_t ms);
|
||||
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
|
||||
void __attribute__((noreturn)) arch_restart();
|
||||
void arch_init();
|
||||
@@ -114,13 +37,9 @@ void arch_feed_wdt();
|
||||
uint32_t arch_get_cpu_cycle_count();
|
||||
uint32_t arch_get_cpu_freq_hz();
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266: pgm_read_* does real flash reads on Harvard architecture
|
||||
uint8_t progmem_read_byte(const uint8_t *addr);
|
||||
const char *progmem_read_ptr(const char *const *addr);
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr);
|
||||
#else
|
||||
// All other platforms: PROGMEM is a no-op, so these are direct dereferences
|
||||
#ifndef USE_ESP8266
|
||||
// All non-ESP8266 platforms: PROGMEM is a no-op, so these are direct dereferences.
|
||||
// ESP8266's out-of-line declarations live in hal/hal_esp8266.h.
|
||||
inline uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
inline const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
inline uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <cstdint>
|
||||
#include <esp_attr.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include "esphome/core/time_conversion.h"
|
||||
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrContext() != 0; }
|
||||
|
||||
// Forward decl from <esp_timer.h>.
|
||||
// NOLINTNEXTLINE(readability-redundant-declaration)
|
||||
extern "C" int64_t esp_timer_get_time(void);
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { vPortYield(); }
|
||||
__attribute__((always_inline)) inline void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(esp_timer_get_time()); }
|
||||
uint32_t millis();
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() {
|
||||
return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time()));
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include <c_types.h>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/time_64.h"
|
||||
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM ICACHE_RODATA_ATTR
|
||||
#endif
|
||||
|
||||
// Forward decls from Arduino's <Arduino.h> for the inline wrappers below.
|
||||
// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
extern "C" void yield(void);
|
||||
extern "C" void delay(unsigned long ms);
|
||||
extern "C" unsigned long micros(void);
|
||||
extern "C" unsigned long millis(void);
|
||||
// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
/// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is
|
||||
/// non-zero both in a real ISR and when user code masks interrupts. The
|
||||
/// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule
|
||||
/// which is ISR-safe) so this helper is unused on this platform.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { ::yield(); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
void delay(uint32_t ms);
|
||||
uint32_t millis();
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); }
|
||||
|
||||
// ESP8266: pgm_read_* does real flash reads on Harvard architecture
|
||||
uint8_t progmem_read_byte(const uint8_t *addr);
|
||||
const char *progmem_read_ptr(const char *const *addr);
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr);
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP8266
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#define IRAM_ATTR
|
||||
#define PROGMEM
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
/// Host has no ISR concept.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
|
||||
|
||||
void yield();
|
||||
void delay(uint32_t ms);
|
||||
uint32_t micros();
|
||||
uint32_t millis();
|
||||
uint64_t millis_64();
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_HOST
|
||||
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// For the inline millis() fast paths (xTaskGetTickCount, portTICK_PERIOD_MS).
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
|
||||
#include "esphome/core/time_64.h"
|
||||
|
||||
// IRAM_ATTR places a function in executable RAM so it is callable from an
|
||||
// ISR even while flash is busy (XIP stall, OTA, logger flash write).
|
||||
// Each family uses a section its stock linker already routes to RAM:
|
||||
// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the
|
||||
// exception: its stock linker has no matching glob, so patch_linker.py
|
||||
// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link.
|
||||
//
|
||||
// BK72xx (all variants) are left as a no-op: their SDK wraps flash
|
||||
// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for
|
||||
// the duration of every write, so no ISR fires while flash is stalled and
|
||||
// the race IRAM_ATTR guards against cannot occur. The trade-off is that
|
||||
// interrupts are delayed (not dropped) by up to ~20 ms during a sector
|
||||
// erase, but that is an SDK-level choice and cannot be changed from this
|
||||
// layer.
|
||||
#if defined(USE_BK72XX)
|
||||
#define IRAM_ATTR
|
||||
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
|
||||
// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM).
|
||||
#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text")))
|
||||
#else
|
||||
// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text.
|
||||
// LN882H: patch_linker.py.script injects *(.sram.text*) into
|
||||
// .flash_copysection (> RAM0 AT> FLASH).
|
||||
#define IRAM_ATTR __attribute__((noinline, section(".sram.text")))
|
||||
#endif
|
||||
#define PROGMEM
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so
|
||||
// it is callable from Thumb code via interworking. The MRS CPSR instruction
|
||||
// is ARM-only and user code here may be built in Thumb, so in_isr_context()
|
||||
// defers to this port helper on BK72xx instead of reading CPSR inline.
|
||||
extern "C" uint32_t platform_is_in_interrupt_context(void);
|
||||
#endif
|
||||
|
||||
// Forward decls from Arduino's <Arduino.h> for the inline wrappers below.
|
||||
// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
extern "C" void yield(void);
|
||||
extern "C" void delay(unsigned long ms);
|
||||
extern "C" unsigned long micros(void);
|
||||
extern "C" unsigned long millis(void);
|
||||
// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() {
|
||||
#if defined(USE_BK72XX)
|
||||
// BK72xx is ARM968E-S (ARM9); see extern declaration above.
|
||||
return platform_is_in_interrupt_context() != 0;
|
||||
#else
|
||||
// Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number;
|
||||
// non-zero means we're in a handler.
|
||||
uint32_t ipsr;
|
||||
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
|
||||
return ipsr != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { ::yield(); }
|
||||
__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
|
||||
// Per-variant millis() fast path — matches MillisInternal::get().
|
||||
#if defined(USE_RTL87XX) || defined(USE_LN882X)
|
||||
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
|
||||
__attribute__((always_inline)) inline uint32_t millis() {
|
||||
// xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract.
|
||||
return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount();
|
||||
}
|
||||
#elif defined(USE_BK72XX)
|
||||
static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick");
|
||||
__attribute__((always_inline)) inline uint32_t millis() { return xTaskGetTickCount() * portTICK_PERIOD_MS; }
|
||||
#else
|
||||
__attribute__((always_inline)) inline uint32_t millis() { return static_cast<uint32_t>(::millis()); }
|
||||
#endif
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_RP2040
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/time_conversion.h"
|
||||
|
||||
#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical")))
|
||||
#define PROGMEM
|
||||
|
||||
// Forward decls from Arduino's <Arduino.h> for the inline wrappers below.
|
||||
// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
extern "C" void yield(void);
|
||||
extern "C" void delay(unsigned long ms);
|
||||
extern "C" unsigned long micros(void);
|
||||
extern "C" unsigned long millis(void);
|
||||
// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
|
||||
// Forward decl from <pico/time.h>.
|
||||
extern "C" uint64_t time_us_64(void);
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() {
|
||||
uint32_t ipsr;
|
||||
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
|
||||
return ipsr != 0;
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { ::yield(); }
|
||||
__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
__attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); }
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis<uint64_t>(::time_us_64()); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_RP2040
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#define IRAM_ATTR
|
||||
#define PROGMEM
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
/// Zephyr/nRF52: not currently consulted — wake path is platform-specific.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
|
||||
|
||||
void yield();
|
||||
void delay(uint32_t ms);
|
||||
uint32_t micros();
|
||||
uint32_t millis();
|
||||
uint64_t millis_64();
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ZEPHYR
|
||||
+7
-40
@@ -20,6 +20,7 @@
|
||||
#include <strings.h>
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/time_conversion.h"
|
||||
|
||||
// Backward compatibility re-export of heap-allocating helpers.
|
||||
// These functions have moved to alloc_helpers.h. External components should
|
||||
@@ -833,43 +834,9 @@ template<std::integral T> constexpr uint32_t fnv1a_hash_extend(uint32_t hash, T
|
||||
constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); }
|
||||
inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); }
|
||||
|
||||
/// Convert a 64-bit microsecond count to milliseconds without calling
|
||||
/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz).
|
||||
///
|
||||
/// Returns uint32_t by default (for millis()), or uint64_t when requested
|
||||
/// (for millis_64()). The only difference is whether hi * Q is truncated
|
||||
/// to 32 bits or widened to 64.
|
||||
///
|
||||
/// On 32-bit targets, GCC does not optimize 64-bit constant division into a
|
||||
/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3
|
||||
/// (free divide-by-8), then use the Euclidean division identity to decompose
|
||||
/// the remaining 64-bit divide-by-125 into a single 32-bit division:
|
||||
///
|
||||
/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers]
|
||||
/// 2^32 = Q * 125 + R (34359738 * 125 + 46)
|
||||
/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125
|
||||
///
|
||||
/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal
|
||||
/// (mulhu + shift), so no division instruction is emitted.
|
||||
///
|
||||
/// Safe for us up to ~3.2e18 (~101,700 years of microseconds).
|
||||
///
|
||||
/// See: https://en.wikipedia.org/wiki/Euclidean_division
|
||||
/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html
|
||||
template<typename ReturnT = uint32_t> inline constexpr ESPHOME_ALWAYS_INLINE ReturnT micros_to_millis(uint64_t us) {
|
||||
constexpr uint32_t d = 125U;
|
||||
constexpr uint32_t q = static_cast<uint32_t>((1ULL << 32) / d); // 34359738
|
||||
constexpr uint32_t r = static_cast<uint32_t>((1ULL << 32) % d); // 46
|
||||
// 1000 = 8 * 125; divide-by-8 is a free shift
|
||||
uint64_t x = us >> 3;
|
||||
uint32_t lo = static_cast<uint32_t>(x);
|
||||
uint32_t hi = static_cast<uint32_t>(x >> 32);
|
||||
// Combine remainder term: hi * (2^32 % 125) + lo
|
||||
uint32_t adj = hi * r + lo;
|
||||
// If adj overflowed, the true value is 2^32 + adj; apply the identity again
|
||||
// static_cast<ReturnT>(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q
|
||||
return static_cast<ReturnT>(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d);
|
||||
}
|
||||
// micros_to_millis<>() lives in its own lightweight header so hal.h can pull it
|
||||
// in for inline millis_64() without forcing every TU that includes hal.h to
|
||||
// also include the rest of helpers.h.
|
||||
|
||||
/// Return a random 32-bit unsigned integer.
|
||||
/// Not thread-safe. Must only be called from the main loop.
|
||||
@@ -1666,7 +1633,7 @@ template<typename... Ts> struct Callback<void(Ts...)> {
|
||||
void *ctx_{nullptr};
|
||||
|
||||
/// Invoke the callback. Only valid on Callbacks created via create(), never on default-constructed instances.
|
||||
void call(Ts... args) const { this->fn_(this->ctx_, args...); }
|
||||
void call(Ts... args) const { this->fn_(this->ctx_, std::forward<Ts>(args)...); }
|
||||
|
||||
/// Create from any callable. Small trivially-copyable callables (like [this] lambdas)
|
||||
/// are stored inline in the ctx pointer without heap allocation.
|
||||
@@ -1742,7 +1709,7 @@ template<typename... Ts> class CallbackManager<void(Ts...)> {
|
||||
template<typename F> void add(F &&callback) { this->add_(CbType::create(std::forward<F>(callback))); }
|
||||
|
||||
/// Call all callbacks in this manager.
|
||||
inline void ESPHOME_ALWAYS_INLINE call(Ts... args) {
|
||||
inline void ESPHOME_ALWAYS_INLINE call(const Ts &...args) {
|
||||
if (this->size_ != 0) {
|
||||
for (auto *it = this->data_, *end = it + this->size_; it != end; ++it) {
|
||||
it->call(args...);
|
||||
@@ -1752,7 +1719,7 @@ template<typename... Ts> class CallbackManager<void(Ts...)> {
|
||||
uint16_t size() const { return this->size_; }
|
||||
|
||||
/// Call all callbacks in this manager.
|
||||
void operator()(Ts... args) { this->call(args...); }
|
||||
void operator()(const Ts &...args) { this->call(args...); }
|
||||
|
||||
protected:
|
||||
template<typename...> friend class LazyCallbackManager;
|
||||
|
||||
@@ -26,25 +26,23 @@ extern "C" {
|
||||
struct lwip_sock *esphome_lwip_get_sock(int fd);
|
||||
|
||||
/// Check if a cached LwIP socket has data ready via unlocked hint read of rcvevent.
|
||||
/// This avoids lwIP core lock contention between the main loop (CPU0) and
|
||||
/// streaming/networking work (CPU1). Correctness is preserved because callers
|
||||
/// already handle EWOULDBLOCK on nonblocking sockets — a stale hint simply causes
|
||||
/// a harmless retry on the next loop iteration. In practice, stale reads have not
|
||||
/// been observed across multi-day testing, but the design does not depend on that.
|
||||
///
|
||||
/// The sock pointer must have been obtained from esphome_lwip_get_sock() and must
|
||||
/// remain valid (caller owns socket lifetime — no concurrent close).
|
||||
/// Hot path: inlined volatile 16-bit load — no function call overhead.
|
||||
/// Uses offset-based access because lwip/priv/sockets_priv.h conflicts with C++.
|
||||
/// On ESPHOME_THREAD_MULTI_ATOMICS builds, the caller must run on the main
|
||||
/// loop task after Application::loop's per-iter std::atomic_thread_fence
|
||||
/// (memory_order_acquire); that fence pairs with the TCP/IP thread's
|
||||
/// SYS_ARCH_UNPROTECT release, so a plain load suffices and avoids the
|
||||
/// per-call `memw` that volatile would emit on Xtensa under default
|
||||
/// -mserialize-volatile. Without atomics (e.g. BK72xx), the fence is skipped
|
||||
/// and the volatile load provides ordering on its own.
|
||||
/// Stale reads are harmless either way: the hooked event_callback
|
||||
/// xTaskNotifyGives on RCVPLUS, so the next iteration re-snapshots and
|
||||
/// ulTaskNotifyTake never loses a wake.
|
||||
/// The offset and size are verified at compile time in lwip_fast_select.c.
|
||||
static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) {
|
||||
// Unlocked hint read — no lwIP core lock needed.
|
||||
// volatile prevents the compiler from caching/reordering this cross-thread read.
|
||||
// The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a
|
||||
// FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value
|
||||
// is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on
|
||||
// Xtensa/RISC-V/ARM and cannot produce torn values.
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
return *(int16_t *) ((char *) sock + (int) ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET) > 0;
|
||||
#else
|
||||
return *(volatile int16_t *) ((char *) sock + (int) ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET) > 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Hook a socket's netconn callback to notify the main loop task on receive events.
|
||||
|
||||
@@ -772,6 +772,8 @@ Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() {
|
||||
// Helper to execute a scheduler item
|
||||
uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
|
||||
App.set_current_component(item->component);
|
||||
// Freshen so callbacks reading App.get_loop_component_start_time() see this item's dispatch time.
|
||||
App.set_loop_component_start_time_(now);
|
||||
WarnIfComponentBlockingGuard guard{item->component, now};
|
||||
item->callback();
|
||||
uint32_t end = guard.finish();
|
||||
|
||||
@@ -22,8 +22,8 @@ static const char *const TAG = "time_64";
|
||||
|
||||
#ifdef ESPHOME_THREAD_SINGLE
|
||||
// Storage for Millis64Impl inline compute() — defined here so all TUs share one copy.
|
||||
uint32_t Millis64Impl::last_millis_{0};
|
||||
uint16_t Millis64Impl::millis_major_{0};
|
||||
uint32_t Millis64Impl::last_millis{0};
|
||||
uint16_t Millis64Impl::millis_major{0};
|
||||
#else
|
||||
|
||||
uint64_t Millis64Impl::compute(uint32_t now) {
|
||||
|
||||
+10
-10
@@ -6,8 +6,6 @@
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
class Scheduler;
|
||||
@@ -21,26 +19,28 @@ class Millis64Impl {
|
||||
|
||||
#ifdef ESPHOME_THREAD_SINGLE
|
||||
// Storage defined in time_64.cpp — declared here so the inline body can access them.
|
||||
static uint32_t last_millis_;
|
||||
static uint16_t millis_major_;
|
||||
static uint32_t last_millis;
|
||||
static uint16_t millis_major;
|
||||
|
||||
static inline uint64_t ESPHOME_ALWAYS_INLINE compute(uint32_t now) {
|
||||
// Raw __attribute__((always_inline)) (not ESPHOME_ALWAYS_INLINE) so this
|
||||
// header does not need to pull helpers.h.
|
||||
static inline uint64_t __attribute__((always_inline)) compute(uint32_t now) {
|
||||
// Half the 32-bit range - used to detect rollovers vs normal time progression
|
||||
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
|
||||
|
||||
// Single-core platforms have no concurrency, so this is a simple implementation
|
||||
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
|
||||
uint16_t major = millis_major_;
|
||||
uint32_t last = last_millis_;
|
||||
uint16_t major = millis_major;
|
||||
uint32_t last = last_millis;
|
||||
|
||||
// Check for rollover
|
||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||
millis_major_++;
|
||||
millis_major++;
|
||||
major++;
|
||||
last_millis_ = now;
|
||||
last_millis = now;
|
||||
} else if (now > last) {
|
||||
// Only update if time moved forward
|
||||
last_millis_ = now;
|
||||
last_millis = now;
|
||||
}
|
||||
|
||||
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Convert a 64-bit microsecond count to milliseconds without calling
|
||||
/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz).
|
||||
///
|
||||
/// Returns uint32_t by default (for millis()), or uint64_t when requested
|
||||
/// (for millis_64()). The only difference is whether hi * Q is truncated
|
||||
/// to 32 bits or widened to 64.
|
||||
///
|
||||
/// On 32-bit targets, GCC does not optimize 64-bit constant division into a
|
||||
/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3
|
||||
/// (free divide-by-8), then use the Euclidean division identity to decompose
|
||||
/// the remaining 64-bit divide-by-125 into a single 32-bit division:
|
||||
///
|
||||
/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers]
|
||||
/// 2^32 = Q * 125 + R (34359738 * 125 + 46)
|
||||
/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125
|
||||
///
|
||||
/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal
|
||||
/// (mulhu + shift), so no division instruction is emitted.
|
||||
///
|
||||
/// Safe for us up to ~3.2e18 (~101,700 years of microseconds).
|
||||
///
|
||||
/// See: https://en.wikipedia.org/wiki/Euclidean_division
|
||||
/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html
|
||||
template<typename ReturnT = uint32_t>
|
||||
__attribute__((always_inline)) inline constexpr ReturnT micros_to_millis(uint64_t us) {
|
||||
constexpr uint32_t d = 125U;
|
||||
constexpr uint32_t q = static_cast<uint32_t>((1ULL << 32) / d); // 34359738
|
||||
constexpr uint32_t r = static_cast<uint32_t>((1ULL << 32) % d); // 46
|
||||
// 1000 = 8 * 125; divide-by-8 is a free shift
|
||||
uint64_t x = us >> 3;
|
||||
uint32_t lo = static_cast<uint32_t>(x);
|
||||
uint32_t hi = static_cast<uint32_t>(x >> 32);
|
||||
// Combine remainder term: hi * (2^32 % 125) + lo
|
||||
uint32_t adj = hi * r + lo;
|
||||
// If adj overflowed, the true value is 2^32 + adj; apply the identity again
|
||||
// static_cast<ReturnT>(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q
|
||||
return static_cast<ReturnT>(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d);
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user