mirror of
https://github.com/esphome/esphome.git
synced 2026-01-19 17:46:23 -07:00
Compare commits
12 Commits
esp32_ble_
...
libretiny_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76b1201c96 | ||
|
|
680e92a226 | ||
|
|
db0b32bfc9 | ||
|
|
21794e28e5 | ||
|
|
728236270c | ||
|
|
01cdc4ed58 | ||
|
|
d6a0c8ffbb | ||
|
|
4cc0f874f7 | ||
|
|
ed58b9372f | ||
|
|
ee2a81923b | ||
|
|
0a1e7ee50b | ||
|
|
4d4283bcfa |
@@ -158,12 +158,14 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_offset_calibration_) {
|
||||
// Initialize flash storage for offset calibrations
|
||||
uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_);
|
||||
uint32_t o_hash = fnv1_hash("_offset_calibration_");
|
||||
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
|
||||
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
|
||||
this->restore_offset_calibrations_();
|
||||
|
||||
// Initialize flash storage for power offset calibrations
|
||||
uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_);
|
||||
uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
|
||||
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
|
||||
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
|
||||
this->restore_power_offset_calibrations_();
|
||||
} else {
|
||||
@@ -183,7 +185,8 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_gain_calibration_) {
|
||||
// Initialize flash storage for gain calibration
|
||||
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_);
|
||||
uint32_t g_hash = fnv1_hash("_gain_calibration_");
|
||||
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
|
||||
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
|
||||
this->restore_gain_calibrations_();
|
||||
|
||||
|
||||
@@ -185,18 +185,16 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
|
||||
return err;
|
||||
}
|
||||
|
||||
std::string url_string = str_lower_case(url);
|
||||
|
||||
if (str_endswith(url_string, ".wav")) {
|
||||
if (str_endswith_ignore_case(url, ".wav")) {
|
||||
file_type = AudioFileType::WAV;
|
||||
}
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
else if (str_endswith(url_string, ".mp3")) {
|
||||
else if (str_endswith_ignore_case(url, ".mp3")) {
|
||||
file_type = AudioFileType::MP3;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
else if (str_endswith(url_string, ".flac")) {
|
||||
else if (str_endswith_ignore_case(url, ".flac")) {
|
||||
file_type = AudioFileType::FLAC;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -135,8 +135,8 @@ void BluetoothConnection::loop() {
|
||||
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
|
||||
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
|
||||
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
|
||||
if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ void BLEClientBase::loop() {
|
||||
this->set_state(espbt::ClientState::INIT);
|
||||
return;
|
||||
}
|
||||
if (this->state() == espbt::ClientState::INIT) {
|
||||
if (this->state_ == espbt::ClientState::INIT) {
|
||||
auto ret = esp_ble_gattc_app_register(this->app_id);
|
||||
if (ret) {
|
||||
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
|
||||
@@ -60,7 +60,7 @@ void BLEClientBase::loop() {
|
||||
}
|
||||
// If idle, we can disable the loop as connect()
|
||||
// will enable it again when a connection is needed.
|
||||
else if (this->state() == espbt::ClientState::IDLE) {
|
||||
else if (this->state_ == espbt::ClientState::IDLE) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
||||
return false;
|
||||
if (this->address_ == 0 || device.address_uint64() != this->address_)
|
||||
return false;
|
||||
if (this->state() != espbt::ClientState::IDLE)
|
||||
if (this->state_ != espbt::ClientState::IDLE)
|
||||
return false;
|
||||
|
||||
this->log_event_("Found device");
|
||||
@@ -102,10 +102,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
||||
|
||||
void BLEClientBase::connect() {
|
||||
// Prevent duplicate connection attempts
|
||||
if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED ||
|
||||
this->state() == espbt::ClientState::ESTABLISHED) {
|
||||
if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
|
||||
this->state_ == espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
|
||||
espbt::client_state_to_string(this->state()));
|
||||
espbt::client_state_to_string(this->state_));
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
|
||||
@@ -133,12 +133,12 @@ void BLEClientBase::connect() {
|
||||
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
|
||||
|
||||
void BLEClientBase::disconnect() {
|
||||
if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) {
|
||||
if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
|
||||
espbt::client_state_to_string(this->state()));
|
||||
espbt::client_state_to_string(this->state_));
|
||||
return;
|
||||
}
|
||||
if (this->state() == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
||||
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
||||
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
|
||||
this->address_str_);
|
||||
this->want_disconnect_ = true;
|
||||
@@ -150,7 +150,7 @@ void BLEClientBase::disconnect() {
|
||||
void BLEClientBase::unconditional_disconnect() {
|
||||
// Disconnect without checking the state.
|
||||
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
|
||||
if (this->state() == espbt::ClientState::DISCONNECTING) {
|
||||
if (this->state_ == espbt::ClientState::DISCONNECTING) {
|
||||
this->log_error_("Already disconnecting");
|
||||
return;
|
||||
}
|
||||
@@ -170,7 +170,7 @@ void BLEClientBase::unconditional_disconnect() {
|
||||
this->log_gattc_warning_("esp_ble_gattc_close", err);
|
||||
}
|
||||
|
||||
if (this->state() == espbt::ClientState::DISCOVERED) {
|
||||
if (this->state_ == espbt::ClientState::DISCOVERED) {
|
||||
this->set_address(0);
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
} else {
|
||||
@@ -295,18 +295,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
|
||||
// error, if the error occurred at the BTA/GATT layer. This can result in the event
|
||||
// arriving after we've already transitioned to IDLE state.
|
||||
if (this->state() == espbt::ClientState::IDLE) {
|
||||
if (this->state_ == espbt::ClientState::IDLE) {
|
||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
|
||||
this->address_str_, param->open.status);
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->state() != espbt::ClientState::CONNECTING) {
|
||||
if (this->state_ != espbt::ClientState::CONNECTING) {
|
||||
// This should not happen but lets log it in case it does
|
||||
// because it means we have a bad assumption about how the
|
||||
// ESP BT stack works.
|
||||
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
|
||||
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
|
||||
this->address_str_, espbt::client_state_to_string(this->state_), param->open.status);
|
||||
}
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
this->log_gattc_warning_("Connection open", param->open.status);
|
||||
@@ -327,7 +327,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
// Cached connections already connected with medium parameters, no update needed
|
||||
// only set our state, subclients might have more stuff to do yet.
|
||||
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
// For V3_WITHOUT_CACHE, we already set fast params before connecting
|
||||
@@ -356,7 +356,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
return false;
|
||||
// Check if we were disconnected while waiting for service discovery
|
||||
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
|
||||
this->state() == espbt::ClientState::CONNECTED) {
|
||||
this->state_ == espbt::ClientState::CONNECTED) {
|
||||
this->log_warning_("Remote closed during discovery");
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
|
||||
@@ -433,7 +433,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
#endif
|
||||
}
|
||||
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
|
||||
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_DESCR_EVT: {
|
||||
|
||||
@@ -44,7 +44,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
void unconditional_disconnect();
|
||||
void release_services();
|
||||
|
||||
bool connected() { return this->state() == espbt::ClientState::ESTABLISHED; }
|
||||
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
|
||||
|
||||
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
|
||||
|
||||
|
||||
@@ -105,13 +105,15 @@ void ESP32BLETracker::loop() {
|
||||
}
|
||||
|
||||
// Check for scan timeout - moved here from scheduler to avoid false reboots
|
||||
// when the loop is blocked. This must run every iteration for safety.
|
||||
// when the loop is blocked
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
switch (this->scan_timeout_state_) {
|
||||
case ScanTimeoutState::MONITORING: {
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
uint32_t timeout_ms = this->scan_duration_ * 2000;
|
||||
// Robust time comparison that handles rollover correctly
|
||||
// This works because unsigned arithmetic wraps around predictably
|
||||
if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) {
|
||||
if ((now - this->scan_start_time_) > timeout_ms) {
|
||||
// First time we've seen the timeout exceeded - wait one more loop iteration
|
||||
// This ensures all components have had a chance to process pending events
|
||||
// This is because esp32_ble may not have run yet and called
|
||||
@@ -126,31 +128,13 @@ void ESP32BLETracker::loop() {
|
||||
ESP_LOGE(TAG, "Scan never terminated, rebooting");
|
||||
App.reboot();
|
||||
break;
|
||||
|
||||
case ScanTimeoutState::INACTIVE:
|
||||
// This case should be unreachable - scanner and timeout states are always synchronized
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fast path: skip expensive client state counting and processing
|
||||
// if no state has changed since last loop iteration.
|
||||
//
|
||||
// How state changes ensure we reach the code below:
|
||||
// - handle_scanner_failure_(): scanner_state_ becomes FAILED via set_scanner_state_(), or
|
||||
// scan_set_param_failed_ requires scanner_state_==RUNNING which can only be reached via
|
||||
// set_scanner_state_(RUNNING) in gap_scan_start_complete_() (scan params are set during
|
||||
// STARTING, not RUNNING, so version is always incremented before this condition is true)
|
||||
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
|
||||
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
|
||||
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
|
||||
//
|
||||
// All conditions that affect the logic below are tied to state changes that increment
|
||||
// state_version_, so the fast path is safe.
|
||||
if (this->state_version_ == this->last_processed_version_) {
|
||||
return;
|
||||
}
|
||||
this->last_processed_version_ = this->state_version_;
|
||||
|
||||
// State changed - do full processing
|
||||
ClientStateCounts counts = this->count_client_states_();
|
||||
if (counts != this->client_state_counts_) {
|
||||
this->client_state_counts_ = counts;
|
||||
@@ -158,7 +142,6 @@ void ESP32BLETracker::loop() {
|
||||
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
|
||||
}
|
||||
|
||||
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
|
||||
if (this->scanner_state_ == ScannerState::FAILED ||
|
||||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
||||
this->handle_scanner_failure_();
|
||||
@@ -177,8 +160,6 @@ void ESP32BLETracker::loop() {
|
||||
|
||||
*/
|
||||
|
||||
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
|
||||
// all clients are idle (their state changes increment version when they finish)
|
||||
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
this->update_coex_preference_(false);
|
||||
@@ -187,9 +168,8 @@ void ESP32BLETracker::loop() {
|
||||
this->start_scan_(false); // first = false
|
||||
}
|
||||
}
|
||||
// Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()),
|
||||
// or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE).
|
||||
// All these trigger state_version_ increment, so we'll process and check promotion eligibility.
|
||||
// If there is a discovered client and no connecting
|
||||
// clients, then promote the discovered client to ready to connect.
|
||||
// We check both RUNNING and IDLE states because:
|
||||
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
|
||||
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
|
||||
@@ -256,7 +236,6 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
// Start timeout monitoring in loop() instead of using scheduler
|
||||
// This prevents false reboots when the loop is blocked
|
||||
this->scan_start_time_ = App.get_loop_component_start_time();
|
||||
this->scan_timeout_ms_ = this->scan_duration_ * 2000;
|
||||
this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
|
||||
|
||||
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
|
||||
@@ -274,10 +253,6 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
client->app_id = ++this->app_id_;
|
||||
// Give client a pointer to our state_version_ so it can notify us of state changes.
|
||||
// This enables loop() fast-path optimization - we skip expensive work when no state changed.
|
||||
// Safe because ESP32BLETracker (singleton) outlives all registered clients.
|
||||
client->set_tracker_state_version(&this->state_version_);
|
||||
this->clients_.push_back(client);
|
||||
this->recalculate_advertisement_parser_types();
|
||||
#endif
|
||||
@@ -407,7 +382,6 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
|
||||
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
|
||||
this->scanner_state_ = state;
|
||||
this->state_version_++;
|
||||
for (auto *listener : this->scanner_state_listeners_) {
|
||||
listener->on_scanner_state(state);
|
||||
}
|
||||
|
||||
@@ -216,19 +216,6 @@ enum class ConnectionType : uint8_t {
|
||||
V3_WITHOUT_CACHE
|
||||
};
|
||||
|
||||
/// Base class for BLE GATT clients that connect to remote devices.
|
||||
///
|
||||
/// State Change Tracking Design:
|
||||
/// -----------------------------
|
||||
/// ESP32BLETracker::loop() needs to know when client states change to avoid
|
||||
/// expensive polling. Rather than checking all clients every iteration (~7000/min),
|
||||
/// we use a version counter owned by ESP32BLETracker that clients increment on
|
||||
/// state changes. The tracker compares versions to skip work when nothing changed.
|
||||
///
|
||||
/// Ownership: ESP32BLETracker owns state_version_. Clients hold a non-owning
|
||||
/// pointer (tracker_state_version_) set during register_client(). Clients
|
||||
/// increment the counter through this pointer when their state changes.
|
||||
/// The pointer may be null if the client is not registered with a tracker.
|
||||
class ESPBTClient : public ESPBTDeviceListener {
|
||||
public:
|
||||
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
@@ -238,49 +225,26 @@ class ESPBTClient : public ESPBTDeviceListener {
|
||||
virtual void disconnect() = 0;
|
||||
bool disconnect_pending() const { return this->want_disconnect_; }
|
||||
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
|
||||
|
||||
/// Set the client state with IDLE handling (clears want_disconnect_).
|
||||
/// Notifies the tracker of state change for loop optimization.
|
||||
virtual void set_state(ClientState st) {
|
||||
this->set_state_internal_(st);
|
||||
this->state_ = st;
|
||||
if (st == ClientState::IDLE) {
|
||||
this->want_disconnect_ = false;
|
||||
}
|
||||
}
|
||||
ClientState state() const { return this->state_; }
|
||||
|
||||
/// Called by ESP32BLETracker::register_client() to enable state change notifications.
|
||||
/// The pointer must remain valid for the lifetime of the client (guaranteed since
|
||||
/// ESP32BLETracker is a singleton that outlives all clients).
|
||||
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
|
||||
ClientState state() const { return state_; }
|
||||
|
||||
// Memory optimized layout
|
||||
uint8_t app_id; // App IDs are small integers assigned sequentially
|
||||
|
||||
protected:
|
||||
/// Set state without IDLE handling - use for direct state transitions.
|
||||
/// Increments the tracker's state version counter to signal that loop()
|
||||
/// should do full processing on the next iteration.
|
||||
void set_state_internal_(ClientState st) {
|
||||
this->state_ = st;
|
||||
// Notify tracker that state changed (tracker_state_version_ is owned by ESP32BLETracker)
|
||||
if (this->tracker_state_version_ != nullptr) {
|
||||
(*this->tracker_state_version_)++;
|
||||
}
|
||||
}
|
||||
|
||||
// Group 1: 1-byte types
|
||||
ClientState state_{ClientState::INIT};
|
||||
// want_disconnect_ is set to true when a disconnect is requested
|
||||
// while the client is connecting. This is used to disconnect the
|
||||
// client as soon as we get the connection id (conn_id_) from the
|
||||
// ESP_GATTC_OPEN_EVT event.
|
||||
bool want_disconnect_{false};
|
||||
|
||||
private:
|
||||
ClientState state_{ClientState::INIT};
|
||||
/// Non-owning pointer to ESP32BLETracker::state_version_. When this client's
|
||||
/// state changes, we increment the tracker's counter to signal that loop()
|
||||
/// should perform full processing. Null if client not registered with tracker.
|
||||
uint8_t *tracker_state_version_{nullptr};
|
||||
// 2 bytes used, 2 bytes padding
|
||||
};
|
||||
|
||||
class ESP32BLETracker : public Component,
|
||||
@@ -416,16 +380,6 @@ class ESP32BLETracker : public Component,
|
||||
// Group 4: 1-byte types (enums, uint8_t, bool)
|
||||
uint8_t app_id_{0};
|
||||
uint8_t scan_start_fail_count_{0};
|
||||
/// Version counter for loop() fast-path optimization. Incremented when:
|
||||
/// - Scanner state changes (via set_scanner_state_())
|
||||
/// - Any registered client's state changes (clients hold pointer to this counter)
|
||||
/// Owned by this class; clients receive non-owning pointer via register_client().
|
||||
/// When loop() sees state_version_ == last_processed_version_, it skips expensive
|
||||
/// client state counting and takes the fast path (just timeout check + return).
|
||||
uint8_t state_version_{0};
|
||||
/// Last state_version_ value when loop() did full processing. Compared against
|
||||
/// state_version_ to detect if any state changed since last iteration.
|
||||
uint8_t last_processed_version_{0};
|
||||
ScannerState scanner_state_{ScannerState::IDLE};
|
||||
bool scan_continuous_;
|
||||
bool scan_active_;
|
||||
@@ -442,8 +396,6 @@ class ESP32BLETracker : public Component,
|
||||
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
|
||||
};
|
||||
uint32_t scan_start_time_{0};
|
||||
/// Precomputed timeout value: scan_duration_ * 2000
|
||||
uint32_t scan_timeout_ms_{0};
|
||||
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
|
||||
};
|
||||
|
||||
|
||||
@@ -271,24 +271,31 @@ class ServerRegister {
|
||||
|
||||
// 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:
|
||||
return std::to_string(static_cast<uint64_t>(value));
|
||||
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:
|
||||
return std::to_string(value);
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
case SensorValueType::FP32_R:
|
||||
case SensorValueType::FP32:
|
||||
return str_sprintf("%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
|
||||
return buf;
|
||||
default:
|
||||
return std::to_string(value);
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,20 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
|
||||
while ((items_left > 0) && index < data.size()) {
|
||||
uint8_t b = data[index];
|
||||
switch (this->encode_) {
|
||||
case RawEncoding::HEXBYTES:
|
||||
output_str += str_snprintf("%02x", 2, b);
|
||||
case RawEncoding::HEXBYTES: {
|
||||
// max 3: 2 hex digits + null
|
||||
char hex_buf[3];
|
||||
snprintf(hex_buf, sizeof(hex_buf), "%02x", b);
|
||||
output_str += hex_buf;
|
||||
break;
|
||||
case RawEncoding::COMMA:
|
||||
output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b);
|
||||
}
|
||||
case RawEncoding::COMMA: {
|
||||
// max 5: optional ','(1) + uint8(3) + null, for both ",%d" and "%d"
|
||||
char dec_buf[5];
|
||||
snprintf(dec_buf, sizeof(dec_buf), index != this->offset ? ",%d" : "%d", b);
|
||||
output_str += dec_buf;
|
||||
break;
|
||||
}
|
||||
case RawEncoding::ANSI:
|
||||
if (b < 0x20)
|
||||
break;
|
||||
|
||||
@@ -43,6 +43,14 @@ namespace network {
|
||||
/// Buffer size for IP address string (IPv6 max: 39 chars + null)
|
||||
static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40;
|
||||
|
||||
/// Lowercase hex digits in IP address string (A-F -> a-f for IPv6 per RFC 5952)
|
||||
inline void lowercase_ip_str(char *buf) {
|
||||
for (char *p = buf; *p; ++p) {
|
||||
if (*p >= 'A' && *p <= 'F')
|
||||
*p += 32;
|
||||
}
|
||||
}
|
||||
|
||||
struct IPAddress {
|
||||
public:
|
||||
#ifdef USE_HOST
|
||||
@@ -52,10 +60,15 @@ struct IPAddress {
|
||||
}
|
||||
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
|
||||
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
|
||||
std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); }
|
||||
std::string str() const {
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
this->str_to(buf);
|
||||
return buf;
|
||||
}
|
||||
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
|
||||
char *str_to(char *buf) const {
|
||||
return const_cast<char *>(inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE));
|
||||
inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
|
||||
return buf; // IPv4 only, no hex letters to lowercase
|
||||
}
|
||||
#else
|
||||
IPAddress() { ip_addr_set_zero(&ip_addr_); }
|
||||
@@ -134,9 +147,18 @@ struct IPAddress {
|
||||
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
|
||||
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
|
||||
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
|
||||
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
|
||||
std::string str() const {
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
this->str_to(buf);
|
||||
return buf;
|
||||
}
|
||||
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
|
||||
char *str_to(char *buf) const { return ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE); }
|
||||
/// Output is lowercased per RFC 5952 (IPv6 hex digits a-f).
|
||||
char *str_to(char *buf) const {
|
||||
ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
|
||||
lowercase_ip_str(buf);
|
||||
return buf;
|
||||
}
|
||||
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
|
||||
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
|
||||
IPAddress &operator+=(uint8_t increase) {
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "pipsolar.output";
|
||||
|
||||
void PipsolarOutput::write_state(float state) {
|
||||
char tmp[10];
|
||||
sprintf(tmp, this->set_command_.c_str(), state);
|
||||
snprintf(tmp, sizeof(tmp), this->set_command_, state);
|
||||
|
||||
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
|
||||
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
|
||||
|
||||
@@ -15,13 +15,15 @@ class PipsolarOutput : public output::FloatOutput {
|
||||
public:
|
||||
PipsolarOutput() {}
|
||||
void set_parent(Pipsolar *parent) { this->parent_ = parent; }
|
||||
void set_set_command(const std::string &command) { this->set_command_ = command; };
|
||||
void set_set_command(const char *command) { this->set_command_ = command; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_set_command(const std::string &command) = delete;
|
||||
void set_possible_values(std::vector<float> possible_values) { this->possible_values_ = std::move(possible_values); }
|
||||
void set_value(float value) { this->write_state(value); };
|
||||
void set_value(float value) { this->write_state(value); }
|
||||
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
std::string set_command_;
|
||||
const char *set_command_{nullptr};
|
||||
Pipsolar *parent_;
|
||||
std::vector<float> possible_values_;
|
||||
};
|
||||
|
||||
@@ -9,14 +9,9 @@ static const char *const TAG = "pipsolar.switch";
|
||||
|
||||
void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
|
||||
void PipsolarSwitch::write_state(bool state) {
|
||||
if (state) {
|
||||
if (!this->on_command_.empty()) {
|
||||
this->parent_->queue_command(this->on_command_);
|
||||
}
|
||||
} else {
|
||||
if (!this->off_command_.empty()) {
|
||||
this->parent_->queue_command(this->off_command_);
|
||||
}
|
||||
const char *command = state ? this->on_command_ : this->off_command_;
|
||||
if (command != nullptr) {
|
||||
this->parent_->queue_command(command);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,18 @@ namespace pipsolar {
|
||||
class Pipsolar;
|
||||
class PipsolarSwitch : public switch_::Switch, public Component {
|
||||
public:
|
||||
void set_parent(Pipsolar *parent) { this->parent_ = parent; };
|
||||
void set_on_command(const std::string &command) { this->on_command_ = command; };
|
||||
void set_off_command(const std::string &command) { this->off_command_ = command; };
|
||||
void set_parent(Pipsolar *parent) { this->parent_ = parent; }
|
||||
void set_on_command(const char *command) { this->on_command_ = command; }
|
||||
void set_off_command(const char *command) { this->off_command_ = command; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_on_command(const std::string &command) = delete;
|
||||
void set_off_command(const std::string &command) = delete;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
std::string on_command_;
|
||||
std::string off_command_;
|
||||
const char *on_command_{nullptr};
|
||||
const char *off_command_{nullptr};
|
||||
Pipsolar *parent_;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||
void set_parent(Sun *parent) { parent_ = parent; }
|
||||
void set_elevation(double elevation) { elevation_ = elevation; }
|
||||
void set_sunrise(bool sunrise) { sunrise_ = sunrise; }
|
||||
void set_format(const std::string &format) { format_ = format; }
|
||||
void set_format(const char *format) { this->format_ = format; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_format(const std::string &format) = delete;
|
||||
|
||||
void update() override {
|
||||
optional<ESPTime> res;
|
||||
@@ -29,14 +31,14 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||
}
|
||||
|
||||
char buf[ESPTime::STRFTIME_BUFFER_SIZE];
|
||||
size_t len = res->strftime_to(buf, this->format_.c_str());
|
||||
size_t len = res->strftime_to(buf, this->format_);
|
||||
this->publish_state(buf, len);
|
||||
}
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
std::string format_{};
|
||||
const char *format_{nullptr};
|
||||
Sun *parent_;
|
||||
double elevation_;
|
||||
bool sunrise_;
|
||||
|
||||
@@ -118,8 +118,7 @@ async def to_code(config):
|
||||
var = await alarm_control_panel.new_alarm_control_panel(config)
|
||||
await cg.register_component(var, config)
|
||||
if CONF_CODES in config:
|
||||
for acode in config[CONF_CODES]:
|
||||
cg.add(var.add_code(acode))
|
||||
cg.add(var.set_codes(config[CONF_CODES]))
|
||||
if CONF_REQUIRES_CODE_TO_ARM in config:
|
||||
cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM]))
|
||||
|
||||
|
||||
@@ -206,7 +206,13 @@ bool TemplateAlarmControlPanel::is_code_valid_(optional<std::string> code) {
|
||||
if (!this->codes_.empty()) {
|
||||
if (code.has_value()) {
|
||||
ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str());
|
||||
return (std::count(this->codes_.begin(), this->codes_.end(), code.value()) == 1);
|
||||
// Use strcmp for const char* comparison
|
||||
const char *code_cstr = code.value().c_str();
|
||||
for (const char *stored_code : this->codes_) {
|
||||
if (strcmp(stored_code, code_cstr) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "No code provided");
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
@@ -86,11 +87,14 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED);
|
||||
#endif
|
||||
|
||||
/** add a code
|
||||
/** Set the codes (from initializer list).
|
||||
*
|
||||
* @param code The code
|
||||
* @param codes The list of valid codes
|
||||
*/
|
||||
void add_code(const std::string &code) { this->codes_.push_back(code); }
|
||||
void set_codes(std::initializer_list<const char *> codes) { this->codes_ = codes; }
|
||||
|
||||
// Deleted overload to catch incorrect std::string usage at compile time
|
||||
void set_codes(std::initializer_list<std::string> codes) = delete;
|
||||
|
||||
/** set requires a code to arm
|
||||
*
|
||||
@@ -155,8 +159,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
uint32_t pending_time_;
|
||||
// the time in trigger
|
||||
uint32_t trigger_time_;
|
||||
// a list of codes
|
||||
std::vector<std::string> codes_;
|
||||
// a list of codes (const char* pointers to string literals in flash)
|
||||
FixedVector<const char *> codes_;
|
||||
// requires a code to arm
|
||||
bool requires_code_to_arm_ = false;
|
||||
bool supports_arm_home_ = false;
|
||||
|
||||
@@ -8,16 +8,23 @@ static const char *const TAG = "template.text";
|
||||
void TemplateText::setup() {
|
||||
if (this->f_.has_value())
|
||||
return;
|
||||
std::string value = this->initial_value_;
|
||||
if (!this->pref_) {
|
||||
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
|
||||
} else {
|
||||
uint32_t key = this->get_preference_hash();
|
||||
key += this->traits.get_min_length() << 2;
|
||||
key += this->traits.get_max_length() << 4;
|
||||
key += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
|
||||
this->pref_->setup(key, value);
|
||||
|
||||
if (this->pref_ == nullptr) {
|
||||
// No restore - use const char* directly, no heap allocation needed
|
||||
if (this->initial_value_ != nullptr && this->initial_value_[0] != '\0') {
|
||||
ESP_LOGD(TAG, "State from initial: %s", this->initial_value_);
|
||||
this->publish_state(this->initial_value_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Need std::string for pref_->setup() to fill from flash
|
||||
std::string value{this->initial_value_ != nullptr ? this->initial_value_ : ""};
|
||||
uint32_t key = this->get_preference_hash();
|
||||
key += this->traits.get_min_length() << 2;
|
||||
key += this->traits.get_max_length() << 4;
|
||||
key += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
|
||||
this->pref_->setup(key, value);
|
||||
if (!value.empty())
|
||||
this->publish_state(value);
|
||||
}
|
||||
|
||||
@@ -70,13 +70,15 @@ class TemplateText final : public text::Text, public PollingComponent {
|
||||
|
||||
Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; }
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void set_initial_value(const std::string &initial_value) { this->initial_value_ = initial_value; }
|
||||
void set_initial_value(const char *initial_value) { this->initial_value_ = initial_value; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_initial_value(const std::string &initial_value) = delete;
|
||||
void set_value_saver(TemplateTextSaverBase *restore_value_saver) { this->pref_ = restore_value_saver; }
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
bool optimistic_ = false;
|
||||
std::string initial_value_;
|
||||
const char *initial_value_{nullptr};
|
||||
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
|
||||
TemplateLambda<std::string> f_{};
|
||||
|
||||
|
||||
@@ -108,8 +108,7 @@ async def to_code(config):
|
||||
cg.add(var.set_broadcast_port(conf_port[CONF_BROADCAST_PORT]))
|
||||
if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255":
|
||||
cg.add(var.set_listen_address(listen_address))
|
||||
for address in config[CONF_ADDRESSES]:
|
||||
cg.add(var.add_address(str(address)))
|
||||
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
|
||||
if on_receive := config.get(CONF_ON_RECEIVE):
|
||||
on_receive = on_receive[0]
|
||||
trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "udp_component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace udp {
|
||||
namespace esphome::udp {
|
||||
|
||||
static const char *const TAG = "udp";
|
||||
|
||||
@@ -95,7 +94,7 @@ void UDPComponent::setup() {
|
||||
// 8266 and RP2040 `Duino
|
||||
for (const auto &address : this->addresses_) {
|
||||
auto ipaddr = IPAddress();
|
||||
ipaddr.fromString(address.c_str());
|
||||
ipaddr.fromString(address);
|
||||
this->ipaddrs_.push_back(ipaddr);
|
||||
}
|
||||
if (this->should_listen_)
|
||||
@@ -130,8 +129,8 @@ void UDPComponent::dump_config() {
|
||||
" Listen Port: %u\n"
|
||||
" Broadcast Port: %u",
|
||||
this->listen_port_, this->broadcast_port_);
|
||||
for (const auto &address : this->addresses_)
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address.c_str());
|
||||
for (const char *address : this->addresses_)
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address);
|
||||
if (this->listen_address_.has_value()) {
|
||||
char addr_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf));
|
||||
@@ -162,7 +161,6 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace udp
|
||||
} // namespace esphome
|
||||
} // namespace esphome::udp
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
#include "esphome/components/socket/socket.h"
|
||||
@@ -9,15 +10,17 @@
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
#include <WiFiUdp.h>
|
||||
#endif
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace udp {
|
||||
namespace esphome::udp {
|
||||
|
||||
static const size_t MAX_PACKET_SIZE = 508;
|
||||
class UDPComponent : public Component {
|
||||
public:
|
||||
void add_address(const char *addr) { this->addresses_.emplace_back(addr); }
|
||||
void set_addresses(std::initializer_list<const char *> addresses) { this->addresses_ = addresses; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_addresses(std::initializer_list<std::string> addresses) = delete;
|
||||
void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); }
|
||||
void set_listen_port(uint16_t port) { this->listen_port_ = port; }
|
||||
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
||||
@@ -49,11 +52,10 @@ class UDPComponent : public Component {
|
||||
std::vector<IPAddress> ipaddrs_{};
|
||||
WiFiUDP udp_client_{};
|
||||
#endif
|
||||
std::vector<std::string> addresses_{};
|
||||
FixedVector<const char *> addresses_{};
|
||||
|
||||
optional<network::IPAddress> listen_address_{};
|
||||
};
|
||||
|
||||
} // namespace udp
|
||||
} // namespace esphome
|
||||
} // namespace esphome::udp
|
||||
#endif
|
||||
|
||||
@@ -4,19 +4,13 @@
|
||||
/// @details The classes declared in this file can be used by the Weikai family
|
||||
|
||||
#include "weikai.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace weikai {
|
||||
|
||||
static const char *const TAG = "weikai";
|
||||
|
||||
/// @brief convert an int to binary representation as C++ std::string
|
||||
/// @param val integer to convert
|
||||
/// @return a std::string
|
||||
inline std::string i2s(uint8_t val) { return std::bitset<8>(val).to_string(); }
|
||||
/// Convert std::string to C string
|
||||
#define I2S2CS(val) (i2s(val).c_str())
|
||||
|
||||
/// @brief measure the time elapsed between two calls
|
||||
/// @param last_time time of the previous call
|
||||
/// @return the elapsed time in milliseconds
|
||||
@@ -170,17 +164,18 @@ void WeikaiComponent::test_gpio_input_() {
|
||||
static bool init_input{false};
|
||||
static uint8_t state{0};
|
||||
uint8_t value;
|
||||
char bin_buf[9]; // 8 binary digits + null
|
||||
if (!init_input) {
|
||||
init_input = true;
|
||||
// set all pins in input mode
|
||||
this->reg(WKREG_GPDIR, 0) = 0x00;
|
||||
ESP_LOGI(TAG, "initializing all pins to input mode");
|
||||
state = this->reg(WKREG_GPDAT, 0);
|
||||
ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, I2S2CS(state));
|
||||
ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, format_bin_to(bin_buf, state));
|
||||
}
|
||||
value = this->reg(WKREG_GPDAT, 0);
|
||||
if (value != state) {
|
||||
ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, I2S2CS(value));
|
||||
ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, format_bin_to(bin_buf, value));
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
@@ -188,6 +183,7 @@ void WeikaiComponent::test_gpio_input_() {
|
||||
void WeikaiComponent::test_gpio_output_() {
|
||||
static bool init_output{false};
|
||||
static uint8_t state{0};
|
||||
char bin_buf[9]; // 8 binary digits + null
|
||||
if (!init_output) {
|
||||
init_output = true;
|
||||
// set all pins in output mode
|
||||
@@ -198,7 +194,7 @@ void WeikaiComponent::test_gpio_output_() {
|
||||
}
|
||||
state = ~state;
|
||||
this->reg(WKREG_GPDAT, 0) = state;
|
||||
ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, I2S2CS(state));
|
||||
ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, format_bin_to(bin_buf, state));
|
||||
delay(100); // NOLINT
|
||||
}
|
||||
#endif
|
||||
@@ -208,7 +204,9 @@ void WeikaiComponent::test_gpio_output_() {
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
bool WeikaiComponent::read_pin_val_(uint8_t pin) {
|
||||
this->input_state_ = this->reg(WKREG_GPDAT, 0);
|
||||
ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin), I2S2CS(input_state_));
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin),
|
||||
format_bin_to(bin_buf, this->input_state_));
|
||||
return this->input_state_ & (1 << pin);
|
||||
}
|
||||
|
||||
@@ -218,7 +216,9 @@ void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
|
||||
} else {
|
||||
this->output_state_ &= ~(1 << pin);
|
||||
}
|
||||
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value), I2S2CS(this->output_state_));
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
|
||||
format_bin_to(bin_buf, this->output_state_));
|
||||
this->reg(WKREG_GPDAT, 0) = this->output_state_;
|
||||
}
|
||||
|
||||
@@ -232,7 +232,8 @@ void WeikaiComponent::set_pin_direction_(uint8_t pin, gpio::Flags flags) {
|
||||
ESP_LOGE(TAG, "pin %d direction invalid", pin);
|
||||
}
|
||||
}
|
||||
ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, I2S2CS(this->pin_config_));
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, format_bin_to(bin_buf, this->pin_config_));
|
||||
this->reg(WKREG_GPDIR, 0) = this->pin_config_; // TODO check ~
|
||||
}
|
||||
|
||||
@@ -241,7 +242,6 @@ void WeikaiGPIOPin::setup() {
|
||||
flags_ == gpio::FLAG_INPUT ? "Input"
|
||||
: this->flags_ == gpio::FLAG_OUTPUT ? "Output"
|
||||
: "NOT SPECIFIED");
|
||||
// ESP_LOGCONFIG(TAG, "Setting GPIO pins mode to '%s' %02X", I2S2CS(this->flags_), this->flags_);
|
||||
this->pin_mode(this->flags_);
|
||||
}
|
||||
|
||||
@@ -297,8 +297,9 @@ void WeikaiChannel::set_line_param_() {
|
||||
break; // no parity 000x
|
||||
}
|
||||
this->reg(WKREG_LCR) = lcr; // write LCR
|
||||
char bin_buf[9];
|
||||
ESP_LOGV(TAG, " line config: %d data_bits, %d stop_bits, parity %s register [%s]", this->data_bits_,
|
||||
this->stop_bits_, p2s(this->parity_), I2S2CS(lcr));
|
||||
this->stop_bits_, p2s(this->parity_), format_bin_to(bin_buf, lcr));
|
||||
}
|
||||
|
||||
void WeikaiChannel::set_baudrate_() {
|
||||
@@ -334,7 +335,8 @@ size_t WeikaiChannel::tx_in_fifo_() {
|
||||
if (tfcnt == 0) {
|
||||
uint8_t const fsr = this->reg(WKREG_FSR);
|
||||
if (fsr & FSR_TFFULL) {
|
||||
ESP_LOGVV(TAG, "tx FIFO full FSR=%s", I2S2CS(fsr));
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "tx FIFO full FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
tfcnt = FIFO_SIZE;
|
||||
}
|
||||
}
|
||||
@@ -346,14 +348,15 @@ size_t WeikaiChannel::rx_in_fifo_() {
|
||||
size_t available = this->reg(WKREG_RFCNT);
|
||||
uint8_t const fsr = this->reg(WKREG_FSR);
|
||||
if (fsr & (FSR_RFOE | FSR_RFLB | FSR_RFFE | FSR_RFPE)) {
|
||||
char bin_buf[9];
|
||||
if (fsr & FSR_RFOE)
|
||||
ESP_LOGE(TAG, "Receive data overflow FSR=%s", I2S2CS(fsr));
|
||||
ESP_LOGE(TAG, "Receive data overflow FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
if (fsr & FSR_RFLB)
|
||||
ESP_LOGE(TAG, "Receive line break FSR=%s", I2S2CS(fsr));
|
||||
ESP_LOGE(TAG, "Receive line break FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
if (fsr & FSR_RFFE)
|
||||
ESP_LOGE(TAG, "Receive frame error FSR=%s", I2S2CS(fsr));
|
||||
ESP_LOGE(TAG, "Receive frame error FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
if (fsr & FSR_RFPE)
|
||||
ESP_LOGE(TAG, "Receive parity error FSR=%s", I2S2CS(fsr));
|
||||
ESP_LOGE(TAG, "Receive parity error FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
}
|
||||
if ((available == 0) && (fsr & FSR_RFDAT)) {
|
||||
// here we should be very careful because we can have something like this:
|
||||
@@ -362,11 +365,13 @@ size_t WeikaiChannel::rx_in_fifo_() {
|
||||
// - so to be sure we need to do another read of RFCNT and if it is still zero -> buffer full
|
||||
available = this->reg(WKREG_RFCNT);
|
||||
if (available == 0) { // still zero ?
|
||||
ESP_LOGV(TAG, "rx FIFO is full FSR=%s", I2S2CS(fsr));
|
||||
char bin_buf[9];
|
||||
ESP_LOGV(TAG, "rx FIFO is full FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
available = FIFO_SIZE;
|
||||
}
|
||||
}
|
||||
ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, I2S2CS(fsr));
|
||||
char bin_buf2[9];
|
||||
ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, format_bin_to(bin_buf2, fsr));
|
||||
return available;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
/// wk2132_i2c, wk2168_i2c, wk2204_i2c, wk2212_i2c
|
||||
|
||||
#pragma once
|
||||
#include <bitset>
|
||||
#include <memory>
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
@@ -10,13 +10,6 @@ namespace weikai_spi {
|
||||
using namespace weikai;
|
||||
static const char *const TAG = "weikai_spi";
|
||||
|
||||
/// @brief convert an int to binary representation as C++ std::string
|
||||
/// @param val integer to convert
|
||||
/// @return a std::string
|
||||
inline std::string i2s(uint8_t val) { return std::bitset<8>(val).to_string(); }
|
||||
/// Convert std::string to C string
|
||||
#define I2S2CS(val) (i2s(val).c_str())
|
||||
|
||||
/// @brief measure the time elapsed between two calls
|
||||
/// @param last_time time of the previous call
|
||||
/// @return the elapsed time in microseconds
|
||||
@@ -107,7 +100,8 @@ uint8_t WeikaiRegisterSPI::read_reg() const {
|
||||
spi_comp->write_byte(cmd);
|
||||
uint8_t val = spi_comp->read_byte();
|
||||
spi_comp->disable();
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", I2S2CS(cmd), cmd,
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, cmd), cmd,
|
||||
reg_to_str(this->register_, this->comp_->page1()), this->channel_, val);
|
||||
return val;
|
||||
}
|
||||
@@ -120,8 +114,9 @@ void WeikaiRegisterSPI::read_fifo(uint8_t *data, size_t length) const {
|
||||
spi_comp->read_array(data, length);
|
||||
spi_comp->disable();
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", I2S2CS(cmd), cmd, this->channel_,
|
||||
length);
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
|
||||
this->channel_, length);
|
||||
print_buffer(data, length);
|
||||
#endif
|
||||
}
|
||||
@@ -132,8 +127,9 @@ void WeikaiRegisterSPI::write_reg(uint8_t value) {
|
||||
spi_comp->enable();
|
||||
spi_comp->write_array(buf, 2);
|
||||
spi_comp->disable();
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", I2S2CS(buf[0]), buf[0],
|
||||
reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]);
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, buf[0]),
|
||||
buf[0], reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]);
|
||||
}
|
||||
|
||||
void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
|
||||
@@ -145,8 +141,9 @@ void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
|
||||
spi_comp->disable();
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", I2S2CS(cmd), cmd, this->channel_,
|
||||
length);
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
|
||||
this->channel_, length);
|
||||
print_buffer(data, length);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
/// wk2124_spi, wk2132_spi, wk2168_spi, wk2204_spi, wk2212_spi,
|
||||
|
||||
#pragma once
|
||||
#include <bitset>
|
||||
#include <memory>
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
@@ -660,21 +660,27 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
this->scan_result_.clear();
|
||||
this->scan_done_ = true;
|
||||
|
||||
int16_t num = WiFi.scanComplete();
|
||||
if (num < 0)
|
||||
// Access scan data directly to avoid String allocation from WiFi.SSID(i)
|
||||
// WiFi.scan is public in LibreTiny (WiFi.h)
|
||||
if (WiFi.scan == nullptr || WiFi.scan->running)
|
||||
return;
|
||||
|
||||
this->scan_result_.init(static_cast<unsigned int>(num));
|
||||
for (int i = 0; i < num; i++) {
|
||||
String ssid = WiFi.SSID(i);
|
||||
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
|
||||
int32_t rssi = WiFi.RSSI(i);
|
||||
uint8_t *bssid = WiFi.BSSID(i);
|
||||
int32_t channel = WiFi.channel(i);
|
||||
uint8_t num = WiFi.scan->count;
|
||||
if (num == 0) {
|
||||
WiFi.scanDelete();
|
||||
return;
|
||||
}
|
||||
|
||||
this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]},
|
||||
std::string(ssid.c_str()), channel, rssi, authmode != WIFI_AUTH_OPEN,
|
||||
ssid.length() == 0);
|
||||
this->scan_result_.init(num);
|
||||
for (uint8_t i = 0; i < num; i++) {
|
||||
const auto &ap = WiFi.scan->ap[i];
|
||||
const char *ssid_cstr = ap.ssid;
|
||||
size_t ssid_len = ssid_cstr ? strlen(ssid_cstr) : 0;
|
||||
|
||||
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
|
||||
ap.bssid.addr[4], ap.bssid.addr[5]},
|
||||
std::string(ssid_cstr ? ssid_cstr : "", ssid_len), ap.channel, ap.rssi,
|
||||
ap.auth != WIFI_AUTH_OPEN, ssid_len == 0);
|
||||
}
|
||||
WiFi.scanDelete();
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
|
||||
@@ -30,6 +30,7 @@ _WG_KEY_REGEX = re.compile(r"^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$")
|
||||
|
||||
wireguard_ns = cg.esphome_ns.namespace("wireguard")
|
||||
Wireguard = wireguard_ns.class_("Wireguard", cg.Component, cg.PollingComponent)
|
||||
AllowedIP = wireguard_ns.struct("AllowedIP")
|
||||
WireguardPeerOnlineCondition = wireguard_ns.class_(
|
||||
"WireguardPeerOnlineCondition", automation.Condition
|
||||
)
|
||||
@@ -108,8 +109,18 @@ async def to_code(config):
|
||||
)
|
||||
)
|
||||
|
||||
for ip in allowed_ips:
|
||||
cg.add(var.add_allowed_ip(str(ip.network_address), str(ip.netmask)))
|
||||
cg.add(
|
||||
var.set_allowed_ips(
|
||||
[
|
||||
cg.StructInitializer(
|
||||
AllowedIP,
|
||||
("ip", str(ip.network_address)),
|
||||
("netmask", str(ip.netmask)),
|
||||
)
|
||||
for ip in allowed_ips
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
cg.add(var.set_srctime(await cg.get_variable(config[CONF_TIME_ID])))
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
#include <esp_wireguard.h>
|
||||
#include <esp_wireguard_err.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace wireguard {
|
||||
namespace esphome::wireguard {
|
||||
|
||||
static const char *const TAG = "wireguard";
|
||||
|
||||
@@ -28,16 +27,16 @@ static const char *const LOGMSG_ONLINE = "online";
|
||||
static const char *const LOGMSG_OFFLINE = "offline";
|
||||
|
||||
void Wireguard::setup() {
|
||||
this->wg_config_.address = this->address_.c_str();
|
||||
this->wg_config_.private_key = this->private_key_.c_str();
|
||||
this->wg_config_.endpoint = this->peer_endpoint_.c_str();
|
||||
this->wg_config_.public_key = this->peer_public_key_.c_str();
|
||||
this->wg_config_.address = this->address_;
|
||||
this->wg_config_.private_key = this->private_key_;
|
||||
this->wg_config_.endpoint = this->peer_endpoint_;
|
||||
this->wg_config_.public_key = this->peer_public_key_;
|
||||
this->wg_config_.port = this->peer_port_;
|
||||
this->wg_config_.netmask = this->netmask_.c_str();
|
||||
this->wg_config_.netmask = this->netmask_;
|
||||
this->wg_config_.persistent_keepalive = this->keepalive_;
|
||||
|
||||
if (!this->preshared_key_.empty())
|
||||
this->wg_config_.preshared_key = this->preshared_key_.c_str();
|
||||
if (this->preshared_key_ != nullptr)
|
||||
this->wg_config_.preshared_key = this->preshared_key_;
|
||||
|
||||
this->publish_enabled_state();
|
||||
|
||||
@@ -131,6 +130,10 @@ void Wireguard::update() {
|
||||
}
|
||||
|
||||
void Wireguard::dump_config() {
|
||||
char private_key_masked[MASK_KEY_BUFFER_SIZE];
|
||||
char preshared_key_masked[MASK_KEY_BUFFER_SIZE];
|
||||
mask_key_to(private_key_masked, sizeof(private_key_masked), this->private_key_);
|
||||
mask_key_to(preshared_key_masked, sizeof(preshared_key_masked), this->preshared_key_);
|
||||
// clang-format off
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
@@ -142,13 +145,13 @@ void Wireguard::dump_config() {
|
||||
" Peer Port: " LOG_SECRET("%d") "\n"
|
||||
" Peer Public Key: " LOG_SECRET("%s") "\n"
|
||||
" Peer Pre-shared Key: " LOG_SECRET("%s"),
|
||||
this->address_.c_str(), this->netmask_.c_str(), mask_key(this->private_key_).c_str(),
|
||||
this->peer_endpoint_.c_str(), this->peer_port_, this->peer_public_key_.c_str(),
|
||||
(!this->preshared_key_.empty() ? mask_key(this->preshared_key_).c_str() : "NOT IN USE"));
|
||||
this->address_, this->netmask_, private_key_masked,
|
||||
this->peer_endpoint_, this->peer_port_, this->peer_public_key_,
|
||||
(this->preshared_key_ != nullptr ? preshared_key_masked : "NOT IN USE"));
|
||||
// clang-format on
|
||||
ESP_LOGCONFIG(TAG, " Peer Allowed IPs:");
|
||||
for (auto &allowed_ip : this->allowed_ips_) {
|
||||
ESP_LOGCONFIG(TAG, " - %s/%s", std::get<0>(allowed_ip).c_str(), std::get<1>(allowed_ip).c_str());
|
||||
for (const AllowedIP &allowed_ip : this->allowed_ips_) {
|
||||
ESP_LOGCONFIG(TAG, " - %s/%s", allowed_ip.ip, allowed_ip.netmask);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Peer Persistent Keepalive: %d%s", this->keepalive_,
|
||||
(this->keepalive_ > 0 ? "s" : " (DISABLED)"));
|
||||
@@ -176,18 +179,6 @@ time_t Wireguard::get_latest_handshake() const {
|
||||
return result;
|
||||
}
|
||||
|
||||
void Wireguard::set_address(const std::string &address) { this->address_ = address; }
|
||||
void Wireguard::set_netmask(const std::string &netmask) { this->netmask_ = netmask; }
|
||||
void Wireguard::set_private_key(const std::string &key) { this->private_key_ = key; }
|
||||
void Wireguard::set_peer_endpoint(const std::string &endpoint) { this->peer_endpoint_ = endpoint; }
|
||||
void Wireguard::set_peer_public_key(const std::string &key) { this->peer_public_key_ = key; }
|
||||
void Wireguard::set_peer_port(const uint16_t port) { this->peer_port_ = port; }
|
||||
void Wireguard::set_preshared_key(const std::string &key) { this->preshared_key_ = key; }
|
||||
|
||||
void Wireguard::add_allowed_ip(const std::string &ip, const std::string &netmask) {
|
||||
this->allowed_ips_.emplace_back(ip, netmask);
|
||||
}
|
||||
|
||||
void Wireguard::set_keepalive(const uint16_t seconds) { this->keepalive_ = seconds; }
|
||||
void Wireguard::set_reboot_timeout(const uint32_t seconds) { this->reboot_timeout_ = seconds; }
|
||||
void Wireguard::set_srctime(time::RealTimeClock *srctime) { this->srctime_ = srctime; }
|
||||
@@ -274,9 +265,8 @@ void Wireguard::start_connection_() {
|
||||
|
||||
ESP_LOGD(TAG, "Configuring allowed IPs list");
|
||||
bool allowed_ips_ok = true;
|
||||
for (std::tuple<std::string, std::string> ip : this->allowed_ips_) {
|
||||
allowed_ips_ok &=
|
||||
(esp_wireguard_add_allowed_ip(&(this->wg_ctx_), std::get<0>(ip).c_str(), std::get<1>(ip).c_str()) == ESP_OK);
|
||||
for (const AllowedIP &ip : this->allowed_ips_) {
|
||||
allowed_ips_ok &= (esp_wireguard_add_allowed_ip(&(this->wg_ctx_), ip.ip, ip.netmask) == ESP_OK);
|
||||
}
|
||||
|
||||
if (allowed_ips_ok) {
|
||||
@@ -299,8 +289,25 @@ void Wireguard::stop_connection_() {
|
||||
}
|
||||
}
|
||||
|
||||
std::string mask_key(const std::string &key) { return (key.substr(0, 5) + "[...]="); }
|
||||
void mask_key_to(char *buffer, size_t len, const char *key) {
|
||||
// Format: "XXXXX[...]=\0" = MASK_KEY_BUFFER_SIZE chars minimum
|
||||
if (len < MASK_KEY_BUFFER_SIZE || key == nullptr) {
|
||||
if (len > 0)
|
||||
buffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
// Copy first 5 characters of the key
|
||||
size_t i = 0;
|
||||
for (; i < 5 && key[i] != '\0'; ++i) {
|
||||
buffer[i] = key[i];
|
||||
}
|
||||
// Append "[...]="
|
||||
const char *suffix = "[...]=";
|
||||
for (size_t j = 0; suffix[j] != '\0' && (i + j) < len - 1; ++j) {
|
||||
buffer[i + j] = suffix[j];
|
||||
}
|
||||
buffer[i + 6] = '\0';
|
||||
}
|
||||
|
||||
} // namespace wireguard
|
||||
} // namespace esphome
|
||||
} // namespace esphome::wireguard
|
||||
#endif
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_WIREGUARD
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -22,8 +22,13 @@
|
||||
|
||||
#include <esp_wireguard.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace wireguard {
|
||||
namespace esphome::wireguard {
|
||||
|
||||
/// Allowed IP entry for WireGuard peer configuration.
|
||||
struct AllowedIP {
|
||||
const char *ip;
|
||||
const char *netmask;
|
||||
};
|
||||
|
||||
/// Main Wireguard component class.
|
||||
class Wireguard : public PollingComponent {
|
||||
@@ -37,15 +42,25 @@ class Wireguard : public PollingComponent {
|
||||
|
||||
float get_setup_priority() const override { return esphome::setup_priority::BEFORE_CONNECTION; }
|
||||
|
||||
void set_address(const std::string &address);
|
||||
void set_netmask(const std::string &netmask);
|
||||
void set_private_key(const std::string &key);
|
||||
void set_peer_endpoint(const std::string &endpoint);
|
||||
void set_peer_public_key(const std::string &key);
|
||||
void set_peer_port(uint16_t port);
|
||||
void set_preshared_key(const std::string &key);
|
||||
void set_address(const char *address) { this->address_ = address; }
|
||||
void set_netmask(const char *netmask) { this->netmask_ = netmask; }
|
||||
void set_private_key(const char *key) { this->private_key_ = key; }
|
||||
void set_peer_endpoint(const char *endpoint) { this->peer_endpoint_ = endpoint; }
|
||||
void set_peer_public_key(const char *key) { this->peer_public_key_ = key; }
|
||||
void set_peer_port(uint16_t port) { this->peer_port_ = port; }
|
||||
void set_preshared_key(const char *key) { this->preshared_key_ = key; }
|
||||
|
||||
void add_allowed_ip(const std::string &ip, const std::string &netmask);
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_address(const std::string &address) = delete;
|
||||
void set_netmask(const std::string &netmask) = delete;
|
||||
void set_private_key(const std::string &key) = delete;
|
||||
void set_peer_endpoint(const std::string &endpoint) = delete;
|
||||
void set_peer_public_key(const std::string &key) = delete;
|
||||
void set_preshared_key(const std::string &key) = delete;
|
||||
|
||||
void set_allowed_ips(std::initializer_list<AllowedIP> ips) { this->allowed_ips_ = ips; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_allowed_ips(std::initializer_list<std::tuple<std::string, std::string>> ips) = delete;
|
||||
|
||||
void set_keepalive(uint16_t seconds);
|
||||
void set_reboot_timeout(uint32_t seconds);
|
||||
@@ -83,14 +98,14 @@ class Wireguard : public PollingComponent {
|
||||
time_t get_latest_handshake() const;
|
||||
|
||||
protected:
|
||||
std::string address_;
|
||||
std::string netmask_;
|
||||
std::string private_key_;
|
||||
std::string peer_endpoint_;
|
||||
std::string peer_public_key_;
|
||||
std::string preshared_key_;
|
||||
const char *address_{nullptr};
|
||||
const char *netmask_{nullptr};
|
||||
const char *private_key_{nullptr};
|
||||
const char *peer_endpoint_{nullptr};
|
||||
const char *peer_public_key_{nullptr};
|
||||
const char *preshared_key_{nullptr};
|
||||
|
||||
std::vector<std::tuple<std::string, std::string>> allowed_ips_;
|
||||
FixedVector<AllowedIP> allowed_ips_;
|
||||
|
||||
uint16_t peer_port_;
|
||||
uint16_t keepalive_;
|
||||
@@ -142,8 +157,11 @@ class Wireguard : public PollingComponent {
|
||||
void suspend_wdt();
|
||||
void resume_wdt();
|
||||
|
||||
/// Size of buffer required for mask_key_to: 5 chars + "[...]=" + null = 12
|
||||
static constexpr size_t MASK_KEY_BUFFER_SIZE = 12;
|
||||
|
||||
/// Strip most part of the key only for secure printing
|
||||
std::string mask_key(const std::string &key);
|
||||
void mask_key_to(char *buffer, size_t len, const char *key);
|
||||
|
||||
/// Condition to check if remote peer is online.
|
||||
template<typename... Ts> class WireguardPeerOnlineCondition : public Condition<Ts...>, public Parented<Wireguard> {
|
||||
@@ -169,6 +187,5 @@ template<typename... Ts> class WireguardDisableAction : public Action<Ts...>, pu
|
||||
void play(const Ts &...x) override { this->parent_->disable(); }
|
||||
};
|
||||
|
||||
} // namespace wireguard
|
||||
} // namespace esphome
|
||||
} // namespace esphome::wireguard
|
||||
#endif
|
||||
|
||||
@@ -174,6 +174,13 @@ bool str_endswith(const std::string &str, const std::string &end) {
|
||||
return str.rfind(end) == (str.size() - end.size());
|
||||
}
|
||||
#endif
|
||||
|
||||
bool str_endswith_ignore_case(const char *str, size_t str_len, const char *suffix, size_t suffix_len) {
|
||||
if (suffix_len > str_len)
|
||||
return false;
|
||||
return strncasecmp(str + str_len - suffix_len, suffix, suffix_len) == 0;
|
||||
}
|
||||
|
||||
std::string str_truncate(const std::string &str, size_t length) {
|
||||
return str.length() > length ? str.substr(0, length) : str;
|
||||
}
|
||||
|
||||
@@ -395,6 +395,28 @@ constexpr uint32_t FNV1_OFFSET_BASIS = 2166136261UL;
|
||||
/// FNV-1 32-bit prime
|
||||
constexpr uint32_t FNV1_PRIME = 16777619UL;
|
||||
|
||||
/// Extend a FNV-1 hash with an integer (hashes each byte).
|
||||
template<std::integral T> constexpr uint32_t fnv1_hash_extend(uint32_t hash, T value) {
|
||||
using UnsignedT = std::make_unsigned_t<T>;
|
||||
UnsignedT uvalue = static_cast<UnsignedT>(value);
|
||||
for (size_t i = 0; i < sizeof(T); i++) {
|
||||
hash *= FNV1_PRIME;
|
||||
hash ^= (uvalue >> (i * 8)) & 0xFF;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
/// Extend a FNV-1 hash with additional string data.
|
||||
constexpr uint32_t fnv1_hash_extend(uint32_t hash, const char *str) {
|
||||
if (str) {
|
||||
while (*str) {
|
||||
hash *= FNV1_PRIME;
|
||||
hash ^= *str++;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
inline uint32_t fnv1_hash_extend(uint32_t hash, const std::string &str) { return fnv1_hash_extend(hash, str.c_str()); }
|
||||
|
||||
/// Extend a FNV-1a hash with additional string data.
|
||||
constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str) {
|
||||
if (str) {
|
||||
@@ -523,6 +545,15 @@ bool str_startswith(const std::string &str, const std::string &start);
|
||||
/// Check whether a string ends with a value.
|
||||
bool str_endswith(const std::string &str, const std::string &end);
|
||||
|
||||
/// Case-insensitive check if string ends with suffix (no heap allocation).
|
||||
bool str_endswith_ignore_case(const char *str, size_t str_len, const char *suffix, size_t suffix_len);
|
||||
inline bool str_endswith_ignore_case(const char *str, const char *suffix) {
|
||||
return str_endswith_ignore_case(str, strlen(str), suffix, strlen(suffix));
|
||||
}
|
||||
inline bool str_endswith_ignore_case(const std::string &str, const char *suffix) {
|
||||
return str_endswith_ignore_case(str.c_str(), str.size(), suffix, strlen(suffix));
|
||||
}
|
||||
|
||||
/// Truncate a string to a specific length.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_truncate(const std::string &str, size_t length);
|
||||
|
||||
@@ -9,6 +9,8 @@ alarm_control_panel:
|
||||
name: Alarm Panel
|
||||
codes:
|
||||
- "1234"
|
||||
- "5678"
|
||||
- "0000"
|
||||
requires_code_to_arm: true
|
||||
arming_home_time: 1s
|
||||
arming_night_time: 1s
|
||||
@@ -29,6 +31,7 @@ alarm_control_panel:
|
||||
name: Alarm Panel 2
|
||||
codes:
|
||||
- "1234"
|
||||
- "9999"
|
||||
requires_code_to_arm: true
|
||||
arming_home_time: 1s
|
||||
arming_night_time: 1s
|
||||
|
||||
@@ -5,7 +5,10 @@ wifi:
|
||||
udp:
|
||||
id: my_udp
|
||||
listen_address: 239.0.60.53
|
||||
addresses: ["239.0.60.53"]
|
||||
addresses:
|
||||
- "239.0.60.53"
|
||||
- "192.168.1.255"
|
||||
- "10.0.0.255"
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received %d bytes"
|
||||
|
||||
33
tests/integration/fixtures/udp_send_receive.yaml
Normal file
33
tests/integration/fixtures/udp_send_receive.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
esphome:
|
||||
name: udp-test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
services:
|
||||
- service: send_udp_message
|
||||
then:
|
||||
- udp.write:
|
||||
id: test_udp
|
||||
data: "HELLO_UDP_TEST"
|
||||
- service: send_udp_bytes
|
||||
then:
|
||||
- udp.write:
|
||||
id: test_udp
|
||||
data: [0x55, 0x44, 0x50, 0x5F, 0x42, 0x59, 0x54, 0x45, 0x53] # "UDP_BYTES"
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
udp:
|
||||
- id: test_udp
|
||||
addresses:
|
||||
- "127.0.0.1"
|
||||
- "127.0.0.2"
|
||||
port:
|
||||
listen_port: UDP_LISTEN_PORT_PLACEHOLDER
|
||||
broadcast_port: UDP_BROADCAST_PORT_PLACEHOLDER
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received UDP: %d bytes"
|
||||
args: [data.size()]
|
||||
171
tests/integration/test_udp.py
Normal file
171
tests/integration/test_udp.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Integration test for UDP component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
import contextlib
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@dataclass
|
||||
class UDPReceiver:
|
||||
"""Collects UDP messages received."""
|
||||
|
||||
messages: list[bytes] = field(default_factory=list)
|
||||
message_received: asyncio.Event = field(default_factory=asyncio.Event)
|
||||
|
||||
def on_message(self, data: bytes) -> None:
|
||||
"""Called when a message is received."""
|
||||
self.messages.append(data)
|
||||
self.message_received.set()
|
||||
|
||||
async def wait_for_message(self, timeout: float = 5.0) -> bytes:
|
||||
"""Wait for a message to be received."""
|
||||
await asyncio.wait_for(self.message_received.wait(), timeout=timeout)
|
||||
return self.messages[-1]
|
||||
|
||||
async def wait_for_content(self, content: bytes, timeout: float = 5.0) -> bytes:
|
||||
"""Wait for a specific message content."""
|
||||
deadline = asyncio.get_event_loop().time() + timeout
|
||||
while True:
|
||||
for msg in self.messages:
|
||||
if content in msg:
|
||||
return msg
|
||||
remaining = deadline - asyncio.get_event_loop().time()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError(
|
||||
f"Content {content!r} not found in messages: {self.messages}"
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(self.message_received.wait(), timeout=remaining)
|
||||
self.message_received.clear()
|
||||
except TimeoutError:
|
||||
raise TimeoutError(
|
||||
f"Content {content!r} not found in messages: {self.messages}"
|
||||
) from None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]]:
|
||||
"""Async context manager that listens for UDP messages.
|
||||
|
||||
Args:
|
||||
port: Port to listen on. 0 for auto-assign.
|
||||
|
||||
Yields:
|
||||
Tuple of (port, UDPReceiver) where port is the UDP port being listened on.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("127.0.0.1", port))
|
||||
sock.setblocking(False)
|
||||
actual_port = sock.getsockname()[1]
|
||||
|
||||
receiver = UDPReceiver()
|
||||
|
||||
async def receive_messages() -> None:
|
||||
"""Background task to receive UDP messages."""
|
||||
loop = asyncio.get_running_loop()
|
||||
while True:
|
||||
try:
|
||||
data = await loop.sock_recv(sock, 4096)
|
||||
if data:
|
||||
receiver.on_message(data)
|
||||
except BlockingIOError:
|
||||
await asyncio.sleep(0.01)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
task = asyncio.create_task(receive_messages())
|
||||
try:
|
||||
yield actual_port, receiver
|
||||
finally:
|
||||
task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
sock.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_udp_send_receive(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test UDP component can send messages with multiple addresses configured."""
|
||||
# Track log lines to verify dump_config output
|
||||
log_lines: list[str] = []
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
log_lines.append(line)
|
||||
|
||||
async with udp_listener() as (udp_port, receiver):
|
||||
# Replace placeholders in the config
|
||||
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1))
|
||||
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port))
|
||||
|
||||
async with (
|
||||
run_compiled(config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device is running
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "udp-test"
|
||||
|
||||
# Get services
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
# Test sending string message
|
||||
send_message_service = next(
|
||||
(s for s in services if s.name == "send_udp_message"), None
|
||||
)
|
||||
assert send_message_service is not None, (
|
||||
"send_udp_message service not found"
|
||||
)
|
||||
|
||||
await client.execute_service(send_message_service, {})
|
||||
|
||||
try:
|
||||
msg = await receiver.wait_for_content(b"HELLO_UDP_TEST", timeout=5.0)
|
||||
assert b"HELLO_UDP_TEST" in msg
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"UDP string message not received. Got: {receiver.messages}"
|
||||
)
|
||||
|
||||
# Test sending bytes
|
||||
send_bytes_service = next(
|
||||
(s for s in services if s.name == "send_udp_bytes"), None
|
||||
)
|
||||
assert send_bytes_service is not None, "send_udp_bytes service not found"
|
||||
|
||||
await client.execute_service(send_bytes_service, {})
|
||||
|
||||
try:
|
||||
msg = await receiver.wait_for_content(b"UDP_BYTES", timeout=5.0)
|
||||
assert b"UDP_BYTES" in msg
|
||||
except TimeoutError:
|
||||
pytest.fail(f"UDP bytes message not received. Got: {receiver.messages}")
|
||||
|
||||
# Verify we received at least 2 messages (string + bytes)
|
||||
assert len(receiver.messages) >= 2, (
|
||||
f"Expected at least 2 messages, got {len(receiver.messages)}"
|
||||
)
|
||||
|
||||
# Verify dump_config logged all configured addresses
|
||||
# This tests that FixedVector<const char*> stores addresses correctly
|
||||
log_text = "\n".join(log_lines)
|
||||
assert "Address: 127.0.0.1" in log_text, (
|
||||
f"Address 127.0.0.1 not found in dump_config. Log: {log_text[-2000:]}"
|
||||
)
|
||||
assert "Address: 127.0.0.2" in log_text, (
|
||||
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
|
||||
)
|
||||
Reference in New Issue
Block a user