#include "esphome/core/helpers.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" #include #include #include #include #include #include #ifdef USE_ESP32 #include "esp_rom_crc.h" #endif namespace esphome { #ifdef USE_ESP8266 std::string progmem_string(ProgmemStr str) { auto *src = reinterpret_cast(str); std::string result(strlen_P(src), '\0'); memcpy_P(result.data(), src, result.size()); return result; } #endif static const char *const TAG = "helpers"; __attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, size_t elem_size) { ESPHOME_DEBUG_ASSERT(size < UINT16_MAX); uint16_t new_cap = size + 1; auto *new_data = ::operator new(new_cap *elem_size); if (data) { __builtin_memcpy(new_data, data, size * elem_size); ::operator delete(data); } capacity = new_cap; return new_data; } static const uint16_t CRC16_A001_LE_LUT_L[] = {0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440}; static const uint16_t CRC16_A001_LE_LUT_H[] = {0x0000, 0xcc01, 0xd801, 0x1400, 0xf001, 0x3c00, 0x2800, 0xe401, 0xa001, 0x6c00, 0x7800, 0xb401, 0x5000, 0x9c01, 0x8801, 0x4400}; #ifndef USE_ESP32 static const uint16_t CRC16_8408_LE_LUT_L[] = {0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7}; static const uint16_t CRC16_8408_LE_LUT_H[] = {0x0000, 0x1081, 0x2102, 0x3183, 0x4204, 0x5285, 0x6306, 0x7387, 0x8408, 0x9489, 0xa50a, 0xb58b, 0xc60c, 0xd68d, 0xe70e, 0xf78f}; #endif #ifndef USE_ESP32 static const uint16_t CRC16_1021_BE_LUT_L[] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef}; static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0x48c4, 0x5af5, 0x6ca6, 0x7e97, 0x9188, 0x83b9, 0xb5ea, 0xa7db, 0xd94c, 0xcb7d, 0xfd2e, 0xef1f}; #endif // Mathematics uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc, uint8_t poly, bool msb_first) { while ((len--) != 0u) { uint8_t inbyte = *data++; if (msb_first) { // MSB first processing (for polynomials like 0x31, 0x07) crc ^= inbyte; for (uint8_t i = 8; i != 0u; i--) { if (crc & 0x80) { crc = (crc << 1) ^ poly; } else { crc <<= 1; } } } else { // LSB first processing (default for Dallas/Maxim 0x8C) for (uint8_t i = 8; i != 0u; i--) { bool mix = (crc ^ inbyte) & 0x01; crc >>= 1; if (mix) crc ^= poly; inbyte >>= 1; } } } return crc; } uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse_poly, bool refin, bool refout) { #ifdef USE_ESP32 if (reverse_poly == 0x8408) { crc = esp_rom_crc16_le(refin ? crc : (crc ^ 0xffff), data, len); return refout ? crc : (crc ^ 0xffff); } #endif if (refin) { crc ^= 0xffff; } #ifndef USE_ESP32 if (reverse_poly == 0x8408) { while (len--) { uint8_t combo = crc ^ (uint8_t) *data++; crc = (crc >> 8) ^ CRC16_8408_LE_LUT_L[combo & 0x0F] ^ CRC16_8408_LE_LUT_H[combo >> 4]; } } else #endif { if (reverse_poly == 0xa001) { while (len--) { uint8_t combo = crc ^ (uint8_t) *data++; crc = (crc >> 8) ^ CRC16_A001_LE_LUT_L[combo & 0x0F] ^ CRC16_A001_LE_LUT_H[combo >> 4]; } } else { while (len--) { crc ^= *data++; for (uint8_t i = 0; i < 8; i++) { if (crc & 0x0001) { crc = (crc >> 1) ^ reverse_poly; } else { crc >>= 1; } } } } } return refout ? (crc ^ 0xffff) : crc; } uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t poly, bool refin, bool refout) { #ifdef USE_ESP32 if (poly == 0x1021) { crc = esp_rom_crc16_be(refin ? crc : (crc ^ 0xffff), data, len); return refout ? crc : (crc ^ 0xffff); } #endif if (refin) { crc ^= 0xffff; } #ifndef USE_ESP32 if (poly == 0x1021) { while (len--) { uint8_t combo = (crc >> 8) ^ *data++; crc = (crc << 8) ^ CRC16_1021_BE_LUT_L[combo & 0x0F] ^ CRC16_1021_BE_LUT_H[combo >> 4]; } } else #endif { while (len--) { crc ^= (((uint16_t) *data++) << 8); for (uint8_t i = 0; i < 8; i++) { if (crc & 0x8000) { crc = (crc << 1) ^ poly; } else { crc <<= 1; } } } } return refout ? (crc ^ 0xffff) : crc; } // FNV-1 hash - deprecated, use fnv1a_hash() for new code uint32_t fnv1_hash(const char *str) { uint32_t hash = FNV1_OFFSET_BASIS; if (str) { while (*str) { hash *= FNV1_PRIME; hash ^= *str++; } } return hash; } // SplitMix32 — a fast, non-cryptographic PRNG from the SplitMix family // (Steele et al., 2014). Uses a Weyl sequence with golden-ratio increment // and the MurmurHash3 32-bit finalizer as output mixing function. // Reference: https://doi.org/10.1145/2714064.2660195 // Test results: https://lemire.me/blog/2017/08/22/testing-non-cryptographic-random-number-generators-my-results/ // Seeded lazily from the platform's secure RNG via random_bytes(). // ESP8266 uses os_random() instead (defined in esp8266/helpers.cpp). #ifndef USE_ESP8266 static uint32_t splitmix32_state; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) uint32_t random_uint32() { // State of 0 means unseeded. The state will wrap back to 0 after 2^32 calls, // triggering one extra random_bytes() call — an acceptable trade-off vs. adding // a separate bool flag (4 bytes BSS + branch on every call). if (splitmix32_state == 0) { random_bytes(reinterpret_cast(&splitmix32_state), sizeof(splitmix32_state)); splitmix32_state |= 1; // ensure non-zero seed } splitmix32_state += 0x9e3779b9u; uint32_t z = splitmix32_state; z = (z ^ (z >> 16)) * 0x85ebca6bu; z = (z ^ (z >> 13)) * 0xc2b2ae35u; return z ^ (z >> 16); } #endif float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } // Strings bool str_equals_case_insensitive(const std::string &a, const std::string &b) { return strcasecmp(a.c_str(), b.c_str()) == 0; } bool str_equals_case_insensitive(StringRef a, StringRef b) { return a.size() == b.size() && strncasecmp(a.c_str(), b.c_str(), a.size()) == 0; } #if __cplusplus >= 202002L bool str_startswith(const std::string &str, const std::string &start) { return str.starts_with(start); } bool str_endswith(const std::string &str, const std::string &end) { return str.ends_with(end); } #else bool str_startswith(const std::string &str, const std::string &start) { return str.rfind(start, 0) == 0; } 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; } bool str_contains_ignore_case_fallback(const char *haystack, const char *needle) { const size_t needle_len = strlen(needle); if (needle_len == 0) { return true; } for (const char *p = haystack; *p != '\0'; p++) { if (strncasecmp(p, needle, needle_len) == 0) { return true; } } return false; } #ifdef USE_ESP8266 // _P mirror of str_contains_ignore_case_fallback above; host tests cover only the fallback, // so keep the two bodies in sync. bool str_contains_ignore_case_p(const char *haystack, PGM_P needle) { if (haystack == nullptr || needle == nullptr) { return false; } const size_t needle_len = strlen_P(needle); if (needle_len == 0) { return true; } for (const char *p = haystack; *p != '\0'; p++) { if (strncasecmp_P(p, needle, needle_len) == 0) { return true; } } return false; } #endif // USE_ESP8266 // str_truncate, str_until, str_lower_case, str_upper_case, str_snake_case moved to alloc_helpers.cpp char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { if (buffer_size == 0) { return buffer; } size_t i = 0; while (*str && i < buffer_size - 1) { buffer[i++] = to_sanitized_char(*str++); } buffer[i] = '\0'; return buffer; } // str_sanitize, str_snprintf, str_sprintf moved to alloc_helpers.cpp size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep, const char *suffix_ptr, size_t suffix_len) { size_t total_len = name_len + 1 + suffix_len; // Silently truncate if needed: prioritize keeping the full suffix if (total_len >= buffer_size) { // NOTE: This calculation could underflow if suffix_len >= buffer_size - 2, // but this is safe because this helper is only called with small suffixes: // MAC suffixes (6-12 bytes), ".local" (5 bytes), etc. name_len = buffer_size - suffix_len - 2; // -2 for separator and null terminator total_len = name_len + 1 + suffix_len; } memcpy(buffer, name, name_len); buffer[name_len] = sep; memcpy(buffer + name_len + 1, suffix_ptr, suffix_len); buffer[total_len] = '\0'; return total_len; } // Parsing & formatting size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { size_t chars = std::min(length, 2 * count); for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) { uint8_t val = parse_hex_char(*str); if (val == INVALID_HEX_CHAR) return 0; data[i >> 1] = (i & 1) ? data[i >> 1] | val : val << 4; } return chars; } // format_mac_address_pretty moved to alloc_helpers.cpp // Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase. // When separator is set, it is written unconditionally after each byte and the last // one is overwritten with '\0', eliminating the per-byte `i < length - 1` check. static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator, char base) { if (length == 0 || buffer_size == 0) { if (buffer_size > 0) buffer[0] = '\0'; return buffer; } uint8_t stride = separator ? 3 : 2; size_t max_bytes = separator ? (buffer_size / 3) : ((buffer_size - 1) / 2); if (max_bytes == 0) { buffer[0] = '\0'; return buffer; } if (length > max_bytes) { length = max_bytes; } for (size_t i = 0; i < length; i++) { size_t pos = i * stride; buffer[pos] = format_hex_char(data[i] >> 4, base); buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); if (separator) { buffer[pos + 2] = separator; } } // With separator: overwrite last separator with '\0' // Without: write '\0' after last hex char buffer[length * stride - (separator ? 1 : 0)] = '\0'; return buffer; } char *uint32_to_str_unchecked(char *buf, uint32_t val) { if (val == 0) { *buf++ = '0'; return buf; } char *start = buf; while (val > 0) { *buf++ = '0' + (val % 10); val /= 10; } std::reverse(start, buf); return buf; } char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { return format_hex_internal(buffer, buffer_size, data, length, 0, 'a'); } const char *json_escape_into_buffer(std::span buf, StringRef value, bool short_control_escapes) { if (buf.empty()) return ""; // Reserve one byte for the null terminator. const size_t limit = buf.size() - 1; size_t pos = 0; for (char ch : value) { auto c = static_cast(ch); // Every short form is a backslash followed by a single character, so only that character is needed here. Keeping // it a char rather than a string avoids putting the sequences in read only data, which is RAM on the ESP8266. char escape = '\0'; switch (c) { case '"': escape = '"'; break; case '\\': escape = '\\'; break; case '\n': escape = 'n'; break; case '\r': escape = 'r'; break; case '\t': escape = 't'; break; case '\b': escape = 'b'; break; case '\f': escape = 'f'; break; default: break; } // " and \ are always written as two characters, but the control characters fall through to \u00XX when the // caller did not ask for the short forms. if (!short_control_escapes && c < 0x20) escape = '\0'; if (escape != '\0') { if (pos + 2 > limit) break; buf[pos++] = '\\'; buf[pos++] = escape; } else if (c < 0x20) { // Remaining control characters have no short form and must be written as \u00XX. The value is below 0x20, so // the two high hex digits are always zero. if (pos + JSON_ESCAPE_MAX_EXPANSION > limit) break; buf[pos++] = '\\'; buf[pos++] = 'u'; buf[pos++] = '0'; buf[pos++] = '0'; buf[pos++] = format_hex_char(static_cast(c >> 4)); buf[pos++] = format_hex_char(static_cast(c & 0x0F)); } else { if (pos + 1 > limit) break; buf[pos++] = static_cast(c); } } buf[pos] = '\0'; return buf.data(); } // format_hex (std::string returning overloads) moved to alloc_helpers.cpp char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { return format_hex_internal(buffer, buffer_size, data, length, separator, 'A'); } char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint16_t *data, size_t length, char separator) { if (length == 0 || buffer_size == 0) { if (buffer_size > 0) buffer[0] = '\0'; return buffer; } // With separator: each uint16_t needs 5 chars (4 hex + 1 sep), except last has no separator // Without separator: each uint16_t needs 4 chars, plus null terminator uint8_t stride = separator ? 5 : 4; size_t max_values = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride); if (max_values == 0) { buffer[0] = '\0'; return buffer; } if (length > max_values) { length = max_values; } for (size_t i = 0; i < length; i++) { size_t pos = i * stride; buffer[pos] = format_hex_pretty_char((data[i] & 0xF000) >> 12); buffer[pos + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8); buffer[pos + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4); buffer[pos + 3] = format_hex_pretty_char(data[i] & 0x000F); if (separator && i < length - 1) { buffer[pos + 4] = separator; } } buffer[length * stride - (separator ? 1 : 0)] = '\0'; return buffer; } // format_hex_pretty (all std::string returning overloads) moved to alloc_helpers.cpp char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { if (buffer_size == 0) { return buffer; } // Calculate max bytes we can format: each byte needs 8 chars size_t max_bytes = (buffer_size - 1) / 8; if (max_bytes == 0 || length == 0) { buffer[0] = '\0'; return buffer; } size_t bytes_to_format = std::min(length, max_bytes); for (size_t byte_idx = 0; byte_idx < bytes_to_format; byte_idx++) { for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) { buffer[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0'; } } buffer[bytes_to_format * 8] = '\0'; return buffer; } // format_bin moved to alloc_helpers.cpp ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { if (on == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("on")) == 0) return PARSE_ON; if (on != nullptr && strcasecmp(str, on) == 0) return PARSE_ON; if (off == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("off")) == 0) return PARSE_OFF; if (off != nullptr && strcasecmp(str, off) == 0) return PARSE_OFF; if (ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("toggle")) == 0) return PARSE_TOGGLE; return PARSE_NONE; } int8_t ilog10(float value) { float abs_val = fabsf(value); int8_t exp = 0; if (abs_val >= 10.0f) { while (abs_val >= 10.0f) { abs_val /= 10.0f; exp++; } } else if (abs_val < 1.0f) { while (abs_val < 1.0f) { abs_val *= 10.0f; exp--; } } return exp; } static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { float divisor; if (accuracy_decimals == -1) { divisor = 10.0f; } else if (accuracy_decimals == -2) { divisor = 100.0f; } else { divisor = pow10_int(-accuracy_decimals); } value = roundf(value / divisor) * divisor; accuracy_decimals = 0; } } // value_accuracy_to_string moved to alloc_helpers.cpp // Fast float-to-string for accuracy_decimals 0-3 (covers virtually all sensor usage). // Avoids snprintf("%.*f") which pulls in heavy float formatting machinery. // Caller must guarantee value is finite and |value| * mult fits in uint32_t. static size_t value_accuracy_to_buf_fast(char *buf, float value, int8_t accuracy_decimals, uint32_t mult) { char *p = buf; if (std::signbit(value)) { *p++ = '-'; value = -value; } // Cast to double for the multiply to match snprintf's rounding precision. // float*int loses bits at exact-half boundaries (e.g. 23.45f*10 = 234.5 in float, // but snprintf sees 234.500007... via double promotion and rounds differently). // llrint returns long long so the result fits even on 32-bit targets where // long is 32-bit; caller has already bounded |value * mult| to UINT32_MAX. uint32_t scaled = static_cast(llrint(static_cast(value) * mult)); p = uint32_to_str_unchecked(p, scaled / mult); if (accuracy_decimals > 0) { *p++ = '.'; p = frac_to_str_unchecked(p, scaled % mult, mult / 10); } *p = '\0'; return static_cast(p - buf); } size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals) { normalize_accuracy_decimals(value, accuracy_decimals); // Fast path for accuracy 0-3, finite values whose scaled magnitude fits in uint32_t. // For 3 decimals that's |value| < ~4.29e6; larger totals fall through to snprintf. if (accuracy_decimals <= 3 && std::isfinite(value)) { const uint32_t mult = small_pow10(accuracy_decimals); if (std::fabs(value) < static_cast(UINT32_MAX) / mult) { return value_accuracy_to_buf_fast(buf.data(), value, accuracy_decimals, mult); } } // Fallback for NaN/Inf/high accuracy/out-of-range int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, static_cast(value)); if (len < 0) return 0; return static_cast(len) >= buf.size() ? buf.size() - 1 : static_cast(len); } size_t value_accuracy_with_uom_to_buf(std::span buf, float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { size_t len = value_accuracy_to_buf(buf, value, accuracy_decimals); if (len == 0 || unit_of_measurement.empty()) { return len; } char *end = buf_append_sep_str(buf.data() + len, buf.size() - len, ' ', unit_of_measurement.c_str(), unit_of_measurement.size()); return static_cast(end - buf.data()); } int8_t step_to_accuracy_decimals(float step) { // Decimals needed to show the step at five significant digits, trailing zeros dropped. if (!std::isfinite(step) || step == 0.0f) return 0; float mantissa = std::fabs(step); int8_t decimals = 4; // decimals needed for five significant digits when mantissa is in [1, 10) while (mantissa >= 10.0f) { mantissa /= 10.0f; decimals--; } while (mantissa < 1.0f) { mantissa *= 10.0f; decimals++; } if (decimals <= 0) return 0; float scaled = mantissa * 10000.0f; auto digits = static_cast(scaled); if (scaled - static_cast(digits) >= 0.5f) digits++; while (decimals > 0 && digits % 10 == 0) { digits /= 10; decimals--; } return decimals; } static constexpr uint8_t INVALID_BASE64_CHAR = 0xFF; // 6-bit value of a base64 or base64url char, or INVALID_BASE64_CHAR. // No lookup table: .rodata lives in DRAM on ESP8266. static constexpr uint8_t base64_char_value(uint8_t c) { if (c >= 'A' && c <= 'Z') return c - 'A'; if (c >= 'a' && c <= 'z') return c - 'a' + 26; if (c >= '0' && c <= '9') return c - '0' + 52; // base64url variants: '-' maps to '+' (index 62), '_' maps to '/' (index 63) if (c == '+' || c == '-') return 62; if (c == '/' || c == '_') return 63; return INVALID_BASE64_CHAR; } // base64_encode (both overloads) moved to alloc_helpers.cpp size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) { return base64_decode(reinterpret_cast(encoded_string.data()), encoded_string.size(), buf, buf_len); } size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len) { size_t out = 0; uint32_t accum = 0; uint32_t bits = 0; // Stops at '=' or any non-alphabet char; leftover bits of a partial group are dropped. for (size_t in = 0; in < encoded_len; in++) { uint8_t value = base64_char_value(encoded_data[in]); if (value == INVALID_BASE64_CHAR) break; accum = (accum << 6) | value; bits += 6; if (bits >= 8) { bits -= 8; if (out == buf_len) { ESP_LOGW(TAG, "Base64 decode: buffer too small, truncating"); return out; } buf[out++] = static_cast(accum >> bits); } } return out; } // base64_decode (vector-returning overload) moved to alloc_helpers.cpp /// Decode base64/base64url string directly into vector of little-endian int32 values /// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted) /// @param out Output vector (cleared and filled with decoded int32 values) /// @return true if successful, false if decode failed or invalid size bool base64_decode_int32_vector(const std::string &base64, std::vector &out) { // Decode in chunks to minimize stack usage constexpr size_t chunk_bytes = 48; // 12 int32 values constexpr size_t chunk_chars = 64; // 48 * 4/3 = 64 chars uint8_t chunk[chunk_bytes]; out.clear(); const uint8_t *input = reinterpret_cast(base64.data()); size_t remaining = base64.size(); size_t pos = 0; while (remaining > 0) { size_t chars_to_decode = std::min(remaining, chunk_chars); size_t decoded_len = base64_decode(input + pos, chars_to_decode, chunk, chunk_bytes); if (decoded_len == 0) return false; // Parse little-endian int32 values for (size_t i = 0; i + 3 < decoded_len; i += 4) { int32_t timing = static_cast(encode_uint32(chunk[i + 3], chunk[i + 2], chunk[i + 1], chunk[i])); out.push_back(timing); } // Check for incomplete int32 in last chunk if (remaining <= chunk_chars && (decoded_len % 4) != 0) return false; pos += chars_to_decode; remaining -= chars_to_decode; } return !out.empty(); } // Colors void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) { float max_color_value = std::max({red, green, blue}); float min_color_value = std::min({red, green, blue}); float delta = max_color_value - min_color_value; if (delta == 0) { hue = 0; } else if (max_color_value == red) { hue = int(fmodf((60.0f * ((green - blue) / delta)) + 360.0f, 360.0f)); } else if (max_color_value == green) { hue = int(fmodf((60.0f * ((blue - red) / delta)) + 120.0f, 360.0f)); } else if (max_color_value == blue) { hue = int(fmodf((60.0f * ((red - green) / delta)) + 240.0f, 360.0f)); } if (max_color_value == 0) { saturation = 0; } else { saturation = delta / max_color_value; } value = max_color_value; } void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green, float &blue) { float chroma = value * saturation; float hue_prime = fmodf(hue / 60.0f, 6.0f); float intermediate = chroma * (1.0f - fabsf(fmodf(hue_prime, 2.0f) - 1.0f)); float delta = value - chroma; if (0 <= hue_prime && hue_prime < 1) { red = chroma; green = intermediate; blue = 0; } else if (1 <= hue_prime && hue_prime < 2) { red = intermediate; green = chroma; blue = 0; } else if (2 <= hue_prime && hue_prime < 3) { red = 0; green = chroma; blue = intermediate; } else if (3 <= hue_prime && hue_prime < 4) { red = 0; green = intermediate; blue = chroma; } else if (4 <= hue_prime && hue_prime < 5) { red = intermediate; green = 0; blue = chroma; } else if (5 <= hue_prime && hue_prime < 6) { red = chroma; green = 0; blue = intermediate; } else { red = 0; green = 0; blue = 0; } red += delta; green += delta; blue += delta; } uint8_t HighFrequencyLoopRequester::num_requests = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void HighFrequencyLoopRequester::start() { if (this->started_) return; num_requests++; this->started_ = true; } void HighFrequencyLoopRequester::stop() { if (!this->started_) return; num_requests--; this->started_ = false; } // get_mac_address, get_mac_address_pretty moved to alloc_helpers.cpp void get_mac_address_into_buffer(std::span buf) { uint8_t mac[MAC_ADDRESS_SIZE]; get_mac_address_raw(mac); format_mac_addr_lower_no_sep(mac, buf.data()); } const char *get_mac_address_pretty_into_buffer(std::span buf) { uint8_t mac[MAC_ADDRESS_SIZE]; get_mac_address_raw(mac); format_mac_addr_upper(mac, buf.data()); return buf.data(); } #ifndef USE_ESP32 bool has_custom_mac_address() { return false; } #endif bool mac_address_is_valid(const uint8_t *mac) { bool is_all_zeros = true; bool is_all_ones = true; for (uint8_t i = 0; i < 6; i++) { if (mac[i] != 0) { is_all_zeros = false; } if (mac[i] != 0xFF) { is_all_ones = false; } } if (is_all_zeros || is_all_ones) { return false; } // Reject multicast MACs (bit 0 of first byte set) - device MACs must be unicast. // This catches garbage data from corrupted eFuse custom MAC areas, which often // has random values that would otherwise pass the all-zeros/all-ones check. if (mac[0] & 0x01) { return false; } return true; } void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability uint32_t start = micros(); constexpr uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known if (us > lag) { delay((us - lag) / 1000UL); // note: in disabled-interrupt contexts delay() won't actually sleep while (micros() - start < us - lag) delay(1); // in those cases, this loop allows to yield for BT/WiFi stack tasks } while (micros() - start < us) // fine delay the remaining usecs ; } } // namespace esphome