mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'drop_unique_id' into integration
This commit is contained in:
		| @@ -324,6 +324,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw | ||||
| esphome/components/nfc/* @jesserockz @kbx81 | ||||
| esphome/components/noblex/* @AGalfra | ||||
| esphome/components/npi19/* @bakerkj | ||||
| esphome/components/nrf52/* @tomaszduda23 | ||||
| esphome/components/number/* @esphome/core | ||||
| esphome/components/one_wire/* @ssieb | ||||
| esphome/components/online_image/* @clydebarrow @guillempages | ||||
| @@ -536,5 +537,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix | ||||
| esphome/components/xl9535/* @mreditor97 | ||||
| esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | ||||
| esphome/components/xxtea/* @clydebarrow | ||||
| esphome/components/zephyr/* @tomaszduda23 | ||||
| esphome/components/zhlt01/* @cfeenstra1024 | ||||
| esphome/components/zio_ultrasonic/* @kahrendt | ||||
|   | ||||
| @@ -51,82 +51,83 @@ SAMPLING_MODES = { | ||||
|     "max": sampling_mode.MAX, | ||||
| } | ||||
|  | ||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | ||||
| adc2_channel_t = cg.global_ns.enum("adc2_channel_t") | ||||
| adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True) | ||||
|  | ||||
| adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True) | ||||
|  | ||||
| # pin to adc1 channel mapping | ||||
| # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h | ||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32: { | ||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         36: adc_channel_t.ADC_CHANNEL_0, | ||||
|         37: adc_channel_t.ADC_CHANNEL_1, | ||||
|         38: adc_channel_t.ADC_CHANNEL_2, | ||||
|         39: adc_channel_t.ADC_CHANNEL_3, | ||||
|         32: adc_channel_t.ADC_CHANNEL_4, | ||||
|         33: adc_channel_t.ADC_CHANNEL_5, | ||||
|         34: adc_channel_t.ADC_CHANNEL_6, | ||||
|         35: adc_channel_t.ADC_CHANNEL_7, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C2: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         0: adc_channel_t.ADC_CHANNEL_0, | ||||
|         1: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         3: adc_channel_t.ADC_CHANNEL_3, | ||||
|         4: adc_channel_t.ADC_CHANNEL_4, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C3: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         0: adc_channel_t.ADC_CHANNEL_0, | ||||
|         1: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         3: adc_channel_t.ADC_CHANNEL_3, | ||||
|         4: adc_channel_t.ADC_CHANNEL_4, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C6: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         0: adc_channel_t.ADC_CHANNEL_0, | ||||
|         1: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         3: adc_channel_t.ADC_CHANNEL_3, | ||||
|         4: adc_channel_t.ADC_CHANNEL_4, | ||||
|         5: adc_channel_t.ADC_CHANNEL_5, | ||||
|         6: adc_channel_t.ADC_CHANNEL_6, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32H2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|         6: adc_channel_t.ADC_CHANNEL_5, | ||||
|         7: adc_channel_t.ADC_CHANNEL_6, | ||||
|         8: adc_channel_t.ADC_CHANNEL_7, | ||||
|         9: adc_channel_t.ADC_CHANNEL_8, | ||||
|         10: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S3: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|         6: adc_channel_t.ADC_CHANNEL_5, | ||||
|         7: adc_channel_t.ADC_CHANNEL_6, | ||||
|         8: adc_channel_t.ADC_CHANNEL_7, | ||||
|         9: adc_channel_t.ADC_CHANNEL_8, | ||||
|         10: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| @@ -135,24 +136,24 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
| ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32: { | ||||
|         4: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         0: adc2_channel_t.ADC2_CHANNEL_1, | ||||
|         2: adc2_channel_t.ADC2_CHANNEL_2, | ||||
|         15: adc2_channel_t.ADC2_CHANNEL_3, | ||||
|         13: adc2_channel_t.ADC2_CHANNEL_4, | ||||
|         12: adc2_channel_t.ADC2_CHANNEL_5, | ||||
|         14: adc2_channel_t.ADC2_CHANNEL_6, | ||||
|         27: adc2_channel_t.ADC2_CHANNEL_7, | ||||
|         25: adc2_channel_t.ADC2_CHANNEL_8, | ||||
|         26: adc2_channel_t.ADC2_CHANNEL_9, | ||||
|         4: adc_channel_t.ADC_CHANNEL_0, | ||||
|         0: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         15: adc_channel_t.ADC_CHANNEL_3, | ||||
|         13: adc_channel_t.ADC_CHANNEL_4, | ||||
|         12: adc_channel_t.ADC_CHANNEL_5, | ||||
|         14: adc_channel_t.ADC_CHANNEL_6, | ||||
|         27: adc_channel_t.ADC_CHANNEL_7, | ||||
|         25: adc_channel_t.ADC_CHANNEL_8, | ||||
|         26: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C2: { | ||||
|         5: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         5: adc_channel_t.ADC_CHANNEL_0, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C3: { | ||||
|         5: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         5: adc_channel_t.ADC_CHANNEL_0, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C6: {},  # no ADC2 | ||||
| @@ -160,29 +161,29 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | ||||
|     VARIANT_ESP32H2: {},  # no ADC2 | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S2: { | ||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, | ||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, | ||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, | ||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, | ||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, | ||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, | ||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, | ||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, | ||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, | ||||
|         11: adc_channel_t.ADC_CHANNEL_0, | ||||
|         12: adc_channel_t.ADC_CHANNEL_1, | ||||
|         13: adc_channel_t.ADC_CHANNEL_2, | ||||
|         14: adc_channel_t.ADC_CHANNEL_3, | ||||
|         15: adc_channel_t.ADC_CHANNEL_4, | ||||
|         16: adc_channel_t.ADC_CHANNEL_5, | ||||
|         17: adc_channel_t.ADC_CHANNEL_6, | ||||
|         18: adc_channel_t.ADC_CHANNEL_7, | ||||
|         19: adc_channel_t.ADC_CHANNEL_8, | ||||
|         20: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S3: { | ||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, | ||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, | ||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, | ||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, | ||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, | ||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, | ||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, | ||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, | ||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, | ||||
|         11: adc_channel_t.ADC_CHANNEL_0, | ||||
|         12: adc_channel_t.ADC_CHANNEL_1, | ||||
|         13: adc_channel_t.ADC_CHANNEL_2, | ||||
|         14: adc_channel_t.ADC_CHANNEL_3, | ||||
|         15: adc_channel_t.ADC_CHANNEL_4, | ||||
|         16: adc_channel_t.ADC_CHANNEL_5, | ||||
|         17: adc_channel_t.ADC_CHANNEL_6, | ||||
|         18: adc_channel_t.ADC_CHANNEL_7, | ||||
|         19: adc_channel_t.ADC_CHANNEL_8, | ||||
|         20: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -3,12 +3,15 @@ | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #include <esp_adc_cal.h> | ||||
| #include "driver/adc.h" | ||||
| #endif  // USE_ESP32 | ||||
| #include "esp_adc/adc_cali.h" | ||||
| #include "esp_adc/adc_cali_scheme.h" | ||||
| #include "esp_adc/adc_oneshot.h" | ||||
| #include "hal/adc_types.h"  // This defines ADC_CHANNEL_MAX | ||||
| #endif                      // USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
| @@ -49,33 +52,72 @@ class Aggregator { | ||||
|  | ||||
| class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { | ||||
|  public: | ||||
|   /// Update the sensor's state by reading the current ADC value. | ||||
|   /// This method is called periodically based on the update interval. | ||||
|   void update() override; | ||||
|  | ||||
|   /// Set up the ADC sensor by initializing hardware and calibration parameters. | ||||
|   /// This method is called once during device initialization. | ||||
|   void setup() override; | ||||
|  | ||||
|   /// Output the configuration details of the ADC sensor for debugging purposes. | ||||
|   /// This method is called during the ESPHome setup process to log the configuration. | ||||
|   void dump_config() override; | ||||
|  | ||||
|   /// Return the setup priority for this component. | ||||
|   /// Components with higher priority are initialized earlier during setup. | ||||
|   /// @return A float representing the setup priority. | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|   /// Set the GPIO pin to be used by the ADC sensor. | ||||
|   /// @param pin Pointer to an InternalGPIOPin representing the ADC input pin. | ||||
|   void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } | ||||
|  | ||||
|   /// Enable or disable the output of raw ADC values (unprocessed data). | ||||
|   /// @param output_raw Boolean indicating whether to output raw ADC values (true) or processed values (false). | ||||
|   void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } | ||||
|  | ||||
|   /// Set the number of samples to be taken for ADC readings to improve accuracy. | ||||
|   /// A higher sample count reduces noise but increases the reading time. | ||||
|   /// @param sample_count The number of samples (e.g., 1, 4, 8). | ||||
|   void set_sample_count(uint8_t sample_count); | ||||
|  | ||||
|   /// Set the sampling mode for how multiple ADC samples are combined into a single measurement. | ||||
|   /// | ||||
|   /// When multiple samples are taken (controlled by set_sample_count), they can be combined | ||||
|   /// in one of three ways: | ||||
|   ///   - SamplingMode::AVG: Compute the average (default) | ||||
|   ///   - SamplingMode::MIN: Use the lowest sample value | ||||
|   ///   - SamplingMode::MAX: Use the highest sample value | ||||
|   /// @param sampling_mode The desired sampling mode to use for aggregating ADC samples. | ||||
|   void set_sampling_mode(SamplingMode sampling_mode); | ||||
|  | ||||
|   /// Perform a single ADC sampling operation and return the measured value. | ||||
|   /// This function handles raw readings, calibration, and averaging as needed. | ||||
|   /// @return The sampled value as a float. | ||||
|   float sample() override; | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   /// Set the attenuation for this pin. Only available on the ESP32. | ||||
|   /// Set the ADC attenuation level to adjust the input voltage range. | ||||
|   /// This determines how the ADC interprets input voltages, allowing for greater precision | ||||
|   /// or the ability to measure higher voltages depending on the chosen attenuation level. | ||||
|   /// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11). | ||||
|   void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } | ||||
|   void set_channel1(adc1_channel_t channel) { | ||||
|     this->channel1_ = channel; | ||||
|     this->channel2_ = ADC2_CHANNEL_MAX; | ||||
|   } | ||||
|   void set_channel2(adc2_channel_t channel) { | ||||
|     this->channel2_ = channel; | ||||
|     this->channel1_ = ADC1_CHANNEL_MAX; | ||||
|  | ||||
|   /// Configure the ADC to use a specific channel on ADC1. | ||||
|   /// This sets the channel for single-shot or continuous ADC measurements. | ||||
|   /// @param channel The ADC1 channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc. | ||||
|   void set_channel(adc_unit_t unit, adc_channel_t channel) { | ||||
|     this->adc_unit_ = unit; | ||||
|     this->channel_ = channel; | ||||
|   } | ||||
|  | ||||
|   /// Set whether autoranging should be enabled for the ADC. | ||||
|   /// Autoranging automatically adjusts the attenuation level to handle a wide range of input voltages. | ||||
|   /// @param autorange Boolean indicating whether to enable autoranging. | ||||
|   void set_autorange(bool autorange) { this->autorange_ = autorange; } | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
|   /// Update ADC values | ||||
|   void update() override; | ||||
|   /// Setup ADC | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   /// `HARDWARE_LATE` setup priority | ||||
|   float get_setup_priority() const override; | ||||
|   void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } | ||||
|   void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } | ||||
|   void set_sample_count(uint8_t sample_count); | ||||
|   void set_sampling_mode(SamplingMode sampling_mode); | ||||
|   float sample() override; | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   void set_is_temperature() { this->is_temperature_ = true; } | ||||
| #endif  // USE_RP2040 | ||||
| @@ -86,17 +128,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|   InternalGPIOPin *pin_; | ||||
|   SamplingMode sampling_mode_{SamplingMode::AVG}; | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   float sample_autorange_(); | ||||
|   float sample_fixed_attenuation_(); | ||||
|   bool autorange_{false}; | ||||
|   adc_oneshot_unit_handle_t adc_handle_{nullptr}; | ||||
|   adc_cali_handle_t calibration_handle_{nullptr}; | ||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||
|   adc_channel_t channel_; | ||||
|   adc_unit_t adc_unit_; | ||||
|   struct SetupFlags { | ||||
|     uint8_t init_complete : 1; | ||||
|     uint8_t config_complete : 1; | ||||
|     uint8_t handle_init_complete : 1; | ||||
|     uint8_t calibration_complete : 1; | ||||
|     uint8_t reserved : 4; | ||||
|   } setup_flags_{}; | ||||
|   static adc_oneshot_unit_handle_t shared_adc_handles[2]; | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   bool is_temperature_{false}; | ||||
| #endif  // USE_RP2040 | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||
|   adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; | ||||
|   adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; | ||||
|   bool autorange_{false}; | ||||
|   esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {}; | ||||
| #endif  // USE_ESP32 | ||||
| }; | ||||
|  | ||||
| }  // namespace adc | ||||
|   | ||||
| @@ -8,145 +8,308 @@ namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc.esp32"; | ||||
|  | ||||
| static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); | ||||
| adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr}; | ||||
|  | ||||
| #ifndef SOC_ADC_RTC_MAX_BITWIDTH | ||||
| #if USE_ESP32_VARIANT_ESP32S2 | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; | ||||
| #else | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; | ||||
| #endif  // USE_ESP32_VARIANT_ESP32S2 | ||||
| #endif  // SOC_ADC_RTC_MAX_BITWIDTH | ||||
|  | ||||
| static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; | ||||
| static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
|  | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); | ||||
|     if (!this->autorange_) { | ||||
|       adc1_config_channel_atten(this->channel1_, this->attenuation_); | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     if (!this->autorange_) { | ||||
|       adc2_config_channel_atten(this->channel2_, this->attenuation_); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { | ||||
|     auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; | ||||
|     auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, | ||||
|                                               1100,  // default vref | ||||
|                                               &this->cal_characteristics_[i]); | ||||
|     switch (cal_value) { | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_VREF: | ||||
|         ESP_LOGV(TAG, "Using eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_TP: | ||||
|         ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_DEFAULT_VREF: | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
| const LogString *attenuation_to_str(adc_atten_t attenuation) { | ||||
|   switch (attenuation) { | ||||
|     case ADC_ATTEN_DB_0: | ||||
|       return LOG_STR("0 dB"); | ||||
|     case ADC_ATTEN_DB_2_5: | ||||
|       return LOG_STR("2.5 dB"); | ||||
|     case ADC_ATTEN_DB_6: | ||||
|       return LOG_STR("6 dB"); | ||||
|     case ADC_ATTEN_DB_12_COMPAT: | ||||
|       return LOG_STR("12 dB"); | ||||
|     default: | ||||
|       return LOG_STR("Unknown Attenuation"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   static const char *const ATTEN_AUTO_STR = "auto"; | ||||
|   static const char *const ATTEN_0DB_STR = "0 db"; | ||||
|   static const char *const ATTEN_2_5DB_STR = "2.5 db"; | ||||
|   static const char *const ATTEN_6DB_STR = "6 db"; | ||||
|   static const char *const ATTEN_12DB_STR = "12 db"; | ||||
|   const char *atten_str = ATTEN_AUTO_STR; | ||||
| const LogString *adc_unit_to_str(adc_unit_t unit) { | ||||
|   switch (unit) { | ||||
|     case ADC_UNIT_1: | ||||
|       return LOG_STR("ADC1"); | ||||
|     case ADC_UNIT_2: | ||||
|       return LOG_STR("ADC2"); | ||||
|     default: | ||||
|       return LOG_STR("Unknown ADC Unit"); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|  | ||||
|   if (!this->autorange_) { | ||||
|     switch (this->attenuation_) { | ||||
|       case ADC_ATTEN_DB_0: | ||||
|         atten_str = ATTEN_0DB_STR; | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_2_5: | ||||
|         atten_str = ATTEN_2_5DB_STR; | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_6: | ||||
|         atten_str = ATTEN_6DB_STR; | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_12_COMPAT: | ||||
|         atten_str = ATTEN_12DB_STR; | ||||
|         break; | ||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|         break; | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
|   // Check if another sensor already initialized this ADC unit | ||||
|   if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) { | ||||
|     adc_oneshot_unit_init_cfg_t init_config = {};  // Zero initialize | ||||
|     init_config.unit_id = this->adc_unit_; | ||||
|     init_config.ulp_mode = ADC_ULP_MODE_DISABLE; | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 | ||||
|     init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; | ||||
| #endif  // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 | ||||
|     esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   this->adc_handle_ = ADCSensor::shared_adc_handles[this->adc_unit_]; | ||||
|  | ||||
|   this->setup_flags_.handle_init_complete = true; | ||||
|  | ||||
|   adc_oneshot_chan_cfg_t config = { | ||||
|       .atten = this->attenuation_, | ||||
|       .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||
|   }; | ||||
|   esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Error configuring channel: %d", err); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   this->setup_flags_.config_complete = true; | ||||
|  | ||||
|   // Initialize ADC calibration | ||||
|   if (this->calibration_handle_ == nullptr) { | ||||
|     adc_cali_handle_t handle = nullptr; | ||||
|     esp_err_t err; | ||||
|  | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|     // RISC-V variants and S3 use curve fitting calibration | ||||
|     adc_cali_curve_fitting_config_t cali_config = {};  // Zero initialize first | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
|     cali_config.chan = this->channel_; | ||||
| #endif  // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
|     cali_config.unit_id = this->adc_unit_; | ||||
|     cali_config.atten = this->attenuation_; | ||||
|     cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; | ||||
|  | ||||
|     err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); | ||||
|     if (err == ESP_OK) { | ||||
|       this->calibration_handle_ = handle; | ||||
|       this->setup_flags_.calibration_complete = true; | ||||
|       ESP_LOGV(TAG, "Using curve fitting calibration"); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); | ||||
|       this->setup_flags_.calibration_complete = false; | ||||
|     } | ||||
| #else  // Other ESP32 variants use line fitting calibration | ||||
|     adc_cali_line_fitting_config_t cali_config = { | ||||
|       .unit_id = this->adc_unit_, | ||||
|       .atten = this->attenuation_, | ||||
|       .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32S2) | ||||
|       .default_vref = 1100,  // Default reference voltage in mV | ||||
| #endif  // !defined(USE_ESP32_VARIANT_ESP32S2) | ||||
|     }; | ||||
|     err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); | ||||
|     if (err == ESP_OK) { | ||||
|       this->calibration_handle_ = handle; | ||||
|       this->setup_flags_.calibration_complete = true; | ||||
|       ESP_LOGV(TAG, "Using line fitting calibration"); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); | ||||
|       this->setup_flags_.calibration_complete = false; | ||||
|     } | ||||
| #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C6 || ESP32S3 || ESP32H2 | ||||
|   } | ||||
|  | ||||
|   this->setup_flags_.init_complete = true; | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Attenuation: %s\n" | ||||
|                 "  Samples: %i\n" | ||||
|                 "  Channel:       %d\n" | ||||
|                 "  Unit:          %s\n" | ||||
|                 "  Attenuation:   %s\n" | ||||
|                 "  Samples:       %i\n" | ||||
|                 "  Sampling mode: %s", | ||||
|                 atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); | ||||
|                 this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), | ||||
|                 this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, | ||||
|                 LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); | ||||
|  | ||||
|   ESP_LOGCONFIG( | ||||
|       TAG, | ||||
|       "  Setup Status:\n" | ||||
|       "    Handle Init:  %s\n" | ||||
|       "    Config:       %s\n" | ||||
|       "    Calibration:  %s\n" | ||||
|       "    Overall Init: %s", | ||||
|       this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED", | ||||
|       this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED"); | ||||
|  | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ADCSensor::sample() { | ||||
|   if (!this->autorange_) { | ||||
|     auto aggr = Aggregator(this->sampling_mode_); | ||||
|   if (this->autorange_) { | ||||
|     return this->sample_autorange_(); | ||||
|   } else { | ||||
|     return this->sample_fixed_attenuation_(); | ||||
|   } | ||||
| } | ||||
|  | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       int raw = -1; | ||||
|       if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|         raw = adc1_get_raw(this->channel1_); | ||||
|       } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); | ||||
|       } | ||||
|       if (raw == -1) { | ||||
|         return NAN; | ||||
|       } | ||||
| float ADCSensor::sample_fixed_attenuation_() { | ||||
|   auto aggr = Aggregator(this->sampling_mode_); | ||||
|  | ||||
|       aggr.add_sample(raw); | ||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|     int raw; | ||||
|     esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); | ||||
|  | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "ADC read failed with error %d", err); | ||||
|       continue; | ||||
|     } | ||||
|     if (this->output_raw_) { | ||||
|       return aggr.aggregate(); | ||||
|  | ||||
|     if (raw == -1) { | ||||
|       ESP_LOGW(TAG, "Invalid ADC reading"); | ||||
|       continue; | ||||
|     } | ||||
|     uint32_t mv = | ||||
|         esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); | ||||
|     return mv / 1000.0f; | ||||
|  | ||||
|     aggr.add_sample(raw); | ||||
|   } | ||||
|  | ||||
|   int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; | ||||
|   uint32_t final_value = aggr.aggregate(); | ||||
|  | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); | ||||
|     raw12 = adc1_get_raw(this->channel1_); | ||||
|     if (raw12 < ADC_MAX) { | ||||
|       adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); | ||||
|       raw6 = adc1_get_raw(this->channel1_); | ||||
|       if (raw6 < ADC_MAX) { | ||||
|         adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); | ||||
|         raw2 = adc1_get_raw(this->channel1_); | ||||
|         if (raw2 < ADC_MAX) { | ||||
|           adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); | ||||
|           raw0 = adc1_get_raw(this->channel1_); | ||||
|         } | ||||
|   if (this->output_raw_) { | ||||
|     return final_value; | ||||
|   } | ||||
|  | ||||
|   if (this->calibration_handle_ != nullptr) { | ||||
|     int voltage_mv; | ||||
|     esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv); | ||||
|     if (err == ESP_OK) { | ||||
|       return voltage_mv / 1000.0f; | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); | ||||
|       if (this->calibration_handle_ != nullptr) { | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|         adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||
| #else   // Other ESP32 variants use line fitting calibration | ||||
|         adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||
| #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C6 || ESP32S3 || ESP32H2 | ||||
|         this->calibration_handle_ = nullptr; | ||||
|       } | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT); | ||||
|     adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); | ||||
|     if (raw12 < ADC_MAX) { | ||||
|       adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6); | ||||
|       adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); | ||||
|       if (raw6 < ADC_MAX) { | ||||
|         adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); | ||||
|         if (raw2 < ADC_MAX) { | ||||
|           adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); | ||||
|           adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); | ||||
|         } | ||||
|   } | ||||
|  | ||||
|   return final_value * 3.3f / 4095.0f; | ||||
| } | ||||
|  | ||||
| float ADCSensor::sample_autorange_() { | ||||
|   // Auto-range mode | ||||
|   auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> { | ||||
|     // First reconfigure the attenuation for this reading | ||||
|     adc_oneshot_chan_cfg_t config = { | ||||
|         .atten = atten, | ||||
|         .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||
|     }; | ||||
|  | ||||
|     esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config); | ||||
|  | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "Error configuring ADC channel for autorange: %d", err); | ||||
|       return {-1, 0.0f}; | ||||
|     } | ||||
|  | ||||
|     // Need to recalibrate for the new attenuation | ||||
|     if (this->calibration_handle_ != nullptr) { | ||||
|       // Delete old calibration handle | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|       adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||
| #else | ||||
|       adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||
| #endif | ||||
|       this->calibration_handle_ = nullptr; | ||||
|     } | ||||
|  | ||||
|     // Create new calibration handle for this attenuation | ||||
|     adc_cali_handle_t handle = nullptr; | ||||
|  | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|     adc_cali_curve_fitting_config_t cali_config = {}; | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
|     cali_config.chan = this->channel_; | ||||
| #endif | ||||
|     cali_config.unit_id = this->adc_unit_; | ||||
|     cali_config.atten = atten; | ||||
|     cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; | ||||
|  | ||||
|     err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); | ||||
| #else | ||||
|     adc_cali_line_fitting_config_t cali_config = { | ||||
|       .unit_id = this->adc_unit_, | ||||
|       .atten = atten, | ||||
|       .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32S2) | ||||
|       .default_vref = 1100, | ||||
| #endif | ||||
|     }; | ||||
|     err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); | ||||
| #endif | ||||
|  | ||||
|     int raw; | ||||
|     err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); | ||||
|  | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); | ||||
|       if (handle != nullptr) { | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|         adc_cali_delete_scheme_curve_fitting(handle); | ||||
| #else | ||||
|         adc_cali_delete_scheme_line_fitting(handle); | ||||
| #endif | ||||
|       } | ||||
|       return {-1, 0.0f}; | ||||
|     } | ||||
|  | ||||
|     float voltage = 0.0f; | ||||
|     if (handle != nullptr) { | ||||
|       int voltage_mv; | ||||
|       err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv); | ||||
|       if (err == ESP_OK) { | ||||
|         voltage = voltage_mv / 1000.0f; | ||||
|       } else { | ||||
|         voltage = raw * 3.3f / 4095.0f; | ||||
|       } | ||||
|       // Clean up calibration handle | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|       adc_cali_delete_scheme_curve_fitting(handle); | ||||
| #else | ||||
|       adc_cali_delete_scheme_line_fitting(handle); | ||||
| #endif | ||||
|     } else { | ||||
|       voltage = raw * 3.3f / 4095.0f; | ||||
|     } | ||||
|  | ||||
|     return {raw, voltage}; | ||||
|   }; | ||||
|  | ||||
|   auto [raw12, mv12] = read_atten(ADC_ATTEN_DB_12); | ||||
|   if (raw12 == -1) { | ||||
|     ESP_LOGE(TAG, "Failed to read ADC in autorange mode"); | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   int raw6 = 4095, raw2 = 4095, raw0 = 4095; | ||||
|   float mv6 = 0, mv2 = 0, mv0 = 0; | ||||
|  | ||||
|   if (raw12 < 4095) { | ||||
|     auto [raw6_val, mv6_val] = read_atten(ADC_ATTEN_DB_6); | ||||
|     raw6 = raw6_val; | ||||
|     mv6 = mv6_val; | ||||
|  | ||||
|     if (raw6 < 4095 && raw6 != -1) { | ||||
|       auto [raw2_val, mv2_val] = read_atten(ADC_ATTEN_DB_2_5); | ||||
|       raw2 = raw2_val; | ||||
|       mv2 = mv2_val; | ||||
|  | ||||
|       if (raw2 < 4095 && raw2 != -1) { | ||||
|         auto [raw0_val, mv0_val] = read_atten(ADC_ATTEN_DB_0); | ||||
|         raw0 = raw0_val; | ||||
|         mv0 = mv0_val; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -155,19 +318,19 @@ float ADCSensor::sample() { | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); | ||||
|   uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); | ||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); | ||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); | ||||
|  | ||||
|   uint32_t c12 = std::min(raw12, ADC_HALF); | ||||
|   uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); | ||||
|   uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); | ||||
|   uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); | ||||
|   const int adc_half = 2048; | ||||
|   uint32_t c12 = std::min(raw12, adc_half); | ||||
|   uint32_t c6 = adc_half - std::abs(raw6 - adc_half); | ||||
|   uint32_t c2 = adc_half - std::abs(raw2 - adc_half); | ||||
|   uint32_t c0 = std::min(4095 - raw0, adc_half); | ||||
|   uint32_t csum = c12 + c6 + c2 + c0; | ||||
|  | ||||
|   uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||
|   return mv_scaled / (float) (csum * 1000U); | ||||
|   if (csum == 0) { | ||||
|     ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; | ||||
| } | ||||
|  | ||||
| }  // namespace adc | ||||
|   | ||||
| @@ -10,13 +10,11 @@ from esphome.const import ( | ||||
|     CONF_NUMBER, | ||||
|     CONF_PIN, | ||||
|     CONF_RAW, | ||||
|     CONF_WIFI, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| from . import ( | ||||
|     ATTENUATION_MODES, | ||||
| @@ -24,6 +22,7 @@ from . import ( | ||||
|     ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, | ||||
|     SAMPLING_MODES, | ||||
|     adc_ns, | ||||
|     adc_unit_t, | ||||
|     validate_adc_pin, | ||||
| ) | ||||
|  | ||||
| @@ -57,21 +56,6 @@ def validate_config(config): | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def final_validate_config(config): | ||||
|     if CORE.is_esp32: | ||||
|         variant = get_esp32_variant() | ||||
|         if ( | ||||
|             CONF_WIFI in fv.full_config.get() | ||||
|             and config[CONF_PIN][CONF_NUMBER] | ||||
|             in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] | ||||
|         ): | ||||
|             raise cv.Invalid( | ||||
|                 f"{variant} doesn't support ADC on this pin when Wi-Fi is configured" | ||||
|             ) | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| ADCSensor = adc_ns.class_( | ||||
|     "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler | ||||
| ) | ||||
| @@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All( | ||||
|     validate_config, | ||||
| ) | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = final_validate_config | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
| @@ -119,13 +101,13 @@ async def to_code(config): | ||||
|     cg.add(var.set_sample_count(config[CONF_SAMPLES])) | ||||
|     cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) | ||||
|  | ||||
|     if attenuation := config.get(CONF_ATTENUATION): | ||||
|         if attenuation == "auto": | ||||
|             cg.add(var.set_autorange(cg.global_ns.true)) | ||||
|         else: | ||||
|             cg.add(var.set_attenuation(attenuation)) | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         if attenuation := config.get(CONF_ATTENUATION): | ||||
|             if attenuation == "auto": | ||||
|                 cg.add(var.set_autorange(cg.global_ns.true)) | ||||
|             else: | ||||
|                 cg.add(var.set_attenuation(attenuation)) | ||||
|  | ||||
|         variant = get_esp32_variant() | ||||
|         pin_num = config[CONF_PIN][CONF_NUMBER] | ||||
|         if ( | ||||
| @@ -133,10 +115,10 @@ async def to_code(config): | ||||
|             and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] | ||||
|         ): | ||||
|             chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] | ||||
|             cg.add(var.set_channel1(chan)) | ||||
|             cg.add(var.set_channel(adc_unit_t.ADC_UNIT_1, chan)) | ||||
|         elif ( | ||||
|             variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL | ||||
|             and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] | ||||
|         ): | ||||
|             chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] | ||||
|             cg.add(var.set_channel2(chan)) | ||||
|             cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan)) | ||||
|   | ||||
| @@ -16,6 +16,8 @@ class UserServiceDescriptor { | ||||
|   virtual ListEntitiesServicesResponse encode_list_service_response() = 0; | ||||
|  | ||||
|   virtual bool execute_service(const ExecuteServiceRequest &req) = 0; | ||||
|  | ||||
|   bool is_internal() { return false; } | ||||
| }; | ||||
|  | ||||
| template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg); | ||||
|   | ||||
| @@ -3,8 +3,6 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/as3935/as3935.h" | ||||
| #include "esphome/components/spi/spi.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace as3935_spi { | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|  | ||||
| CONF_BYTE_ORDER = "byte_order" | ||||
| CONF_COLOR_DEPTH = "color_depth" | ||||
| CONF_DRAW_ROUNDING = "draw_rounding" | ||||
| CONF_ON_STATE_CHANGE = "on_state_change" | ||||
| CONF_REQUEST_HEADERS = "request_headers" | ||||
|   | ||||
| @@ -21,6 +21,11 @@ from esphome.components.libretiny.const import ( | ||||
|     COMPONENT_LN882X, | ||||
|     COMPONENT_RTL87XX, | ||||
| ) | ||||
| from esphome.components.zephyr import ( | ||||
|     zephyr_add_cdc_acm, | ||||
|     zephyr_add_overlay, | ||||
|     zephyr_add_prj_conf, | ||||
| ) | ||||
| from esphome.config_helpers import filter_source_files_from_platform | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -41,6 +46,7 @@ from esphome.const import ( | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_LN882X, | ||||
|     PLATFORM_NRF52, | ||||
|     PLATFORM_RP2040, | ||||
|     PLATFORM_RTL87XX, | ||||
|     PlatformFramework, | ||||
| @@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] | ||||
|  | ||||
| UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] | ||||
|  | ||||
| UART_SELECTION_NRF52 = [USB_CDC, UART0] | ||||
|  | ||||
| HARDWARE_UART_TO_UART_SELECTION = { | ||||
|     UART0: logger_ns.UART_SELECTION_UART0, | ||||
|     UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, | ||||
| @@ -167,6 +175,8 @@ def uart_selection(value): | ||||
|             return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) | ||||
|     if CORE.is_host: | ||||
|         raise cv.Invalid("Uart selection not valid for host platform") | ||||
|     if CORE.is_nrf52: | ||||
|         return cv.one_of(*UART_SELECTION_NRF52, upper=True)(value) | ||||
|     raise NotImplementedError | ||||
|  | ||||
|  | ||||
| @@ -186,6 +196,7 @@ LoggerMessageTrigger = logger_ns.class_( | ||||
|     automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
| @@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 bk72xx=DEFAULT, | ||||
|                 ln882x=DEFAULT, | ||||
|                 rtl87xx=DEFAULT, | ||||
|                 nrf52=USB_CDC, | ||||
|             ): cv.All( | ||||
|                 cv.only_on( | ||||
|                     [ | ||||
| @@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                         PLATFORM_BK72XX, | ||||
|                         PLATFORM_LN882X, | ||||
|                         PLATFORM_RTL87XX, | ||||
|                         PLATFORM_NRF52, | ||||
|                     ] | ||||
|                 ), | ||||
|                 uart_selection, | ||||
| @@ -358,6 +371,15 @@ async def to_code(config): | ||||
|     except cv.Invalid: | ||||
|         pass | ||||
|  | ||||
|     if CORE.using_zephyr: | ||||
|         if config[CONF_HARDWARE_UART] == UART0: | ||||
|             zephyr_add_overlay("""&uart0 { status = "okay";};""") | ||||
|         if config[CONF_HARDWARE_UART] == UART1: | ||||
|             zephyr_add_overlay("""&uart1 { status = "okay";};""") | ||||
|         if config[CONF_HARDWARE_UART] == USB_CDC: | ||||
|             zephyr_add_prj_conf("UART_LINE_CTRL", True) | ||||
|             zephyr_add_cdc_acm(config, 0) | ||||
|  | ||||
|     # Register at end for safe mode | ||||
|     await cg.register_component(log, config) | ||||
|  | ||||
| @@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( | ||||
|             PlatformFramework.RTL87XX_ARDUINO, | ||||
|             PlatformFramework.LN882X_ARDUINO, | ||||
|         }, | ||||
|         "logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, | ||||
|         "task_log_buffer.cpp": { | ||||
|             PlatformFramework.ESP32_ARDUINO, | ||||
|             PlatformFramework.ESP32_IDF, | ||||
|   | ||||
| @@ -4,9 +4,9 @@ | ||||
| #include <memory>  // For unique_ptr | ||||
| #endif | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace logger { | ||||
| @@ -160,6 +160,8 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate | ||||
|   this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];  // NOLINT | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
|   this->main_task_ = xTaskGetCurrentTaskHandle(); | ||||
| #elif defined(USE_ZEPHYR) | ||||
|   this->main_task_ = k_current_get(); | ||||
| #endif | ||||
| } | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
| @@ -172,6 +174,7 @@ void Logger::init_log_buffer(size_t total_buffer_size) { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifndef USE_ZEPHYR | ||||
| #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) | ||||
| void Logger::loop() { | ||||
| #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) | ||||
| @@ -185,8 +188,13 @@ void Logger::loop() { | ||||
|     } | ||||
|     opened = !opened; | ||||
|   } | ||||
| #endif | ||||
|   this->process_messages_(); | ||||
| } | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| void Logger::process_messages_() { | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
|   // Process any buffered messages when available | ||||
|   if (this->log_buffer_->has_messages()) { | ||||
| @@ -227,12 +235,11 @@ void Logger::loop() { | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } | ||||
| void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||
| UARTSelection Logger::get_uart() const { return this->uart_; } | ||||
| #endif | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,11 @@ | ||||
| #include <driver/uart.h> | ||||
| #endif  // USE_ESP_IDF | ||||
|  | ||||
| #ifdef USE_ZEPHYR | ||||
| #include <zephyr/kernel.h> | ||||
| struct device; | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| namespace logger { | ||||
| @@ -56,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { | ||||
|     "VV",  // VERY_VERBOSE | ||||
| }; | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||
| /** Enum for logging UART selection | ||||
|  * | ||||
|  * Advanced configuration (pin selection, etc) is not supported. | ||||
| @@ -82,7 +87,7 @@ enum UARTSelection : uint8_t { | ||||
|   UART_SELECTION_UART0_SWAP, | ||||
| #endif  // USE_ESP8266 | ||||
| }; | ||||
| #endif  // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY | ||||
| #endif  // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY || USE_ZEPHYR | ||||
|  | ||||
| /** | ||||
|  * @brief Logger component for all ESPHome logging. | ||||
| @@ -107,7 +112,7 @@ class Logger : public Component { | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
|   void init_log_buffer(size_t total_buffer_size); | ||||
| #endif | ||||
| #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) | ||||
| #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) || defined(USE_ZEPHYR) | ||||
|   void loop() override; | ||||
| #endif | ||||
|   /// Manually set the baud rate for serial, set to 0 to disable. | ||||
| @@ -122,7 +127,7 @@ class Logger : public Component { | ||||
| #ifdef USE_ESP32 | ||||
|   void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } | ||||
| #endif | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||
|   void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } | ||||
|   /// Get the UART used by the logger. | ||||
|   UARTSelection get_uart() const; | ||||
| @@ -157,6 +162,7 @@ class Logger : public Component { | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   void process_messages_(); | ||||
|   void write_msg_(const char *msg); | ||||
|  | ||||
|   // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator | ||||
| @@ -164,7 +170,7 @@ class Logger : public Component { | ||||
|   inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, | ||||
|                                                         va_list args, char *buffer, uint16_t *buffer_at, | ||||
|                                                         uint16_t buffer_size) { | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||
|     this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); | ||||
| #else | ||||
|     this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); | ||||
| @@ -231,7 +237,10 @@ class Logger : public Component { | ||||
| #ifdef USE_ARDUINO | ||||
|   Stream *hw_serial_{nullptr}; | ||||
| #endif | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
| #if defined(USE_ZEPHYR) | ||||
|   const device *uart_dev_{nullptr}; | ||||
| #endif | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||
|   void *main_task_ = nullptr;  // Only used for thread name identification | ||||
| #endif | ||||
| #ifdef USE_ESP32 | ||||
| @@ -256,7 +265,7 @@ class Logger : public Component { | ||||
|   uint16_t tx_buffer_at_{0}; | ||||
|   uint16_t tx_buffer_size_{0}; | ||||
|   uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||
|   UARTSelection uart_{UART_SELECTION_UART0}; | ||||
| #endif | ||||
| #ifdef USE_LIBRETINY | ||||
| @@ -268,9 +277,13 @@ class Logger : public Component { | ||||
|   bool global_recursion_guard_{false};  // Simple global recursion guard for single-task platforms | ||||
| #endif | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||
|   const char *HOT get_thread_name_() { | ||||
| #ifdef USE_ZEPHYR | ||||
|     k_tid_t current_task = k_current_get(); | ||||
| #else | ||||
|     TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); | ||||
| #endif | ||||
|     if (current_task == main_task_) { | ||||
|       return nullptr;  // Main task | ||||
|     } else { | ||||
| @@ -278,6 +291,8 @@ class Logger : public Component { | ||||
|       return pcTaskGetName(current_task); | ||||
| #elif defined(USE_LIBRETINY) | ||||
|       return pcTaskGetTaskName(current_task); | ||||
| #elif defined(USE_ZEPHYR) | ||||
|       return k_thread_name_get(current_task); | ||||
| #endif | ||||
|     } | ||||
|   } | ||||
| @@ -319,7 +334,7 @@ class Logger : public Component { | ||||
|     const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; | ||||
|     const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||
|     if (thread_name != nullptr) { | ||||
|       // Non-main task with thread name | ||||
|       this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, | ||||
|   | ||||
							
								
								
									
										88
									
								
								esphome/components/logger/logger_zephyr.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								esphome/components/logger/logger_zephyr.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| #ifdef USE_ZEPHYR | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "logger.h" | ||||
|  | ||||
| #include <zephyr/device.h> | ||||
| #include <zephyr/drivers/uart.h> | ||||
| #include <zephyr/usb/usb_device.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace logger { | ||||
|  | ||||
| static const char *const TAG = "logger"; | ||||
|  | ||||
| void Logger::loop() { | ||||
| #ifdef USE_LOGGER_USB_CDC | ||||
|   if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) { | ||||
|     return; | ||||
|   } | ||||
|   static bool opened = false; | ||||
|   uint32_t dtr = 0; | ||||
|   uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); | ||||
|  | ||||
|   /* Poll if the DTR flag was set, optional */ | ||||
|   if (opened == dtr) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (!opened) { | ||||
|     App.schedule_dump_config(); | ||||
|   } | ||||
|   opened = !opened; | ||||
| #endif | ||||
|   this->process_messages_(); | ||||
| } | ||||
|  | ||||
| void Logger::pre_setup() { | ||||
|   if (this->baud_rate_ > 0) { | ||||
|     static const struct device *uart_dev = nullptr; | ||||
|     switch (this->uart_) { | ||||
|       case UART_SELECTION_UART0: | ||||
|         uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0)); | ||||
|         break; | ||||
|       case UART_SELECTION_UART1: | ||||
|         uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1)); | ||||
|         break; | ||||
| #ifdef USE_LOGGER_USB_CDC | ||||
|       case UART_SELECTION_USB_CDC: | ||||
|         uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0)); | ||||
|         if (device_is_ready(uart_dev)) { | ||||
|           usb_enable(nullptr); | ||||
|         } | ||||
|         break; | ||||
| #endif | ||||
|     } | ||||
|     if (!device_is_ready(uart_dev)) { | ||||
|       ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_()); | ||||
|     } else { | ||||
|       this->uart_dev_ = uart_dev; | ||||
|     } | ||||
|   } | ||||
|   global_logger = this; | ||||
|   ESP_LOGI(TAG, "Log initialized"); | ||||
| } | ||||
|  | ||||
| void HOT Logger::write_msg_(const char *msg) { | ||||
| #ifdef CONFIG_PRINTK | ||||
|   printk("%s\n", msg); | ||||
| #endif | ||||
|   if (nullptr == this->uart_dev_) { | ||||
|     return; | ||||
|   } | ||||
|   while (*msg) { | ||||
|     uart_poll_out(this->uart_dev_, *msg); | ||||
|     ++msg; | ||||
|   } | ||||
|   uart_poll_out(this->uart_dev_, '\n'); | ||||
| } | ||||
|  | ||||
| const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; | ||||
|  | ||||
| const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } | ||||
|  | ||||
| }  // namespace logger | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"] | ||||
|  | ||||
| DOMAIN = "mipi_spi" | ||||
|  | ||||
| CONF_DRAW_FROM_ORIGIN = "draw_from_origin" | ||||
| CONF_SPI_16 = "spi_16" | ||||
| CONF_PIXEL_MODE = "pixel_mode" | ||||
| CONF_COLOR_DEPTH = "color_depth" | ||||
| CONF_BUS_MODE = "bus_mode" | ||||
| CONF_USE_AXIS_FLIPS = "use_axis_flips" | ||||
| CONF_NATIVE_WIDTH = "native_width" | ||||
|   | ||||
| @@ -3,11 +3,18 @@ import logging | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import display, spi | ||||
| from esphome.components.const import ( | ||||
|     CONF_BYTE_ORDER, | ||||
|     CONF_COLOR_DEPTH, | ||||
|     CONF_DRAW_ROUNDING, | ||||
| ) | ||||
| from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS | ||||
| from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE | ||||
| import esphome.config_validation as cv | ||||
| from esphome.config_validation import ALLOW_EXTRA | ||||
| from esphome.const import ( | ||||
|     CONF_BRIGHTNESS, | ||||
|     CONF_BUFFER_SIZE, | ||||
|     CONF_COLOR_ORDER, | ||||
|     CONF_CS_PIN, | ||||
|     CONF_DATA_RATE, | ||||
| @@ -24,19 +31,19 @@ from esphome.const import ( | ||||
|     CONF_MODEL, | ||||
|     CONF_OFFSET_HEIGHT, | ||||
|     CONF_OFFSET_WIDTH, | ||||
|     CONF_PAGES, | ||||
|     CONF_RESET_PIN, | ||||
|     CONF_ROTATION, | ||||
|     CONF_SWAP_XY, | ||||
|     CONF_TRANSFORM, | ||||
|     CONF_WIDTH, | ||||
| ) | ||||
| from esphome.core import TimePeriod | ||||
| from esphome.core import CORE, TimePeriod | ||||
| from esphome.cpp_generator import TemplateArguments | ||||
| from esphome.final_validate import full_config | ||||
|  | ||||
| from ..const import CONF_DRAW_ROUNDING | ||||
| from ..lvgl.defines import CONF_COLOR_DEPTH | ||||
| from . import ( | ||||
|     CONF_BUS_MODE, | ||||
|     CONF_DRAW_FROM_ORIGIN, | ||||
|     CONF_NATIVE_HEIGHT, | ||||
|     CONF_NATIVE_WIDTH, | ||||
|     CONF_PIXEL_MODE, | ||||
| @@ -55,6 +62,7 @@ from .models import ( | ||||
|     MADCTL_XFLIP, | ||||
|     MADCTL_YFLIP, | ||||
|     DriverChip, | ||||
|     adafruit, | ||||
|     amoled, | ||||
|     cyd, | ||||
|     ili, | ||||
| @@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"] | ||||
|  | ||||
| LOGGER = logging.getLogger(DOMAIN) | ||||
| mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") | ||||
| MipiSpi = mipi_spi_ns.class_( | ||||
|     "MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice | ||||
| MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice) | ||||
| MipiSpiBuffer = mipi_spi_ns.class_( | ||||
|     "MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice | ||||
| ) | ||||
| ColorOrder = display.display_ns.enum("ColorMode") | ||||
| ColorBitness = display.display_ns.enum("ColorBitness") | ||||
| Model = mipi_spi_ns.enum("Model") | ||||
|  | ||||
| PixelMode = mipi_spi_ns.enum("PixelMode") | ||||
| BusType = mipi_spi_ns.enum("BusType") | ||||
|  | ||||
| COLOR_ORDERS = { | ||||
|     MODE_RGB: ColorOrder.COLOR_ORDER_RGB, | ||||
|     MODE_BGR: ColorOrder.COLOR_ORDER_BGR, | ||||
| } | ||||
|  | ||||
| COLOR_DEPTHS = { | ||||
|     8: ColorBitness.COLOR_BITNESS_332, | ||||
|     16: ColorBitness.COLOR_BITNESS_565, | ||||
|     8: PixelMode.PIXEL_MODE_8, | ||||
|     16: PixelMode.PIXEL_MODE_16, | ||||
|     18: PixelMode.PIXEL_MODE_18, | ||||
| } | ||||
|  | ||||
| DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema | ||||
|  | ||||
| BusTypes = { | ||||
|     TYPE_SINGLE: BusType.BUS_TYPE_SINGLE, | ||||
|     TYPE_QUAD: BusType.BUS_TYPE_QUAD, | ||||
|     TYPE_OCTAL: BusType.BUS_TYPE_OCTAL, | ||||
| } | ||||
|  | ||||
| DriverChip("CUSTOM", initsequence={}) | ||||
| DriverChip("CUSTOM") | ||||
|  | ||||
| MODELS = DriverChip.models | ||||
| # These statements are noops, but serve to suppress linting of side-effect-only imports | ||||
| for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): | ||||
| # This loop is a noop, but suppresses linting of side-effect-only imports | ||||
| for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit): | ||||
|     pass | ||||
|  | ||||
| PixelMode = mipi_spi_ns.enum("PixelMode") | ||||
|  | ||||
| PIXEL_MODE_18BIT = "18bit" | ||||
| PIXEL_MODE_16BIT = "16bit" | ||||
| DISPLAY_18BIT = "18bit" | ||||
| DISPLAY_16BIT = "16bit" | ||||
|  | ||||
| PIXEL_MODES = { | ||||
|     PIXEL_MODE_16BIT: 0x55, | ||||
|     PIXEL_MODE_18BIT: 0x66, | ||||
| DISPLAY_PIXEL_MODES = { | ||||
|     DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16), | ||||
|     DISPLAY_18BIT: (0x66, PixelMode.PIXEL_MODE_18), | ||||
| } | ||||
|  | ||||
|  | ||||
| def get_dimensions(config): | ||||
|     if CONF_DIMENSIONS in config: | ||||
|         # Explicit dimensions, just use as is | ||||
|         dimensions = config[CONF_DIMENSIONS] | ||||
|         if isinstance(dimensions, dict): | ||||
|             width = dimensions[CONF_WIDTH] | ||||
|             height = dimensions[CONF_HEIGHT] | ||||
|             offset_width = dimensions[CONF_OFFSET_WIDTH] | ||||
|             offset_height = dimensions[CONF_OFFSET_HEIGHT] | ||||
|             return width, height, offset_width, offset_height | ||||
|         (width, height) = dimensions | ||||
|         return width, height, 0, 0 | ||||
|  | ||||
|     # Default dimensions, use model defaults | ||||
|     transform = get_transform(config) | ||||
|  | ||||
|     model = MODELS[config[CONF_MODEL]] | ||||
|     width = model.get_default(CONF_WIDTH) | ||||
|     height = model.get_default(CONF_HEIGHT) | ||||
|     offset_width = model.get_default(CONF_OFFSET_WIDTH, 0) | ||||
|     offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0) | ||||
|  | ||||
|     # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where | ||||
|     # the offset is asymmetric | ||||
|     if transform[CONF_MIRROR_X]: | ||||
|         native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) | ||||
|         offset_width = native_width - width - offset_width | ||||
|     if transform[CONF_MIRROR_Y]: | ||||
|         native_height = model.get_default( | ||||
|             CONF_NATIVE_HEIGHT, height + offset_height * 2 | ||||
|         ) | ||||
|         offset_height = native_height - height - offset_height | ||||
|     # Swap default dimensions if swap_xy is set | ||||
|     if transform[CONF_SWAP_XY] is True: | ||||
|         width, height = height, width | ||||
|         offset_height, offset_width = offset_width, offset_height | ||||
|     return width, height, offset_width, offset_height | ||||
|  | ||||
|  | ||||
| def denominator(config): | ||||
|     """ | ||||
|     Calculate the best denominator for a buffer size fraction. | ||||
|     The denominator must be a number between 2 and 16 that divides the display height evenly, | ||||
|     and the fraction represented by the denominator must be less than or equal to the given fraction. | ||||
|     :config: The configuration dictionary containing the buffer size fraction and display dimensions | ||||
|     :return: The denominator to use for the buffer size fraction | ||||
|     """ | ||||
|     frac = config.get(CONF_BUFFER_SIZE) | ||||
|     if frac is None or frac > 0.75: | ||||
|         return 1 | ||||
|     height, _width, _offset_width, _offset_height = get_dimensions(config) | ||||
|     try: | ||||
|         return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0) | ||||
|     except StopIteration: | ||||
|         raise cv.Invalid( | ||||
|             f"Buffer size fraction {frac} is not compatible with display height {height}" | ||||
|         ) from StopIteration | ||||
|  | ||||
|  | ||||
| def validate_dimension(rounding): | ||||
|     def validator(value): | ||||
|         value = cv.positive_int(value) | ||||
| @@ -158,41 +235,50 @@ def dimension_schema(rounding): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def model_schema(bus_mode, model: DriverChip, swapsies: bool): | ||||
| def swap_xy_schema(model): | ||||
|     uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED | ||||
|  | ||||
|     def validator(value): | ||||
|         if value: | ||||
|             raise cv.Invalid("Axis swapping not supported by this model") | ||||
|         return cv.boolean(value) | ||||
|  | ||||
|     if uses_swap: | ||||
|         return {cv.Required(CONF_SWAP_XY): cv.boolean} | ||||
|     return {cv.Optional(CONF_SWAP_XY, default=False): validator} | ||||
|  | ||||
|  | ||||
| def model_schema(config): | ||||
|     model = MODELS[config[CONF_MODEL]] | ||||
|     bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) | ||||
|     transform = cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_MIRROR_X): cv.boolean, | ||||
|             cv.Required(CONF_MIRROR_Y): cv.boolean, | ||||
|             **swap_xy_schema(model), | ||||
|         } | ||||
|     ) | ||||
|     if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED: | ||||
|         transform = transform.extend( | ||||
|             { | ||||
|                 cv.Optional(CONF_SWAP_XY): cv.invalid( | ||||
|                     "Axis swapping not supported by this model" | ||||
|                 ) | ||||
|             } | ||||
|         ) | ||||
|     else: | ||||
|         transform = transform.extend( | ||||
|             { | ||||
|                 cv.Required(CONF_SWAP_XY): cv.boolean, | ||||
|             } | ||||
|         ) | ||||
|     # CUSTOM model will need to provide a custom init sequence | ||||
|     iseqconf = ( | ||||
|         cv.Required(CONF_INIT_SEQUENCE) | ||||
|         if model.initsequence is None | ||||
|         else cv.Optional(CONF_INIT_SEQUENCE) | ||||
|     ) | ||||
|     # Dimensions are optional if the model has a default width and the transform is not overridden | ||||
|     # Dimensions are optional if the model has a default width and the x-y transform is not overridden | ||||
|     is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True | ||||
|     cv_dimensions = ( | ||||
|         cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required | ||||
|         cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required | ||||
|     ) | ||||
|     pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,) | ||||
|     pixel_modes = DISPLAY_PIXEL_MODES if bus_mode == TYPE_SINGLE else (DISPLAY_16BIT,) | ||||
|     color_depth = ( | ||||
|         ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") | ||||
|     ) | ||||
|     other_options = [ | ||||
|         CONF_INVERT_COLORS, | ||||
|         CONF_USE_AXIS_FLIPS, | ||||
|     ] | ||||
|     if bus_mode == TYPE_SINGLE: | ||||
|         other_options.append(CONF_SPI_16) | ||||
|     schema = ( | ||||
|         display.FULL_DISPLAY_SCHEMA.extend( | ||||
|             spi.spi_device_schema( | ||||
| @@ -220,11 +306,13 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | ||||
|                 model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( | ||||
|                     COLOR_ORDERS, upper=True | ||||
|                 ), | ||||
|                 model.option(CONF_BYTE_ORDER, "big_endian"): cv.one_of( | ||||
|                     "big_endian", "little_endian", lower=True | ||||
|                 ), | ||||
|                 model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), | ||||
|                 model.option(CONF_DRAW_ROUNDING, 2): power_of_two, | ||||
|                 model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( | ||||
|                     cv.one_of(*pixel_modes, lower=True), | ||||
|                     cv.int_range(0, 255, min_included=True, max_included=True), | ||||
|                 model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of( | ||||
|                     *pixel_modes, lower=True | ||||
|                 ), | ||||
|                 cv.Optional(CONF_TRANSFORM): transform, | ||||
|                 cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( | ||||
| @@ -232,19 +320,12 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | ||||
|                 ), | ||||
|                 cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), | ||||
|                 iseqconf: cv.ensure_list(map_sequence), | ||||
|                 cv.Optional(CONF_BUFFER_SIZE): cv.All( | ||||
|                     cv.percentage, cv.Range(0.12, 1.0) | ||||
|                 ), | ||||
|             } | ||||
|         ) | ||||
|         .extend( | ||||
|             { | ||||
|                 model.option(x): cv.boolean | ||||
|                 for x in [ | ||||
|                     CONF_DRAW_FROM_ORIGIN, | ||||
|                     CONF_SPI_16, | ||||
|                     CONF_INVERT_COLORS, | ||||
|                     CONF_USE_AXIS_FLIPS, | ||||
|                 ] | ||||
|             } | ||||
|         ) | ||||
|         .extend({model.option(x): cv.boolean for x in other_options}) | ||||
|     ) | ||||
|     if brightness := model.get_default(CONF_BRIGHTNESS): | ||||
|         schema = schema.extend( | ||||
| @@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | ||||
|     return schema | ||||
|  | ||||
|  | ||||
| def rotation_as_transform(model, config): | ||||
| def is_rotation_transformable(config): | ||||
|     """ | ||||
|     Check if a rotation can be implemented in hardware using the MADCTL register. | ||||
|     A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. | ||||
|     """ | ||||
|     model = MODELS[config[CONF_MODEL]] | ||||
|     rotation = config.get(CONF_ROTATION, 0) | ||||
|     return rotation and ( | ||||
|         model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def config_schema(config): | ||||
| def customise_schema(config): | ||||
|     """ | ||||
|     Create a customised config schema for a specific model and validate the configuration. | ||||
|     :param config: The configuration dictionary to validate | ||||
|     :return: The validated configuration dictionary | ||||
|     :raises cv.Invalid: If the configuration is invalid | ||||
|     """ | ||||
|     # First get the model and bus mode | ||||
|     config = cv.Schema( | ||||
|         { | ||||
| @@ -288,29 +376,94 @@ def config_schema(config): | ||||
|         extra=ALLOW_EXTRA, | ||||
|     )(config) | ||||
|     bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) | ||||
|     swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True | ||||
|     config = model_schema(bus_mode, model, swapsies)(config) | ||||
|     config = model_schema(config)(config) | ||||
|     # Check for invalid combinations of MADCTL config | ||||
|     if init_sequence := config.get(CONF_INIT_SEQUENCE): | ||||
|         if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config: | ||||
|         commands = [x[0] for x in init_sequence] | ||||
|         if MADCTL in commands and CONF_TRANSFORM in config: | ||||
|             raise cv.Invalid( | ||||
|                 f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" | ||||
|             ) | ||||
|         if PIXFMT in commands: | ||||
|             raise cv.Invalid( | ||||
|                 f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically" | ||||
|             ) | ||||
|  | ||||
|     if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: | ||||
|         raise cv.Invalid("DC pin is not supported in quad mode") | ||||
|     if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE: | ||||
|         raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus") | ||||
|     if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: | ||||
|         raise cv.Invalid(f"DC pin is required in {bus_mode} mode") | ||||
|     denominator(config) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = config_schema | ||||
| CONFIG_SCHEMA = customise_schema | ||||
|  | ||||
|  | ||||
| def get_transform(model, config): | ||||
|     can_transform = rotation_as_transform(model, config) | ||||
| def requires_buffer(config): | ||||
|     """ | ||||
|     Check if the display configuration requires a buffer. It will do so if any drawing methods are configured. | ||||
|     :param config: | ||||
|     :return:  True if a buffer is required, False otherwise | ||||
|     """ | ||||
|     return any( | ||||
|         config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD) | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def get_color_depth(config): | ||||
|     return int(config[CONF_COLOR_DEPTH].removesuffix("bit")) | ||||
|  | ||||
|  | ||||
| def _final_validate(config): | ||||
|     global_config = full_config.get() | ||||
|  | ||||
|     from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN | ||||
|  | ||||
|     if not requires_buffer(config) and LVGL_DOMAIN not in global_config: | ||||
|         # If no drawing methods are configured, and LVGL is not enabled, show a test card | ||||
|         config[CONF_SHOW_TEST_CARD] = True | ||||
|  | ||||
|     if "psram" not in global_config and CONF_BUFFER_SIZE not in config: | ||||
|         if not requires_buffer(config): | ||||
|             return config  # No buffer needed, so no need to set a buffer size | ||||
|         # If PSRAM is not enabled, choose a small buffer size by default | ||||
|         if not requires_buffer(config): | ||||
|             # not our problem. | ||||
|             return config | ||||
|         color_depth = get_color_depth(config) | ||||
|         frac = denominator(config) | ||||
|         height, width, _offset_width, _offset_height = get_dimensions(config) | ||||
|  | ||||
|         buffer_size = color_depth // 8 * width * height // frac | ||||
|         # Target a buffer size of 20kB | ||||
|         fraction = 20000.0 / buffer_size | ||||
|         try: | ||||
|             config[CONF_BUFFER_SIZE] = 1.0 / next( | ||||
|                 x for x in range(2, 17) if fraction >= 1 / x and height % x == 0 | ||||
|             ) | ||||
|         except StopIteration: | ||||
|             # Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0 | ||||
|             # PSRAM will be needed. | ||||
|             if CORE.is_esp32: | ||||
|                 raise cv.Invalid( | ||||
|                     "PSRAM is required for this display" | ||||
|                 ) from StopIteration | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|  | ||||
|  | ||||
| def get_transform(config): | ||||
|     """ | ||||
|     Get the transformation configuration for the display. | ||||
|     :param config: | ||||
|     :return: | ||||
|     """ | ||||
|     model = MODELS[config[CONF_MODEL]] | ||||
|     can_transform = is_rotation_transformable(config) | ||||
|     transform = config.get( | ||||
|         CONF_TRANSFORM, | ||||
|         { | ||||
| @@ -350,16 +503,13 @@ def get_sequence(model, config): | ||||
|     sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] | ||||
|     commands = [x[0] for x in sequence] | ||||
|     # Set pixel format if not already in the custom sequence | ||||
|     if PIXFMT not in commands: | ||||
|         pixel_mode = config[CONF_PIXEL_MODE] | ||||
|         if not isinstance(pixel_mode, int): | ||||
|             pixel_mode = PIXEL_MODES[pixel_mode] | ||||
|         sequence.append((PIXFMT, pixel_mode)) | ||||
|     pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]] | ||||
|     sequence.append((PIXFMT, pixel_mode[0])) | ||||
|     # Does the chip use the flipping bits for mirroring rather than the reverse order bits? | ||||
|     use_flip = config[CONF_USE_AXIS_FLIPS] | ||||
|     if MADCTL not in commands: | ||||
|         madctl = 0 | ||||
|         transform = get_transform(model, config) | ||||
|         transform = get_transform(config) | ||||
|         if transform.get(CONF_TRANSFORM): | ||||
|             LOGGER.info("Using hardware transform to implement rotation") | ||||
|         if transform.get(CONF_MIRROR_X): | ||||
| @@ -396,63 +546,62 @@ def get_sequence(model, config): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def get_instance(config): | ||||
|     """ | ||||
|     Get the type of MipiSpi instance to create based on the configuration, | ||||
|     and the template arguments. | ||||
|     :param config: | ||||
|     :return: type, template arguments | ||||
|     """ | ||||
|     width, height, offset_width, offset_height = get_dimensions(config) | ||||
|  | ||||
|     color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit")) | ||||
|     bufferpixels = COLOR_DEPTHS[color_depth] | ||||
|  | ||||
|     display_pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]][1] | ||||
|     bus_type = config[CONF_BUS_MODE] | ||||
|     if bus_type == TYPE_SINGLE and config.get(CONF_SPI_16, False): | ||||
|         # If the bus mode is single and spi_16 is set, use single 16-bit mode | ||||
|         bus_type = BusType.BUS_TYPE_SINGLE_16 | ||||
|     else: | ||||
|         bus_type = BusTypes[bus_type] | ||||
|     buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 | ||||
|     frac = denominator(config) | ||||
|     rotation = DISPLAY_ROTATIONS[ | ||||
|         0 if is_rotation_transformable(config) else config.get(CONF_ROTATION, 0) | ||||
|     ] | ||||
|     templateargs = [ | ||||
|         buffer_type, | ||||
|         bufferpixels, | ||||
|         config[CONF_BYTE_ORDER] == "big_endian", | ||||
|         display_pixel_mode, | ||||
|         bus_type, | ||||
|         width, | ||||
|         height, | ||||
|         offset_width, | ||||
|         offset_height, | ||||
|     ] | ||||
|     # If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi | ||||
|     if requires_buffer(config): | ||||
|         templateargs.append(rotation) | ||||
|         templateargs.append(frac) | ||||
|         return MipiSpiBuffer, templateargs | ||||
|     return MipiSpi, templateargs | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     model = MODELS[config[CONF_MODEL]] | ||||
|     transform = get_transform(model, config) | ||||
|     if CONF_DIMENSIONS in config: | ||||
|         # Explicit dimensions, just use as is | ||||
|         dimensions = config[CONF_DIMENSIONS] | ||||
|         if isinstance(dimensions, dict): | ||||
|             width = dimensions[CONF_WIDTH] | ||||
|             height = dimensions[CONF_HEIGHT] | ||||
|             offset_width = dimensions[CONF_OFFSET_WIDTH] | ||||
|             offset_height = dimensions[CONF_OFFSET_HEIGHT] | ||||
|         else: | ||||
|             (width, height) = dimensions | ||||
|             offset_width = 0 | ||||
|             offset_height = 0 | ||||
|     else: | ||||
|         # Default dimensions, use model defaults and transform if needed | ||||
|         width = model.get_default(CONF_WIDTH) | ||||
|         height = model.get_default(CONF_HEIGHT) | ||||
|         offset_width = model.get_default(CONF_OFFSET_WIDTH, 0) | ||||
|         offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0) | ||||
|  | ||||
|         # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where | ||||
|         # the offset is asymmetric | ||||
|         if transform[CONF_MIRROR_X]: | ||||
|             native_width = model.get_default( | ||||
|                 CONF_NATIVE_WIDTH, width + offset_width * 2 | ||||
|             ) | ||||
|             offset_width = native_width - width - offset_width | ||||
|         if transform[CONF_MIRROR_Y]: | ||||
|             native_height = model.get_default( | ||||
|                 CONF_NATIVE_HEIGHT, height + offset_height * 2 | ||||
|             ) | ||||
|             offset_height = native_height - height - offset_height | ||||
|         # Swap default dimensions if swap_xy is set | ||||
|         if transform[CONF_SWAP_XY] is True: | ||||
|             width, height = height, width | ||||
|             offset_height, offset_width = offset_width, offset_height | ||||
|  | ||||
|     color_depth = config[CONF_COLOR_DEPTH] | ||||
|     if color_depth.endswith("bit"): | ||||
|         color_depth = color_depth[:-3] | ||||
|     color_depth = COLOR_DEPTHS[int(color_depth)] | ||||
|  | ||||
|     var = cg.new_Pvariable( | ||||
|         config[CONF_ID], width, height, offset_width, offset_height, color_depth | ||||
|     ) | ||||
|     var_id = config[CONF_ID] | ||||
|     var_id.type, templateargs = get_instance(config) | ||||
|     var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) | ||||
|     cg.add(var.set_init_sequence(get_sequence(model, config))) | ||||
|     if rotation_as_transform(model, config): | ||||
|     if is_rotation_transformable(config): | ||||
|         if CONF_TRANSFORM in config: | ||||
|             LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") | ||||
|         else: | ||||
|             config[CONF_ROTATION] = 0 | ||||
|     cg.add(var.set_model(config[CONF_MODEL])) | ||||
|     cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN])) | ||||
|     cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) | ||||
|     cg.add(var.set_spi_16(config[CONF_SPI_16])) | ||||
|     if enable_pin := config.get(CONF_ENABLE_PIN): | ||||
|         enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] | ||||
|         cg.add(var.set_enable_pins(enable)) | ||||
| @@ -472,4 +621,5 @@ async def to_code(config): | ||||
|         cg.add(var.set_writer(lambda_)) | ||||
|     await display.register_display(var, config) | ||||
|     await spi.register_spi_device(var, config) | ||||
|     # Displays are write-only, set the SPI device to write-only as well | ||||
|     cg.add(var.set_write_only(True)) | ||||
|   | ||||
| @@ -2,489 +2,5 @@ | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace mipi_spi { | ||||
|  | ||||
| void MipiSpi::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->spi_setup(); | ||||
|   if (this->dc_pin_ != nullptr) { | ||||
|     this->dc_pin_->setup(); | ||||
|     this->dc_pin_->digital_write(false); | ||||
|   } | ||||
|   for (auto *pin : this->enable_pins_) { | ||||
|     pin->setup(); | ||||
|     pin->digital_write(true); | ||||
|   } | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->setup(); | ||||
|     this->reset_pin_->digital_write(true); | ||||
|     delay(5); | ||||
|     this->reset_pin_->digital_write(false); | ||||
|     delay(5); | ||||
|     this->reset_pin_->digital_write(true); | ||||
|   } | ||||
|   this->bus_width_ = this->parent_->get_bus_width(); | ||||
|  | ||||
|   // need to know when the display is ready for SLPOUT command - will be 120ms after reset | ||||
|   auto when = millis() + 120; | ||||
|   delay(10); | ||||
|   size_t index = 0; | ||||
|   auto &vec = this->init_sequence_; | ||||
|   while (index != vec.size()) { | ||||
|     if (vec.size() - index < 2) { | ||||
|       ESP_LOGE(TAG, "Malformed init sequence"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|     uint8_t cmd = vec[index++]; | ||||
|     uint8_t x = vec[index++]; | ||||
|     if (x == DELAY_FLAG) { | ||||
|       ESP_LOGD(TAG, "Delay %dms", cmd); | ||||
|       delay(cmd); | ||||
|     } else { | ||||
|       uint8_t num_args = x & 0x7F; | ||||
|       if (vec.size() - index < num_args) { | ||||
|         ESP_LOGE(TAG, "Malformed init sequence"); | ||||
|         this->mark_failed(); | ||||
|         return; | ||||
|       } | ||||
|       auto arg_byte = vec[index]; | ||||
|       switch (cmd) { | ||||
|         case SLEEP_OUT: { | ||||
|           // are we ready, boots? | ||||
|           int duration = when - millis(); | ||||
|           if (duration > 0) { | ||||
|             ESP_LOGD(TAG, "Sleep %dms", duration); | ||||
|             delay(duration); | ||||
|           } | ||||
|         } break; | ||||
|  | ||||
|         case INVERT_ON: | ||||
|           this->invert_colors_ = true; | ||||
|           break; | ||||
|         case MADCTL_CMD: | ||||
|           this->madctl_ = arg_byte; | ||||
|           break; | ||||
|         case PIXFMT: | ||||
|           this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18; | ||||
|           break; | ||||
|         case BRIGHTNESS: | ||||
|           this->brightness_ = arg_byte; | ||||
|           break; | ||||
|  | ||||
|         default: | ||||
|           break; | ||||
|       } | ||||
|       const auto *ptr = vec.data() + index; | ||||
|       ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); | ||||
|       this->write_command_(cmd, ptr, num_args); | ||||
|       index += num_args; | ||||
|       if (cmd == SLEEP_OUT) | ||||
|         delay(10); | ||||
|     } | ||||
|   } | ||||
|   this->setup_complete_ = true; | ||||
|   if (this->draw_from_origin_) | ||||
|     check_buffer_(); | ||||
|   ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); | ||||
| } | ||||
|  | ||||
| void MipiSpi::update() { | ||||
|   if (!this->setup_complete_ || this->is_failed()) { | ||||
|     return; | ||||
|   } | ||||
|   this->do_update_(); | ||||
|   if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) | ||||
|     return; | ||||
|   ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_); | ||||
|   // Some chips require that the drawing window be aligned on certain boundaries | ||||
|   auto dr = this->draw_rounding_; | ||||
|   this->x_low_ = this->x_low_ / dr * dr; | ||||
|   this->y_low_ = this->y_low_ / dr * dr; | ||||
|   this->x_high_ = (this->x_high_ + dr) / dr * dr - 1; | ||||
|   this->y_high_ = (this->y_high_ + dr) / dr * dr - 1; | ||||
|   if (this->draw_from_origin_) { | ||||
|     this->x_low_ = 0; | ||||
|     this->y_low_ = 0; | ||||
|     this->x_high_ = this->width_ - 1; | ||||
|   } | ||||
|   int w = this->x_high_ - this->x_low_ + 1; | ||||
|   int h = this->y_high_ - this->y_low_ + 1; | ||||
|   this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_, | ||||
|                           this->width_ - w - this->x_low_); | ||||
|   // invalidate watermarks | ||||
|   this->x_low_ = this->width_; | ||||
|   this->y_low_ = this->height_; | ||||
|   this->x_high_ = 0; | ||||
|   this->y_high_ = 0; | ||||
| } | ||||
|  | ||||
| void MipiSpi::fill(Color color) { | ||||
|   if (!this->check_buffer_()) | ||||
|     return; | ||||
|   this->x_low_ = 0; | ||||
|   this->y_low_ = 0; | ||||
|   this->x_high_ = this->get_width_internal() - 1; | ||||
|   this->y_high_ = this->get_height_internal() - 1; | ||||
|   switch (this->color_depth_) { | ||||
|     case display::COLOR_BITNESS_332: { | ||||
|       auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); | ||||
|       memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); | ||||
|       break; | ||||
|     } | ||||
|     default: { | ||||
|       auto new_color = display::ColorUtil::color_to_565(color); | ||||
|       if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) { | ||||
|         // Upper and lower is equal can use quicker memset operation. Takes ~20ms. | ||||
|         memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); | ||||
|       } else { | ||||
|         auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_); | ||||
|         auto len = this->buffer_bytes_ / 2; | ||||
|         while (len--) { | ||||
|           *ptr_16++ = new_color; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) { | ||||
|   if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { | ||||
|     return; | ||||
|   } | ||||
|   if (!this->check_buffer_()) | ||||
|     return; | ||||
|   size_t pos = (y * this->width_) + x; | ||||
|   switch (this->color_depth_) { | ||||
|     case display::COLOR_BITNESS_332: { | ||||
|       uint8_t new_color = display::ColorUtil::color_to_332(color); | ||||
|       if (this->buffer_[pos] == new_color) | ||||
|         return; | ||||
|       this->buffer_[pos] = new_color; | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case display::COLOR_BITNESS_565: { | ||||
|       auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_); | ||||
|       uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5); | ||||
|       uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3); | ||||
|       uint16_t new_color = hi_byte | (lo_byte << 8);  // big endian | ||||
|       if (ptr_16[pos] == new_color) | ||||
|         return; | ||||
|       ptr_16[pos] = new_color; | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       return; | ||||
|   } | ||||
|   // low and high watermark may speed up drawing from buffer | ||||
|   if (x < this->x_low_) | ||||
|     this->x_low_ = x; | ||||
|   if (y < this->y_low_) | ||||
|     this->y_low_ = y; | ||||
|   if (x > this->x_high_) | ||||
|     this->x_high_ = x; | ||||
|   if (y > this->y_high_) | ||||
|     this->y_high_ = y; | ||||
| } | ||||
|  | ||||
| void MipiSpi::reset_params_() { | ||||
|   if (!this->is_ready()) | ||||
|     return; | ||||
|   this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); | ||||
|   if (this->brightness_.has_value()) | ||||
|     this->write_command_(BRIGHTNESS, this->brightness_.value()); | ||||
| } | ||||
|  | ||||
| void MipiSpi::write_init_sequence_() { | ||||
|   size_t index = 0; | ||||
|   auto &vec = this->init_sequence_; | ||||
|   while (index != vec.size()) { | ||||
|     if (vec.size() - index < 2) { | ||||
|       ESP_LOGE(TAG, "Malformed init sequence"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|     uint8_t cmd = vec[index++]; | ||||
|     uint8_t x = vec[index++]; | ||||
|     if (x == DELAY_FLAG) { | ||||
|       ESP_LOGV(TAG, "Delay %dms", cmd); | ||||
|       delay(cmd); | ||||
|     } else { | ||||
|       uint8_t num_args = x & 0x7F; | ||||
|       if (vec.size() - index < num_args) { | ||||
|         ESP_LOGE(TAG, "Malformed init sequence"); | ||||
|         this->mark_failed(); | ||||
|         return; | ||||
|       } | ||||
|       const auto *ptr = vec.data() + index; | ||||
|       this->write_command_(cmd, ptr, num_args); | ||||
|       index += num_args; | ||||
|     } | ||||
|   } | ||||
|   this->setup_complete_ = true; | ||||
|   ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); | ||||
| } | ||||
|  | ||||
| void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { | ||||
|   ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); | ||||
|   uint8_t buf[4]; | ||||
|   x1 += this->offset_width_; | ||||
|   x2 += this->offset_width_; | ||||
|   y1 += this->offset_height_; | ||||
|   y2 += this->offset_height_; | ||||
|   put16_be(buf, y1); | ||||
|   put16_be(buf + 2, y2); | ||||
|   this->write_command_(RASET, buf, sizeof buf); | ||||
|   put16_be(buf, x1); | ||||
|   put16_be(buf + 2, x2); | ||||
|   this->write_command_(CASET, buf, sizeof buf); | ||||
| } | ||||
|  | ||||
| void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, | ||||
|                              display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { | ||||
|   if (!this->setup_complete_ || this->is_failed()) | ||||
|     return; | ||||
|   if (w <= 0 || h <= 0) | ||||
|     return; | ||||
|   if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) { | ||||
|     Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); | ||||
|     return; | ||||
|   } | ||||
|   if (this->draw_from_origin_) { | ||||
|     auto stride = x_offset + w + x_pad; | ||||
|     for (int y = 0; y != h; y++) { | ||||
|       memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2, | ||||
|              ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2); | ||||
|     } | ||||
|     ptr = this->buffer_; | ||||
|     w = this->width_; | ||||
|     h += y_start; | ||||
|     x_start = 0; | ||||
|     y_start = 0; | ||||
|     x_offset = 0; | ||||
|     y_offset = 0; | ||||
|   } | ||||
|   this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); | ||||
| } | ||||
|  | ||||
| void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) { | ||||
|   stride -= w; | ||||
|   uint8_t transfer_buffer[6 * 256]; | ||||
|   size_t idx = 0;  // index into transfer_buffer | ||||
|   while (h-- != 0) { | ||||
|     for (auto x = w; x-- != 0;) { | ||||
|       auto color_val = *ptr++; | ||||
|       // deal with byte swapping | ||||
|       transfer_buffer[idx++] = (color_val & 0xF8);                                       // Blue | ||||
|       transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11);  // Green | ||||
|       transfer_buffer[idx++] = (color_val >> 5) & 0xF8;                                  // Red | ||||
|       if (idx == sizeof(transfer_buffer)) { | ||||
|         this->write_array(transfer_buffer, idx); | ||||
|         idx = 0; | ||||
|       } | ||||
|     } | ||||
|     ptr += stride; | ||||
|   } | ||||
|   if (idx != 0) | ||||
|     this->write_array(transfer_buffer, idx); | ||||
| } | ||||
|  | ||||
| void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { | ||||
|   stride -= w; | ||||
|   uint8_t transfer_buffer[6 * 256]; | ||||
|   size_t idx = 0;  // index into transfer_buffer | ||||
|   while (h-- != 0) { | ||||
|     for (auto x = w; x-- != 0;) { | ||||
|       auto color_val = *ptr++; | ||||
|       transfer_buffer[idx++] = color_val & 0xE0;         // Red | ||||
|       transfer_buffer[idx++] = (color_val << 3) & 0xE0;  // Green | ||||
|       transfer_buffer[idx++] = color_val << 6;           // Blue | ||||
|       if (idx == sizeof(transfer_buffer)) { | ||||
|         this->write_array(transfer_buffer, idx); | ||||
|         idx = 0; | ||||
|       } | ||||
|     } | ||||
|     ptr += stride; | ||||
|   } | ||||
|   if (idx != 0) | ||||
|     this->write_array(transfer_buffer, idx); | ||||
| } | ||||
|  | ||||
| void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { | ||||
|   stride -= w; | ||||
|   uint8_t transfer_buffer[6 * 256]; | ||||
|   size_t idx = 0;  // index into transfer_buffer | ||||
|   while (h-- != 0) { | ||||
|     for (auto x = w; x-- != 0;) { | ||||
|       auto color_val = *ptr++; | ||||
|       transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); | ||||
|       transfer_buffer[idx++] = (color_val & 0x3) << 3; | ||||
|       if (idx == sizeof(transfer_buffer)) { | ||||
|         this->write_array(transfer_buffer, idx); | ||||
|         idx = 0; | ||||
|       } | ||||
|     } | ||||
|     ptr += stride; | ||||
|   } | ||||
|   if (idx != 0) | ||||
|     this->write_array(transfer_buffer, idx); | ||||
| } | ||||
|  | ||||
| void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, | ||||
|                                 int x_pad) { | ||||
|   this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); | ||||
|   auto stride = x_offset + w + x_pad; | ||||
|   const auto *offset_ptr = ptr; | ||||
|   if (this->color_depth_ == display::COLOR_BITNESS_332) { | ||||
|     offset_ptr += y_offset * stride + x_offset; | ||||
|   } else { | ||||
|     stride *= 2; | ||||
|     offset_ptr += y_offset * stride + x_offset * 2; | ||||
|   } | ||||
|  | ||||
|   switch (this->bus_width_) { | ||||
|     case 4: | ||||
|       this->enable(); | ||||
|       if (x_offset == 0 && x_pad == 0 && y_offset == 0) { | ||||
|         // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't | ||||
|         // bother | ||||
|         this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4); | ||||
|       } else { | ||||
|         this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4); | ||||
|         for (int y = 0; y != h; y++) { | ||||
|           this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4); | ||||
|           offset_ptr += stride; | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case 8: | ||||
|       this->write_command_(WDATA); | ||||
|       this->enable(); | ||||
|       if (x_offset == 0 && x_pad == 0 && y_offset == 0) { | ||||
|         this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8); | ||||
|       } else { | ||||
|         for (int y = 0; y != h; y++) { | ||||
|           this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8); | ||||
|           offset_ptr += stride; | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     default: | ||||
|       this->write_command_(WDATA); | ||||
|       this->enable(); | ||||
|  | ||||
|       if (this->color_depth_ == display::COLOR_BITNESS_565) { | ||||
|         // Source buffer is 16-bit RGB565 | ||||
|         if (this->pixel_mode_ == PIXEL_MODE_18) { | ||||
|           // Convert RGB565 to RGB666 | ||||
|           this->write_18_from_16_bit_(reinterpret_cast<const uint16_t *>(offset_ptr), w, h, stride / 2); | ||||
|         } else { | ||||
|           // Direct RGB565 output | ||||
|           if (x_offset == 0 && x_pad == 0 && y_offset == 0) { | ||||
|             this->write_array(ptr, w * h * 2); | ||||
|           } else { | ||||
|             for (int y = 0; y != h; y++) { | ||||
|               this->write_array(offset_ptr, w * 2); | ||||
|               offset_ptr += stride; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         // Source buffer is 8-bit RGB332 | ||||
|         if (this->pixel_mode_ == PIXEL_MODE_18) { | ||||
|           // Convert RGB332 to RGB666 | ||||
|           this->write_18_from_8_bit_(offset_ptr, w, h, stride); | ||||
|         } else { | ||||
|           this->write_16_from_8_bit_(offset_ptr, w, h, stride); | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|   } | ||||
|   this->disable(); | ||||
| } | ||||
|  | ||||
| void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { | ||||
|   ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); | ||||
|   if (this->bus_width_ == 4) { | ||||
|     this->enable(); | ||||
|     this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); | ||||
|     this->disable(); | ||||
|   } else if (this->bus_width_ == 8) { | ||||
|     this->dc_pin_->digital_write(false); | ||||
|     this->enable(); | ||||
|     this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8); | ||||
|     this->disable(); | ||||
|     this->dc_pin_->digital_write(true); | ||||
|     if (len != 0) { | ||||
|       this->enable(); | ||||
|       this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8); | ||||
|       this->disable(); | ||||
|     } | ||||
|   } else { | ||||
|     this->dc_pin_->digital_write(false); | ||||
|     this->enable(); | ||||
|     this->write_byte(cmd); | ||||
|     this->disable(); | ||||
|     this->dc_pin_->digital_write(true); | ||||
|     if (len != 0) { | ||||
|       if (this->spi_16_) { | ||||
|         for (size_t i = 0; i != len; i++) { | ||||
|           this->enable(); | ||||
|           this->write_byte(0); | ||||
|           this->write_byte(bytes[i]); | ||||
|           this->disable(); | ||||
|         } | ||||
|       } else { | ||||
|         this->enable(); | ||||
|         this->write_array(bytes, len); | ||||
|         this->disable(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MipiSpi::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "MIPI_SPI Display\n" | ||||
|                 "  Model: %s\n" | ||||
|                 "  Width: %u\n" | ||||
|                 "  Height: %u", | ||||
|                 this->model_, this->width_, this->height_); | ||||
|   if (this->offset_width_ != 0) | ||||
|     ESP_LOGCONFIG(TAG, "  Offset width: %u", this->offset_width_); | ||||
|   if (this->offset_height_ != 0) | ||||
|     ESP_LOGCONFIG(TAG, "  Offset height: %u", this->offset_height_); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Swap X/Y: %s\n" | ||||
|                 "  Mirror X: %s\n" | ||||
|                 "  Mirror Y: %s\n" | ||||
|                 "  Color depth: %d bits\n" | ||||
|                 "  Invert colors: %s\n" | ||||
|                 "  Color order: %s\n" | ||||
|                 "  Pixel mode: %s", | ||||
|                 YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), | ||||
|                 YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), | ||||
|                 this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8, YESNO(this->invert_colors_), | ||||
|                 this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit"); | ||||
|   if (this->brightness_.has_value()) | ||||
|     ESP_LOGCONFIG(TAG, "  Brightness: %u", this->brightness_.value()); | ||||
|   if (this->spi_16_) | ||||
|     ESP_LOGCONFIG(TAG, "  SPI 16bit: YES"); | ||||
|   ESP_LOGCONFIG(TAG, "  Draw rounding: %u", this->draw_rounding_); | ||||
|   if (this->draw_from_origin_) | ||||
|     ESP_LOGCONFIG(TAG, "  Draw from origin: YES"); | ||||
|   LOG_PIN("  CS Pin: ", this->cs_); | ||||
|   LOG_PIN("  Reset Pin: ", this->reset_pin_); | ||||
|   LOG_PIN("  DC Pin: ", this->dc_pin_); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  SPI Mode: %d\n" | ||||
|                 "  SPI Data rate: %dMHz\n" | ||||
|                 "  SPI Bus width: %d", | ||||
|                 this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), this->bus_width_); | ||||
| } | ||||
|  | ||||
| }  // namespace mipi_spi | ||||
| namespace mipi_spi {}  // namespace mipi_spi | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -4,40 +4,39 @@ | ||||
|  | ||||
| #include "esphome/components/spi/spi.h" | ||||
| #include "esphome/components/display/display.h" | ||||
| #include "esphome/components/display/display_buffer.h" | ||||
| #include "esphome/components/display/display_color_utils.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace mipi_spi { | ||||
|  | ||||
| constexpr static const char *const TAG = "display.mipi_spi"; | ||||
| static const uint8_t SW_RESET_CMD = 0x01; | ||||
| static const uint8_t SLEEP_OUT = 0x11; | ||||
| static const uint8_t NORON = 0x13; | ||||
| static const uint8_t INVERT_OFF = 0x20; | ||||
| static const uint8_t INVERT_ON = 0x21; | ||||
| static const uint8_t ALL_ON = 0x23; | ||||
| static const uint8_t WRAM = 0x24; | ||||
| static const uint8_t MIPI = 0x26; | ||||
| static const uint8_t DISPLAY_ON = 0x29; | ||||
| static const uint8_t RASET = 0x2B; | ||||
| static const uint8_t CASET = 0x2A; | ||||
| static const uint8_t WDATA = 0x2C; | ||||
| static const uint8_t TEON = 0x35; | ||||
| static const uint8_t MADCTL_CMD = 0x36; | ||||
| static const uint8_t PIXFMT = 0x3A; | ||||
| static const uint8_t BRIGHTNESS = 0x51; | ||||
| static const uint8_t SWIRE1 = 0x5A; | ||||
| static const uint8_t SWIRE2 = 0x5B; | ||||
| static const uint8_t PAGESEL = 0xFE; | ||||
| static constexpr uint8_t SW_RESET_CMD = 0x01; | ||||
| static constexpr uint8_t SLEEP_OUT = 0x11; | ||||
| static constexpr uint8_t NORON = 0x13; | ||||
| static constexpr uint8_t INVERT_OFF = 0x20; | ||||
| static constexpr uint8_t INVERT_ON = 0x21; | ||||
| static constexpr uint8_t ALL_ON = 0x23; | ||||
| static constexpr uint8_t WRAM = 0x24; | ||||
| static constexpr uint8_t MIPI = 0x26; | ||||
| static constexpr uint8_t DISPLAY_ON = 0x29; | ||||
| static constexpr uint8_t RASET = 0x2B; | ||||
| static constexpr uint8_t CASET = 0x2A; | ||||
| static constexpr uint8_t WDATA = 0x2C; | ||||
| static constexpr uint8_t TEON = 0x35; | ||||
| static constexpr uint8_t MADCTL_CMD = 0x36; | ||||
| static constexpr uint8_t PIXFMT = 0x3A; | ||||
| static constexpr uint8_t BRIGHTNESS = 0x51; | ||||
| static constexpr uint8_t SWIRE1 = 0x5A; | ||||
| static constexpr uint8_t SWIRE2 = 0x5B; | ||||
| static constexpr uint8_t PAGESEL = 0xFE; | ||||
|  | ||||
| static const uint8_t MADCTL_MY = 0x80;     // Bit 7 Bottom to top | ||||
| static const uint8_t MADCTL_MX = 0x40;     // Bit 6 Right to left | ||||
| static const uint8_t MADCTL_MV = 0x20;     // Bit 5 Swap axes | ||||
| static const uint8_t MADCTL_RGB = 0x00;    // Bit 3 Red-Green-Blue pixel order | ||||
| static const uint8_t MADCTL_BGR = 0x08;    // Bit 3 Blue-Green-Red pixel order | ||||
| static const uint8_t MADCTL_XFLIP = 0x02;  // Mirror the display horizontally | ||||
| static const uint8_t MADCTL_YFLIP = 0x01;  // Mirror the display vertically | ||||
| static constexpr uint8_t MADCTL_MY = 0x80;     // Bit 7 Bottom to top | ||||
| static constexpr uint8_t MADCTL_MX = 0x40;     // Bit 6 Right to left | ||||
| static constexpr uint8_t MADCTL_MV = 0x20;     // Bit 5 Swap axes | ||||
| static constexpr uint8_t MADCTL_RGB = 0x00;    // Bit 3 Red-Green-Blue pixel order | ||||
| static constexpr uint8_t MADCTL_BGR = 0x08;    // Bit 3 Blue-Green-Red pixel order | ||||
| static constexpr uint8_t MADCTL_XFLIP = 0x02;  // Mirror the display horizontally | ||||
| static constexpr uint8_t MADCTL_YFLIP = 0x01;  // Mirror the display vertically | ||||
|  | ||||
| static const uint8_t DELAY_FLAG = 0xFF; | ||||
| // store a 16 bit value in a buffer, big endian. | ||||
| @@ -46,28 +45,44 @@ static inline void put16_be(uint8_t *buf, uint16_t value) { | ||||
|   buf[1] = value; | ||||
| } | ||||
|  | ||||
| // Buffer mode, conveniently also the number of bytes in a pixel | ||||
| enum PixelMode { | ||||
|   PIXEL_MODE_16, | ||||
|   PIXEL_MODE_18, | ||||
|   PIXEL_MODE_8 = 1, | ||||
|   PIXEL_MODE_16 = 2, | ||||
|   PIXEL_MODE_18 = 3, | ||||
| }; | ||||
|  | ||||
| class MipiSpi : public display::DisplayBuffer, | ||||
| enum BusType { | ||||
|   BUS_TYPE_SINGLE = 1, | ||||
|   BUS_TYPE_QUAD = 4, | ||||
|   BUS_TYPE_OCTAL = 8, | ||||
|   BUS_TYPE_SINGLE_16 = 16,  // Single bit bus, but 16 bits per transfer | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Base class for MIPI SPI displays. | ||||
|  * All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file. | ||||
|  * | ||||
|  * @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t | ||||
|  * @tparam BUFFERPIXEL Color depth of the buffer | ||||
|  * @tparam DISPLAYPIXEL Color depth of the display | ||||
|  * @tparam BUS_TYPE The type of the interface bus (single, quad, octal) | ||||
|  * @tparam WIDTH Width of the display in pixels | ||||
|  * @tparam HEIGHT Height of the display in pixels | ||||
|  * @tparam OFFSET_WIDTH The x-offset of the display in pixels | ||||
|  * @tparam OFFSET_HEIGHT The y-offset of the display in pixels | ||||
|  * buffer | ||||
|  */ | ||||
| template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE, | ||||
|          int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT> | ||||
| class MipiSpi : public display::Display, | ||||
|                 public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, | ||||
|                                       spi::DATA_RATE_1MHZ> { | ||||
|  public: | ||||
|   MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) | ||||
|       : width_(width), | ||||
|         height_(height), | ||||
|         offset_width_(offset_width), | ||||
|         offset_height_(offset_height), | ||||
|         color_depth_(color_depth) {} | ||||
|   MipiSpi() {} | ||||
|   void update() override { this->stop_poller(); } | ||||
|   void draw_pixel_at(int x, int y, Color color) override {} | ||||
|   void set_model(const char *model) { this->model_ = model; } | ||||
|   void update() override; | ||||
|   void setup() override; | ||||
|   display::ColorOrder get_color_mode() { | ||||
|     return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; | ||||
|   } | ||||
|  | ||||
|   void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } | ||||
|   void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); } | ||||
|   void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } | ||||
| @@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer, | ||||
|     this->brightness_ = brightness; | ||||
|     this->reset_params_(); | ||||
|   } | ||||
|  | ||||
|   void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; } | ||||
|   display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } | ||||
|   void dump_config() override; | ||||
|  | ||||
|   int get_width_internal() override { return this->width_; } | ||||
|   int get_height_internal() override { return this->height_; } | ||||
|   bool can_proceed() override { return this->setup_complete_; } | ||||
|   int get_width_internal() override { return WIDTH; } | ||||
|   int get_height_internal() override { return HEIGHT; } | ||||
|   void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; } | ||||
|   void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } | ||||
|   void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; } | ||||
|  | ||||
|   // reset the display, and write the init sequence | ||||
|   void setup() override { | ||||
|     this->spi_setup(); | ||||
|     if (this->dc_pin_ != nullptr) { | ||||
|       this->dc_pin_->setup(); | ||||
|       this->dc_pin_->digital_write(false); | ||||
|     } | ||||
|     for (auto *pin : this->enable_pins_) { | ||||
|       pin->setup(); | ||||
|       pin->digital_write(true); | ||||
|     } | ||||
|     if (this->reset_pin_ != nullptr) { | ||||
|       this->reset_pin_->setup(); | ||||
|       this->reset_pin_->digital_write(true); | ||||
|       delay(5); | ||||
|       this->reset_pin_->digital_write(false); | ||||
|       delay(5); | ||||
|       this->reset_pin_->digital_write(true); | ||||
|     } | ||||
|  | ||||
|     // need to know when the display is ready for SLPOUT command - will be 120ms after reset | ||||
|     auto when = millis() + 120; | ||||
|     delay(10); | ||||
|     size_t index = 0; | ||||
|     auto &vec = this->init_sequence_; | ||||
|     while (index != vec.size()) { | ||||
|       if (vec.size() - index < 2) { | ||||
|         esph_log_e(TAG, "Malformed init sequence"); | ||||
|         this->mark_failed(); | ||||
|         return; | ||||
|       } | ||||
|       uint8_t cmd = vec[index++]; | ||||
|       uint8_t x = vec[index++]; | ||||
|       if (x == DELAY_FLAG) { | ||||
|         esph_log_d(TAG, "Delay %dms", cmd); | ||||
|         delay(cmd); | ||||
|       } else { | ||||
|         uint8_t num_args = x & 0x7F; | ||||
|         if (vec.size() - index < num_args) { | ||||
|           esph_log_e(TAG, "Malformed init sequence"); | ||||
|           this->mark_failed(); | ||||
|           return; | ||||
|         } | ||||
|         auto arg_byte = vec[index]; | ||||
|         switch (cmd) { | ||||
|           case SLEEP_OUT: { | ||||
|             // are we ready, boots? | ||||
|             int duration = when - millis(); | ||||
|             if (duration > 0) { | ||||
|               esph_log_d(TAG, "Sleep %dms", duration); | ||||
|               delay(duration); | ||||
|             } | ||||
|           } break; | ||||
|  | ||||
|           case INVERT_ON: | ||||
|             this->invert_colors_ = true; | ||||
|             break; | ||||
|           case MADCTL_CMD: | ||||
|             this->madctl_ = arg_byte; | ||||
|             break; | ||||
|           case BRIGHTNESS: | ||||
|             this->brightness_ = arg_byte; | ||||
|             break; | ||||
|  | ||||
|           default: | ||||
|             break; | ||||
|         } | ||||
|         const auto *ptr = vec.data() + index; | ||||
|         esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); | ||||
|         this->write_command_(cmd, ptr, num_args); | ||||
|         index += num_args; | ||||
|         if (cmd == SLEEP_OUT) | ||||
|           delay(10); | ||||
|       } | ||||
|     } | ||||
|     // init sequence no longer needed | ||||
|     this->init_sequence_.clear(); | ||||
|   } | ||||
|  | ||||
|   // Drawing operations | ||||
|  | ||||
|   void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, | ||||
|                       display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override { | ||||
|     if (this->is_failed()) | ||||
|       return; | ||||
|     if (w <= 0 || h <= 0) | ||||
|       return; | ||||
|     if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) { | ||||
|       // note that the usual logging macros are banned in header files, so use their replacement | ||||
|       esph_log_e(TAG, "Unsupported color depth or bit order"); | ||||
|       return; | ||||
|     } | ||||
|     this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const BUFFERTYPE *>(ptr), x_offset, y_offset, | ||||
|                             x_pad); | ||||
|   } | ||||
|  | ||||
|   void dump_config() override { | ||||
|     esph_log_config(TAG, | ||||
|                     "MIPI_SPI Display\n" | ||||
|                     "  Model: %s\n" | ||||
|                     "  Width: %u\n" | ||||
|                     "  Height: %u", | ||||
|                     this->model_, WIDTH, HEIGHT); | ||||
|     if constexpr (OFFSET_WIDTH != 0) | ||||
|       esph_log_config(TAG, "  Offset width: %u", OFFSET_WIDTH); | ||||
|     if constexpr (OFFSET_HEIGHT != 0) | ||||
|       esph_log_config(TAG, "  Offset height: %u", OFFSET_HEIGHT); | ||||
|     esph_log_config(TAG, | ||||
|                     "  Swap X/Y: %s\n" | ||||
|                     "  Mirror X: %s\n" | ||||
|                     "  Mirror Y: %s\n" | ||||
|                     "  Invert colors: %s\n" | ||||
|                     "  Color order: %s\n" | ||||
|                     "  Display pixels: %d bits\n" | ||||
|                     "  Endianness: %s\n", | ||||
|                     YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), | ||||
|                     YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_), | ||||
|                     this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little"); | ||||
|     if (this->brightness_.has_value()) | ||||
|       esph_log_config(TAG, "  Brightness: %u", this->brightness_.value()); | ||||
|     if (this->cs_ != nullptr) | ||||
|       esph_log_config(TAG, "  CS Pin: %s", this->cs_->dump_summary().c_str()); | ||||
|     if (this->reset_pin_ != nullptr) | ||||
|       esph_log_config(TAG, "  Reset Pin: %s", this->reset_pin_->dump_summary().c_str()); | ||||
|     if (this->dc_pin_ != nullptr) | ||||
|       esph_log_config(TAG, "  DC Pin: %s", this->dc_pin_->dump_summary().c_str()); | ||||
|     esph_log_config(TAG, | ||||
|                     "  SPI Mode: %d\n" | ||||
|                     "  SPI Data rate: %dMHz\n" | ||||
|                     "  SPI Bus width: %d", | ||||
|                     this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   bool check_buffer_() { | ||||
|     if (this->is_failed()) | ||||
|       return false; | ||||
|     if (this->buffer_ != nullptr) | ||||
|       return true; | ||||
|     auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1; | ||||
|     this->init_internal_(this->width_ * this->height_ * bytes_per_pixel); | ||||
|     if (this->buffer_ == nullptr) { | ||||
|       this->mark_failed(); | ||||
|       return false; | ||||
|     } | ||||
|     this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel; | ||||
|     return true; | ||||
|   } | ||||
|   void fill(Color color) override; | ||||
|   void draw_absolute_pixel_internal(int x, int y, Color color) override; | ||||
|   void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, | ||||
|                       display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; | ||||
|   void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride); | ||||
|   void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); | ||||
|   void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); | ||||
|   void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, | ||||
|                          int x_pad); | ||||
|   /** | ||||
|    * the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the | ||||
|    * sample code.) | ||||
|    * | ||||
|    * Immediately after enabling /CS send 4 bytes in single-dataline SPI mode: | ||||
|    *    0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be | ||||
|    *        sent in 1-dataline SPI. The second indicates quad mode. | ||||
|    *    1: 0x00 | ||||
|    *    2: The command (register address) byte. | ||||
|    *    3: 0x00 | ||||
|    * | ||||
|    *    This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte. | ||||
|    *    At the conclusion of the write, de-assert /CS. | ||||
|    * | ||||
|    * @param cmd | ||||
|    * @param bytes | ||||
|    * @param len | ||||
|    */ | ||||
|   void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len); | ||||
|  | ||||
|   /* METHODS */ | ||||
|   // convenience functions to write commands with or without data | ||||
|   void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } | ||||
|   void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } | ||||
|   void reset_params_(); | ||||
|   void write_init_sequence_(); | ||||
|   void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2); | ||||
|  | ||||
|   // Writes a command to the display, with the given bytes. | ||||
|   void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { | ||||
|     esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); | ||||
|     if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { | ||||
|       this->enable(); | ||||
|       this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); | ||||
|       this->disable(); | ||||
|     } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { | ||||
|       this->dc_pin_->digital_write(false); | ||||
|       this->enable(); | ||||
|       this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8); | ||||
|       this->disable(); | ||||
|       this->dc_pin_->digital_write(true); | ||||
|       if (len != 0) { | ||||
|         this->enable(); | ||||
|         this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8); | ||||
|         this->disable(); | ||||
|       } | ||||
|     } else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) { | ||||
|       this->dc_pin_->digital_write(false); | ||||
|       this->enable(); | ||||
|       this->write_byte(cmd); | ||||
|       this->disable(); | ||||
|       this->dc_pin_->digital_write(true); | ||||
|       if (len != 0) { | ||||
|         this->enable(); | ||||
|         this->write_array(bytes, len); | ||||
|         this->disable(); | ||||
|       } | ||||
|     } else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) { | ||||
|       this->dc_pin_->digital_write(false); | ||||
|       this->enable(); | ||||
|       this->write_byte(cmd); | ||||
|       this->disable(); | ||||
|       this->dc_pin_->digital_write(true); | ||||
|       for (size_t i = 0; i != len; i++) { | ||||
|         this->enable(); | ||||
|         this->write_byte(0); | ||||
|         this->write_byte(bytes[i]); | ||||
|         this->disable(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // write changed parameters to the display | ||||
|   void reset_params_() { | ||||
|     if (!this->is_ready()) | ||||
|       return; | ||||
|     this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); | ||||
|     if (this->brightness_.has_value()) | ||||
|       this->write_command_(BRIGHTNESS, this->brightness_.value()); | ||||
|   } | ||||
|  | ||||
|   // set the address window for the next data write | ||||
|   void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { | ||||
|     esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); | ||||
|     uint8_t buf[4]; | ||||
|     x1 += OFFSET_WIDTH; | ||||
|     x2 += OFFSET_WIDTH; | ||||
|     y1 += OFFSET_HEIGHT; | ||||
|     y2 += OFFSET_HEIGHT; | ||||
|     put16_be(buf, y1); | ||||
|     put16_be(buf + 2, y2); | ||||
|     this->write_command_(RASET, buf, sizeof buf); | ||||
|     put16_be(buf, x1); | ||||
|     put16_be(buf + 2, x2); | ||||
|     this->write_command_(CASET, buf, sizeof buf); | ||||
|     if constexpr (BUS_TYPE != BUS_TYPE_QUAD) { | ||||
|       this->write_command_(WDATA); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // map the display color bitness to the pixel mode | ||||
|   static PixelMode get_pixel_mode(display::ColorBitness bitness) { | ||||
|     switch (bitness) { | ||||
|       case display::COLOR_BITNESS_888: | ||||
|         return PIXEL_MODE_18;  // 18 bits per pixel | ||||
|       case display::COLOR_BITNESS_565: | ||||
|         return PIXEL_MODE_16;  // 16 bits per pixel | ||||
|       default: | ||||
|         return PIXEL_MODE_8;  // Default to 8 bits per pixel | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Writes a buffer to the display. | ||||
|    * @param w Width of each line in bytes | ||||
|    * @param h Height of the buffer in rows | ||||
|    * @param pad Padding in bytes after each line | ||||
|    */ | ||||
|   void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) { | ||||
|     if (pad == 0) { | ||||
|       if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { | ||||
|         this->write_array(ptr, w * h); | ||||
|       } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { | ||||
|         this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h, 4); | ||||
|       } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { | ||||
|         this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8); | ||||
|       } | ||||
|     } else { | ||||
|       for (size_t y = 0; y != h; y++) { | ||||
|         if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { | ||||
|           this->write_array(ptr, w); | ||||
|         } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { | ||||
|           this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w, 4); | ||||
|         } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { | ||||
|           this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8); | ||||
|         } | ||||
|         ptr += w + pad; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Writes a buffer to the display. | ||||
|    * | ||||
|    * The ptr is a pointer to the pixel data | ||||
|    * The other parameters are all in pixel units. | ||||
|    */ | ||||
|   void write_to_display_(int x_start, int y_start, int w, int h, const BUFFERTYPE *ptr, int x_offset, int y_offset, | ||||
|                          int x_pad) { | ||||
|     this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); | ||||
|     this->enable(); | ||||
|     ptr += y_offset * (x_offset + w + x_pad) + x_offset; | ||||
|     if constexpr (BUFFERPIXEL == DISPLAYPIXEL) { | ||||
|       this->write_display_data_(reinterpret_cast<const uint8_t *>(ptr), w * sizeof(BUFFERTYPE), h, | ||||
|                                 x_pad * sizeof(BUFFERTYPE)); | ||||
|     } else { | ||||
|       // type conversion required, do it in chunks | ||||
|       uint8_t dbuffer[DISPLAYPIXEL * 48]; | ||||
|       uint8_t *dptr = dbuffer; | ||||
|       auto stride = x_offset + w + x_pad;  // stride in pixels | ||||
|       for (size_t y = 0; y != h; y++) { | ||||
|         for (size_t x = 0; x != w; x++) { | ||||
|           auto color_val = ptr[y * stride + x]; | ||||
|           if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) { | ||||
|             // 16 to 18 bit conversion | ||||
|             if constexpr (IS_BIG_ENDIAN) { | ||||
|               *dptr++ = color_val & 0xF8; | ||||
|               *dptr++ = ((color_val & 0x7) << 5) | (color_val & 0xE000) >> 11; | ||||
|               *dptr++ = (color_val >> 5) & 0xF8; | ||||
|             } else { | ||||
|               *dptr++ = (color_val >> 8) & 0xF8;  // Blue | ||||
|               *dptr++ = (color_val & 0x7E0) >> 3; | ||||
|               *dptr++ = color_val << 3; | ||||
|             } | ||||
|           } else if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_8) { | ||||
|             // 8 bit to 18 bit conversion | ||||
|             *dptr++ = color_val << 6;           // Blue | ||||
|             *dptr++ = (color_val & 0x1C) << 3;  // Green | ||||
|             *dptr++ = (color_val & 0xE0);       // Red | ||||
|           } else if constexpr (DISPLAYPIXEL == PIXEL_MODE_16 && BUFFERPIXEL == PIXEL_MODE_8) { | ||||
|             if constexpr (IS_BIG_ENDIAN) { | ||||
|               *dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); | ||||
|               *dptr++ = (color_val & 3) << 3; | ||||
|             } else { | ||||
|               *dptr++ = (color_val & 3) << 3; | ||||
|               *dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); | ||||
|             } | ||||
|           } | ||||
|           // buffer full? Flush. | ||||
|           if (dptr == dbuffer + sizeof(dbuffer)) { | ||||
|             this->write_display_data_(dbuffer, sizeof(dbuffer), 1, 0); | ||||
|             dptr = dbuffer; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       // flush any remaining data | ||||
|       if (dptr != dbuffer) { | ||||
|         this->write_display_data_(dbuffer, dptr - dbuffer, 1, 0); | ||||
|       } | ||||
|     } | ||||
|     this->disable(); | ||||
|   } | ||||
|  | ||||
|   /* PROPERTIES */ | ||||
|  | ||||
|   // GPIO pins | ||||
|   GPIOPin *reset_pin_{nullptr}; | ||||
|   std::vector<GPIOPin *> enable_pins_{}; | ||||
|   GPIOPin *dc_pin_{nullptr}; | ||||
|   uint16_t x_low_{1}; | ||||
|   uint16_t y_low_{1}; | ||||
|   uint16_t x_high_{0}; | ||||
|   uint16_t y_high_{0}; | ||||
|   bool setup_complete_{}; | ||||
|  | ||||
|   // other properties set by configuration | ||||
|   bool invert_colors_{}; | ||||
|   size_t width_; | ||||
|   size_t height_; | ||||
|   int16_t offset_width_; | ||||
|   int16_t offset_height_; | ||||
|   size_t buffer_bytes_{0}; | ||||
|   display::ColorBitness color_depth_; | ||||
|   PixelMode pixel_mode_{PIXEL_MODE_16}; | ||||
|   uint8_t bus_width_{}; | ||||
|   bool spi_16_{}; | ||||
|   uint8_t madctl_{}; | ||||
|   bool draw_from_origin_{false}; | ||||
|   unsigned draw_rounding_{2}; | ||||
|   optional<uint8_t> brightness_{}; | ||||
|   const char *model_{"Unknown"}; | ||||
|   std::vector<uint8_t> init_sequence_{}; | ||||
|   uint8_t madctl_{}; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Class for MIPI SPI displays with a buffer. | ||||
|  * | ||||
|  * @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t | ||||
|  * @tparam BUFFERPIXEL Color depth of the buffer | ||||
|  * @tparam DISPLAYPIXEL Color depth of the display | ||||
|  * @tparam BUS_TYPE The type of the interface bus (single, quad, octal) | ||||
|  * @tparam ROTATION The rotation of the display | ||||
|  * @tparam WIDTH Width of the display in pixels | ||||
|  * @tparam HEIGHT Height of the display in pixels | ||||
|  * @tparam OFFSET_WIDTH The x-offset of the display in pixels | ||||
|  * @tparam OFFSET_HEIGHT The y-offset of the display in pixels | ||||
|  * @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer). | ||||
|  */ | ||||
| template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE, | ||||
|          int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION> | ||||
| class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, | ||||
|                                      OFFSET_WIDTH, OFFSET_HEIGHT> { | ||||
|  public: | ||||
|   MipiSpiBuffer() { this->rotation_ = ROTATION; } | ||||
|  | ||||
|   void dump_config() override { | ||||
|     MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, | ||||
|             OFFSET_HEIGHT>::dump_config(); | ||||
|     esph_log_config(TAG, | ||||
|                     "  Rotation: %d°\n" | ||||
|                     "  Buffer pixels: %d bits\n" | ||||
|                     "  Buffer fraction: 1/%d\n" | ||||
|                     "  Buffer bytes: %zu\n" | ||||
|                     "  Draw rounding: %u", | ||||
|                     this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION, | ||||
|                     this->draw_rounding_); | ||||
|   } | ||||
|  | ||||
|   void setup() override { | ||||
|     MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, | ||||
|             OFFSET_HEIGHT>::setup(); | ||||
|     RAMAllocator<BUFFERTYPE> allocator{}; | ||||
|     this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION); | ||||
|     if (this->buffer_ == nullptr) { | ||||
|       this->mark_failed("Buffer allocation failed"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void update() override { | ||||
| #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||
|     auto now = millis(); | ||||
| #endif | ||||
|     if (this->is_failed()) { | ||||
|       return; | ||||
|     } | ||||
|     // for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of | ||||
|     // the display height, | ||||
|     for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) { | ||||
| #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||
|       auto lap = millis(); | ||||
| #endif | ||||
|       this->end_line_ = this->start_line_ + HEIGHT / FRACTION; | ||||
|       if (this->auto_clear_enabled_) { | ||||
|         this->clear(); | ||||
|       } | ||||
|       if (this->page_ != nullptr) { | ||||
|         this->page_->get_writer()(*this); | ||||
|       } else if (this->writer_.has_value()) { | ||||
|         (*this->writer_)(*this); | ||||
|       } else { | ||||
|         this->test_card(); | ||||
|       } | ||||
| #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||
|       esph_log_v(TAG, "Drawing from line %d took %dms", this->start_line_, millis() - lap); | ||||
|       lap = millis(); | ||||
| #endif | ||||
|       if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) | ||||
|         return; | ||||
|       esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, | ||||
|                  this->y_high_); | ||||
|       // Some chips require that the drawing window be aligned on certain boundaries | ||||
|       auto dr = this->draw_rounding_; | ||||
|       this->x_low_ = this->x_low_ / dr * dr; | ||||
|       this->y_low_ = this->y_low_ / dr * dr; | ||||
|       this->x_high_ = (this->x_high_ + dr) / dr * dr - 1; | ||||
|       this->y_high_ = (this->y_high_ + dr) / dr * dr - 1; | ||||
|       int w = this->x_high_ - this->x_low_ + 1; | ||||
|       int h = this->y_high_ - this->y_low_ + 1; | ||||
|       this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, | ||||
|                               this->y_low_ - this->start_line_, WIDTH - w); | ||||
|       // invalidate watermarks | ||||
|       this->x_low_ = WIDTH; | ||||
|       this->y_low_ = HEIGHT; | ||||
|       this->x_high_ = 0; | ||||
|       this->y_high_ = 0; | ||||
| #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||
|       esph_log_v(TAG, "Write to display took %dms", millis() - lap); | ||||
|       lap = millis(); | ||||
| #endif | ||||
|     } | ||||
| #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||
|     esph_log_v(TAG, "Total update took %dms", millis() - now); | ||||
| #endif | ||||
|   } | ||||
|  | ||||
|   // Draw a pixel at the given coordinates. | ||||
|   void draw_pixel_at(int x, int y, Color color) override { | ||||
|     rotate_coordinates_(x, y); | ||||
|     if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_) | ||||
|       return; | ||||
|     this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color); | ||||
|     if (x < this->x_low_) { | ||||
|       this->x_low_ = x; | ||||
|     } | ||||
|     if (x > this->x_high_) { | ||||
|       this->x_high_ = x; | ||||
|     } | ||||
|     if (y < this->y_low_) { | ||||
|       this->y_low_ = y; | ||||
|     } | ||||
|     if (y > this->y_high_) { | ||||
|       this->y_high_ = y; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Fills the display with a color. | ||||
|   void fill(Color color) override { | ||||
|     this->x_low_ = 0; | ||||
|     this->y_low_ = this->start_line_; | ||||
|     this->x_high_ = WIDTH - 1; | ||||
|     this->y_high_ = this->end_line_ - 1; | ||||
|     std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color)); | ||||
|   } | ||||
|  | ||||
|   int get_width() override { | ||||
|     if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) | ||||
|       return HEIGHT; | ||||
|     return WIDTH; | ||||
|   } | ||||
|  | ||||
|   int get_height() override { | ||||
|     if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) | ||||
|       return WIDTH; | ||||
|     return HEIGHT; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   // Rotate the coordinates to match the display orientation. | ||||
|   void rotate_coordinates_(int &x, int &y) const { | ||||
|     if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) { | ||||
|       x = WIDTH - x - 1; | ||||
|       y = HEIGHT - y - 1; | ||||
|     } else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) { | ||||
|       auto tmp = x; | ||||
|       x = WIDTH - y - 1; | ||||
|       y = tmp; | ||||
|     } else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) { | ||||
|       auto tmp = y; | ||||
|       y = HEIGHT - x - 1; | ||||
|       x = tmp; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Convert a color to the buffer pixel format. | ||||
|   BUFFERTYPE convert_color_(Color &color) const { | ||||
|     if constexpr (BUFFERPIXEL == PIXEL_MODE_8) { | ||||
|       return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6; | ||||
|     } else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) { | ||||
|       if constexpr (IS_BIG_ENDIAN) { | ||||
|         return (color.r & 0xF8) | color.g >> 5 | (color.g & 0x1C) << 11 | (color.b & 0xF8) << 5; | ||||
|       } else { | ||||
|         return (color.r & 0xF8) << 8 | (color.g & 0xFC) << 3 | color.b >> 3; | ||||
|       } | ||||
|     } | ||||
|     return static_cast<BUFFERTYPE>(0); | ||||
|   } | ||||
|  | ||||
|   BUFFERTYPE *buffer_{}; | ||||
|   uint16_t x_low_{WIDTH}; | ||||
|   uint16_t y_low_{HEIGHT}; | ||||
|   uint16_t x_high_{0}; | ||||
|   uint16_t y_high_{0}; | ||||
|   uint16_t start_line_{0}; | ||||
|   uint16_t end_line_{1}; | ||||
| }; | ||||
|  | ||||
| }  // namespace mipi_spi | ||||
| }  // namespace esphome | ||||
|   | ||||
							
								
								
									
										30
									
								
								esphome/components/mipi_spi/models/adafruit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/mipi_spi/models/adafruit.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| from .ili import ST7789V | ||||
|  | ||||
| ST7789V.extend( | ||||
|     "ADAFRUIT-FUNHOUSE", | ||||
|     height=240, | ||||
|     width=240, | ||||
|     offset_height=0, | ||||
|     offset_width=0, | ||||
|     cs_pin=40, | ||||
|     dc_pin=39, | ||||
|     reset_pin=41, | ||||
|     invert_colors=True, | ||||
|     mirror_x=True, | ||||
|     mirror_y=True, | ||||
|     data_rate="80MHz", | ||||
| ) | ||||
|  | ||||
| ST7789V.extend( | ||||
|     "ADAFRUIT-S2-TFT-FEATHER", | ||||
|     height=240, | ||||
|     width=135, | ||||
|     offset_height=52, | ||||
|     offset_width=40, | ||||
|     cs_pin=7, | ||||
|     dc_pin=39, | ||||
|     reset_pin=40, | ||||
|     invert_colors=True, | ||||
| ) | ||||
|  | ||||
| models = {} | ||||
| @@ -67,6 +67,14 @@ RM690B0 = DriverChip( | ||||
|     ), | ||||
| ) | ||||
|  | ||||
| T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD) | ||||
| T4_S3_AMOLED = RM690B0.extend( | ||||
|     "T4-S3", | ||||
|     width=450, | ||||
|     offset_width=16, | ||||
|     cs_pin=11, | ||||
|     reset_pin=13, | ||||
|     enable_pin=9, | ||||
|     bus_mode=TYPE_QUAD, | ||||
| ) | ||||
|  | ||||
| models = {} | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from . import DriverChip | ||||
| from .ili import ILI9488_A | ||||
|  | ||||
| @@ -128,6 +130,7 @@ DriverChip( | ||||
|  | ||||
| ILI9488_A.extend( | ||||
|     "PICO-RESTOUCH-LCD-3.5", | ||||
|     swap_xy=cv.UNDEFINED, | ||||
|     spi_16=True, | ||||
|     pixel_mode="16bit", | ||||
|     mirror_x=True, | ||||
|   | ||||
							
								
								
									
										218
									
								
								esphome/components/nrf52/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								esphome/components/nrf52/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,218 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from pathlib import Path | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.zephyr import ( | ||||
|     copy_files as zephyr_copy_files, | ||||
|     zephyr_add_pm_static, | ||||
|     zephyr_set_core_data, | ||||
|     zephyr_to_code, | ||||
| ) | ||||
| from esphome.components.zephyr.const import ( | ||||
|     BOOTLOADER_MCUBOOT, | ||||
|     KEY_BOOTLOADER, | ||||
|     KEY_ZEPHYR, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BOARD, | ||||
|     CONF_FRAMEWORK, | ||||
|     KEY_CORE, | ||||
|     KEY_FRAMEWORK_VERSION, | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
|     PLATFORM_NRF52, | ||||
| ) | ||||
| from esphome.core import CORE, EsphomeError, coroutine_with_priority | ||||
| from esphome.storage_json import StorageJSON | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| from .boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG | ||||
| from .const import ( | ||||
|     BOOTLOADER_ADAFRUIT, | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD132, | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, | ||||
| ) | ||||
|  | ||||
| # force import gpio to register pin schema | ||||
| from .gpio import nrf52_pin_to_code  # noqa | ||||
|  | ||||
| CODEOWNERS = ["@tomaszduda23"] | ||||
| AUTO_LOAD = ["zephyr"] | ||||
| IS_TARGET_PLATFORM = True | ||||
|  | ||||
|  | ||||
| def set_core_data(config: ConfigType) -> ConfigType: | ||||
|     zephyr_set_core_data(config) | ||||
|     CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52 | ||||
|     CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR | ||||
|     CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(2, 6, 1) | ||||
|  | ||||
|     if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: | ||||
|         zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]) | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| BOOTLOADERS = [ | ||||
|     BOOTLOADER_ADAFRUIT, | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD132, | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, | ||||
|     BOOTLOADER_MCUBOOT, | ||||
| ] | ||||
|  | ||||
|  | ||||
| def _detect_bootloader(config: ConfigType) -> ConfigType: | ||||
|     """Detect the bootloader for the given board.""" | ||||
|     config = config.copy() | ||||
|     bootloaders: list[str] = [] | ||||
|     board = config[CONF_BOARD] | ||||
|  | ||||
|     if board in BOARDS_ZEPHYR and KEY_BOOTLOADER in BOARDS_ZEPHYR[board]: | ||||
|         # this board have bootloaders config available | ||||
|         bootloaders = BOARDS_ZEPHYR[board][KEY_BOOTLOADER] | ||||
|  | ||||
|     if KEY_BOOTLOADER not in config: | ||||
|         if bootloaders: | ||||
|             # there is no bootloader in config -> take first one | ||||
|             config[KEY_BOOTLOADER] = bootloaders[0] | ||||
|         else: | ||||
|             # make mcuboot as default if there is no configuration for that board | ||||
|             config[KEY_BOOTLOADER] = BOOTLOADER_MCUBOOT | ||||
|     elif bootloaders and config[KEY_BOOTLOADER] not in bootloaders: | ||||
|         raise cv.Invalid( | ||||
|             f"{board} does not support {config[KEY_BOOTLOADER]}, select one of: {', '.join(bootloaders)}" | ||||
|         ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_BOARD): cv.string_strict, | ||||
|             cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), | ||||
|         } | ||||
|     ), | ||||
|     _detect_bootloader, | ||||
|     set_core_data, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(1000) | ||||
| async def to_code(config: ConfigType) -> None: | ||||
|     """Convert the configuration to code.""" | ||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||
|     cg.add_build_flag("-DUSE_NRF52") | ||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||
|     cg.add_define("ESPHOME_VARIANT", "NRF52") | ||||
|     cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK]) | ||||
|     cg.add_platformio_option( | ||||
|         "platform", | ||||
|         "https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip", | ||||
|     ) | ||||
|     cg.add_platformio_option( | ||||
|         "platform_packages", | ||||
|         [ | ||||
|             "platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip", | ||||
|             "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip", | ||||
|         ], | ||||
|     ) | ||||
|  | ||||
|     if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT: | ||||
|         # make sure that firmware.zip is created | ||||
|         # for Adafruit_nRF52_Bootloader | ||||
|         cg.add_platformio_option("board_upload.protocol", "nrfutil") | ||||
|         cg.add_platformio_option("board_upload.use_1200bps_touch", "true") | ||||
|         cg.add_platformio_option("board_upload.require_upload_port", "true") | ||||
|         cg.add_platformio_option("board_upload.wait_for_upload_port", "true") | ||||
|  | ||||
|     zephyr_to_code(config) | ||||
|  | ||||
|  | ||||
| def copy_files() -> None: | ||||
|     """Copy files to the build directory.""" | ||||
|     zephyr_copy_files() | ||||
|  | ||||
|  | ||||
| def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: | ||||
|     """Get the download types for the firmware.""" | ||||
|     types = [] | ||||
|     UF2_PATH = "zephyr/zephyr.uf2" | ||||
|     DFU_PATH = "firmware.zip" | ||||
|     HEX_PATH = "zephyr/zephyr.hex" | ||||
|     HEX_MERGED_PATH = "zephyr/merged.hex" | ||||
|     APP_IMAGE_PATH = "zephyr/app_update.bin" | ||||
|     build_dir = Path(storage_json.firmware_bin_path).parent | ||||
|     if (build_dir / UF2_PATH).is_file(): | ||||
|         types = [ | ||||
|             { | ||||
|                 "title": "UF2 package (recommended)", | ||||
|                 "description": "For flashing via Adafruit nRF52 Bootloader as a flash drive.", | ||||
|                 "file": UF2_PATH, | ||||
|                 "download": f"{storage_json.name}.uf2", | ||||
|             }, | ||||
|             { | ||||
|                 "title": "DFU package", | ||||
|                 "description": "For flashing via adafruit-nrfutil using USB CDC.", | ||||
|                 "file": DFU_PATH, | ||||
|                 "download": f"dfu-{storage_json.name}.zip", | ||||
|             }, | ||||
|         ] | ||||
|     else: | ||||
|         types = [ | ||||
|             { | ||||
|                 "title": "HEX package", | ||||
|                 "description": "For flashing via pyocd using SWD.", | ||||
|                 "file": ( | ||||
|                     HEX_MERGED_PATH | ||||
|                     if (build_dir / HEX_MERGED_PATH).is_file() | ||||
|                     else HEX_PATH | ||||
|                 ), | ||||
|                 "download": f"{storage_json.name}.hex", | ||||
|             }, | ||||
|         ] | ||||
|         if (build_dir / APP_IMAGE_PATH).is_file(): | ||||
|             types += [ | ||||
|                 { | ||||
|                     "title": "App update package", | ||||
|                     "description": "For flashing via mcumgr-web using BLE or smpclient using USB CDC.", | ||||
|                     "file": APP_IMAGE_PATH, | ||||
|                     "download": f"app-{storage_json.name}.img", | ||||
|                 }, | ||||
|             ] | ||||
|  | ||||
|     return types | ||||
|  | ||||
|  | ||||
| def _upload_using_platformio( | ||||
|     config: ConfigType, port: str, upload_args: list[str] | ||||
| ) -> int | str: | ||||
|     from esphome import platformio_api | ||||
|  | ||||
|     if port is not None: | ||||
|         upload_args += ["--upload-port", port] | ||||
|     return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) | ||||
|  | ||||
|  | ||||
| def upload_program(config: ConfigType, args, host: str) -> bool: | ||||
|     from esphome.__main__ import check_permissions, get_port_type | ||||
|  | ||||
|     result = 0 | ||||
|     handled = False | ||||
|  | ||||
|     if get_port_type(host) == "SERIAL": | ||||
|         check_permissions(host) | ||||
|         result = _upload_using_platformio(config, host, ["-t", "upload"]) | ||||
|         handled = True | ||||
|  | ||||
|     if host == "PYOCD": | ||||
|         result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"]) | ||||
|         handled = True | ||||
|  | ||||
|     if result != 0: | ||||
|         raise EsphomeError(f"Upload failed with result: {result}") | ||||
|  | ||||
|     return handled | ||||
							
								
								
									
										34
									
								
								esphome/components/nrf52/boards.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/nrf52/boards.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| from esphome.components.zephyr import Section | ||||
| from esphome.components.zephyr.const import KEY_BOOTLOADER | ||||
|  | ||||
| from .const import ( | ||||
|     BOOTLOADER_ADAFRUIT, | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD132, | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, | ||||
| ) | ||||
|  | ||||
| BOARDS_ZEPHYR = { | ||||
|     "adafruit_itsybitsy_nrf52840": { | ||||
|         KEY_BOOTLOADER: [ | ||||
|             BOOTLOADER_ADAFRUIT, | ||||
|             BOOTLOADER_ADAFRUIT_NRF52_SD132, | ||||
|             BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, | ||||
|             BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, | ||||
|         ] | ||||
|     }, | ||||
| } | ||||
|  | ||||
| # https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go | ||||
| # https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map | ||||
| BOOTLOADER_CONFIG = { | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD132: [ | ||||
|         Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), | ||||
|     ], | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [ | ||||
|         Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), | ||||
|     ], | ||||
|     BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [ | ||||
|         Section("empty_app_offset", 0x0, 0x27000, "flash_primary"), | ||||
|     ], | ||||
| } | ||||
							
								
								
									
										4
									
								
								esphome/components/nrf52/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								esphome/components/nrf52/const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| BOOTLOADER_ADAFRUIT = "adafruit" | ||||
| BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132" | ||||
| BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6" | ||||
| BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7" | ||||
							
								
								
									
										53
									
								
								esphome/components/nrf52/gpio.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								esphome/components/nrf52/gpio.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.zephyr.const import zephyr_ns | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_INVERTED, CONF_MODE, CONF_NUMBER, PLATFORM_NRF52 | ||||
|  | ||||
| ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin) | ||||
|  | ||||
|  | ||||
| def _translate_pin(value): | ||||
|     if isinstance(value, dict) or value is None: | ||||
|         raise cv.Invalid( | ||||
|             "This variable only supports pin numbers, not full pin schemas " | ||||
|             "(with inverted and mode)." | ||||
|         ) | ||||
|     if isinstance(value, int): | ||||
|         return value | ||||
|     try: | ||||
|         return int(value) | ||||
|     except ValueError: | ||||
|         pass | ||||
|     # e.g. P0.27 | ||||
|     if len(value) >= len("P0.0") and value[0] == "P" and value[2] == ".": | ||||
|         return cv.int_(value[len("P")].strip()) * 32 + cv.int_( | ||||
|             value[len("P0.") :].strip() | ||||
|         ) | ||||
|     raise cv.Invalid(f"Invalid pin: {value}") | ||||
|  | ||||
|  | ||||
| def validate_gpio_pin(value): | ||||
|     value = _translate_pin(value) | ||||
|     if value < 0 or value > (32 + 16): | ||||
|         raise cv.Invalid(f"NRF52: Invalid pin number: {value}") | ||||
|     return value | ||||
|  | ||||
|  | ||||
| NRF52_PIN_SCHEMA = cv.All( | ||||
|     pins.gpio_base_schema( | ||||
|         ZephyrGPIOPin, | ||||
|         validate_gpio_pin, | ||||
|         modes=pins.GPIO_STANDARD_MODES, | ||||
|     ), | ||||
| ) | ||||
|  | ||||
|  | ||||
| @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_NRF52, NRF52_PIN_SCHEMA) | ||||
| async def nrf52_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
| @@ -1,4 +1,5 @@ | ||||
| import re | ||||
| from typing import Any | ||||
|  | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| @@ -139,6 +140,27 @@ def get_hw_interface_list(): | ||||
|     return [] | ||||
|  | ||||
|  | ||||
| def one_of_interface_validator(additional_values: list[str] | None = None) -> Any: | ||||
|     """Helper to create a one_of validator for SPI interfaces. | ||||
|  | ||||
|     This delays evaluation of get_hw_interface_list() until validation time, | ||||
|     avoiding access to CORE.data during module import. | ||||
|  | ||||
|     Args: | ||||
|         additional_values: List of additional valid values to include | ||||
|     """ | ||||
|     if additional_values is None: | ||||
|         additional_values = [] | ||||
|  | ||||
|     def validator(value: str) -> str: | ||||
|         return cv.one_of( | ||||
|             *sum(get_hw_interface_list(), additional_values), | ||||
|             lower=True, | ||||
|         )(value) | ||||
|  | ||||
|     return cv.All(cv.string, validator) | ||||
|  | ||||
|  | ||||
| # Given an SPI name, return the index of it in the available list | ||||
| def get_spi_index(name): | ||||
|     for i, ilist in enumerate(get_hw_interface_list()): | ||||
| @@ -274,9 +296,8 @@ SPI_SINGLE_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_FORCE_SW): cv.invalid( | ||||
|                 "force_sw is deprecated - use interface: software" | ||||
|             ), | ||||
|             cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( | ||||
|                 *sum(get_hw_interface_list(), ["software", "hardware", "any"]), | ||||
|                 lower=True, | ||||
|             cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator( | ||||
|                 ["software", "hardware", "any"] | ||||
|             ), | ||||
|             cv.Optional(CONF_DATA_PINS): cv.invalid( | ||||
|                 "'data_pins' should be used with 'type: quad or octal' only" | ||||
| @@ -309,10 +330,9 @@ def spi_mode_schema(mode): | ||||
|                     cv.ensure_list(pins.internal_gpio_output_pin_number), | ||||
|                     cv.Length(min=pin_count, max=pin_count), | ||||
|                 ), | ||||
|                 cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( | ||||
|                     *sum(get_hw_interface_list(), ["hardware"]), | ||||
|                     lower=True, | ||||
|                 ), | ||||
|                 cv.Optional( | ||||
|                     CONF_INTERFACE, default="hardware" | ||||
|                 ): one_of_interface_validator(["hardware"]), | ||||
|                 cv.Optional(CONF_MISO_PIN): cv.invalid( | ||||
|                     f"'miso_pin' should not be used with {mode} SPI" | ||||
|                 ), | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import tzlocal | ||||
| from esphome import automation | ||||
| from esphome.automation import Condition | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.zephyr import zephyr_add_prj_conf | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_AT, | ||||
| @@ -25,7 +26,7 @@ from esphome.const import ( | ||||
|     CONF_TIMEZONE, | ||||
|     CONF_TRIGGER_ID, | ||||
| ) | ||||
| from esphome.core import coroutine_with_priority | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -341,6 +342,8 @@ async def register_time(time_var, config): | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| async def to_code(config): | ||||
|     if CORE.using_zephyr: | ||||
|         zephyr_add_prj_conf("POSIX_CLOCK", True) | ||||
|     cg.add_define("USE_TIME") | ||||
|     cg.add_global(time_ns.using) | ||||
|  | ||||
|   | ||||
| @@ -2,13 +2,15 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #ifdef USE_HOST | ||||
| #include <sys/time.h> | ||||
| #elif defined(USE_ZEPHYR) | ||||
| #include <zephyr/posix/time.h> | ||||
| #else | ||||
| #include "lwip/opt.h" | ||||
| #endif | ||||
| #ifdef USE_ESP8266 | ||||
| #include "sys/time.h" | ||||
| #endif | ||||
| #ifdef USE_RP2040 | ||||
| #if defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||
| #include <sys/time.h> | ||||
| #endif | ||||
| #include <cerrno> | ||||
| @@ -22,11 +24,22 @@ static const char *const TAG = "time"; | ||||
|  | ||||
| RealTimeClock::RealTimeClock() = default; | ||||
| void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | ||||
|   ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); | ||||
|   // Update UTC epoch time. | ||||
| #ifdef USE_ZEPHYR | ||||
|   struct timespec ts; | ||||
|   ts.tv_nsec = 0; | ||||
|   ts.tv_sec = static_cast<time_t>(epoch); | ||||
|  | ||||
|   int ret = clock_settime(CLOCK_REALTIME, &ts); | ||||
|  | ||||
|   if (ret != 0) { | ||||
|     ESP_LOGW(TAG, "clock_settime() failed with code %d", ret); | ||||
|   } | ||||
| #else | ||||
|   struct timeval timev { | ||||
|     .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, | ||||
|   }; | ||||
|   ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); | ||||
|   struct timezone tz = {0, 0}; | ||||
|   int ret = settimeofday(&timev, &tz); | ||||
|   if (ret == EINVAL) { | ||||
| @@ -43,7 +56,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | ||||
|   if (ret != 0) { | ||||
|     ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); | ||||
|   } | ||||
|  | ||||
| #endif | ||||
|   auto time = this->now(); | ||||
|   ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, | ||||
|            time.minute, time.second); | ||||
|   | ||||
							
								
								
									
										231
									
								
								esphome/components/zephyr/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								esphome/components/zephyr/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | ||||
| import os | ||||
| from typing import Final, TypedDict | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.const import CONF_BOARD | ||||
| from esphome.core import CORE | ||||
| from esphome.helpers import copy_file_if_changed, write_file_if_changed | ||||
|  | ||||
| from .const import ( | ||||
|     BOOTLOADER_MCUBOOT, | ||||
|     KEY_BOOTLOADER, | ||||
|     KEY_EXTRA_BUILD_FILES, | ||||
|     KEY_OVERLAY, | ||||
|     KEY_PM_STATIC, | ||||
|     KEY_PRJ_CONF, | ||||
|     KEY_ZEPHYR, | ||||
|     zephyr_ns, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@tomaszduda23"] | ||||
| AUTO_LOAD = ["preferences"] | ||||
| KEY_BOARD: Final = "board" | ||||
|  | ||||
| PrjConfValueType = bool | str | int | ||||
|  | ||||
|  | ||||
| class Section: | ||||
|     def __init__(self, name, address, size, region): | ||||
|         self.name = name | ||||
|         self.address = address | ||||
|         self.size = size | ||||
|         self.region = region | ||||
|         self.end_address = self.address + self.size | ||||
|  | ||||
|     def __str__(self): | ||||
|         return ( | ||||
|             f"{self.name}:\n" | ||||
|             f"  address: 0x{self.address:X}\n" | ||||
|             f"  end_address: 0x{self.end_address:X}\n" | ||||
|             f"  region: {self.region}\n" | ||||
|             f"  size: 0x{self.size:X}" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class ZephyrData(TypedDict): | ||||
|     board: str | ||||
|     bootloader: str | ||||
|     prj_conf: dict[str, tuple[PrjConfValueType, bool]] | ||||
|     overlay: str | ||||
|     extra_build_files: dict[str, str] | ||||
|     pm_static: list[Section] | ||||
|  | ||||
|  | ||||
| def zephyr_set_core_data(config): | ||||
|     CORE.data[KEY_ZEPHYR] = ZephyrData( | ||||
|         board=config[CONF_BOARD], | ||||
|         bootloader=config[KEY_BOOTLOADER], | ||||
|         prj_conf={}, | ||||
|         overlay="", | ||||
|         extra_build_files={}, | ||||
|         pm_static=[], | ||||
|     ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def zephyr_data() -> ZephyrData: | ||||
|     return CORE.data[KEY_ZEPHYR] | ||||
|  | ||||
|  | ||||
| def zephyr_add_prj_conf( | ||||
|     name: str, value: PrjConfValueType, required: bool = True | ||||
| ) -> None: | ||||
|     """Set an zephyr prj conf value.""" | ||||
|     if not name.startswith("CONFIG_"): | ||||
|         name = "CONFIG_" + name | ||||
|     prj_conf = zephyr_data()[KEY_PRJ_CONF] | ||||
|     if name not in prj_conf: | ||||
|         prj_conf[name] = (value, required) | ||||
|         return | ||||
|     old_value, old_required = prj_conf[name] | ||||
|     if old_value != value and old_required: | ||||
|         raise ValueError( | ||||
|             f"{name} already set with value '{old_value}', cannot set again to '{value}'" | ||||
|         ) | ||||
|     if required: | ||||
|         prj_conf[name] = (value, required) | ||||
|  | ||||
|  | ||||
| def zephyr_add_overlay(content): | ||||
|     zephyr_data()[KEY_OVERLAY] += content | ||||
|  | ||||
|  | ||||
| def add_extra_build_file(filename: str, path: str) -> bool: | ||||
|     """Add an extra build file to the project.""" | ||||
|     extra_build_files = zephyr_data()[KEY_EXTRA_BUILD_FILES] | ||||
|     if filename not in extra_build_files: | ||||
|         extra_build_files[filename] = path | ||||
|         return True | ||||
|     return False | ||||
|  | ||||
|  | ||||
| def add_extra_script(stage: str, filename: str, path: str): | ||||
|     """Add an extra script to the project.""" | ||||
|     key = f"{stage}:{filename}" | ||||
|     if add_extra_build_file(filename, path): | ||||
|         cg.add_platformio_option("extra_scripts", [key]) | ||||
|  | ||||
|  | ||||
| def zephyr_to_code(config): | ||||
|     cg.add(zephyr_ns.setup_preferences()) | ||||
|     cg.add_build_flag("-DUSE_ZEPHYR") | ||||
|     cg.set_cpp_standard("gnu++20") | ||||
|     # build is done by west so bypass board checking in platformio | ||||
|     cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) | ||||
|  | ||||
|     # c++ support | ||||
|     zephyr_add_prj_conf("NEWLIB_LIBC", True) | ||||
|     zephyr_add_prj_conf("CONFIG_FPU", True) | ||||
|     zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) | ||||
|     zephyr_add_prj_conf("CPLUSPLUS", True) | ||||
|     zephyr_add_prj_conf("CONFIG_STD_CPP20", True) | ||||
|     zephyr_add_prj_conf("LIB_CPLUSPLUS", True) | ||||
|     # preferences | ||||
|     zephyr_add_prj_conf("SETTINGS", True) | ||||
|     zephyr_add_prj_conf("NVS", True) | ||||
|     zephyr_add_prj_conf("FLASH_MAP", True) | ||||
|     zephyr_add_prj_conf("CONFIG_FLASH", True) | ||||
|     # watchdog | ||||
|     zephyr_add_prj_conf("WATCHDOG", True) | ||||
|     zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) | ||||
|     # disable console | ||||
|     zephyr_add_prj_conf("UART_CONSOLE", False) | ||||
|     zephyr_add_prj_conf("CONSOLE", False, False) | ||||
|     # use NFC pins as GPIO | ||||
|     zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) | ||||
|  | ||||
|     # <err> os: ***** USAGE FAULT ***** | ||||
|     # <err> os:   Illegal load of EXC_RETURN into PC | ||||
|     zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048) | ||||
|  | ||||
|     add_extra_script( | ||||
|         "pre", | ||||
|         "pre_build.py", | ||||
|         os.path.join(os.path.dirname(__file__), "pre_build.py.script"), | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def _format_prj_conf_val(value: PrjConfValueType) -> str: | ||||
|     if isinstance(value, bool): | ||||
|         return "y" if value else "n" | ||||
|     if isinstance(value, int): | ||||
|         return str(value) | ||||
|     if isinstance(value, str): | ||||
|         return f'"{value}"' | ||||
|     raise ValueError | ||||
|  | ||||
|  | ||||
| def zephyr_add_cdc_acm(config, id): | ||||
|     zephyr_add_prj_conf("USB_DEVICE_STACK", True) | ||||
|     zephyr_add_prj_conf("USB_CDC_ACM", True) | ||||
|     # prevent device to go to susspend, without this communication stop working in python | ||||
|     # there should be a way to solve it | ||||
|     zephyr_add_prj_conf("USB_DEVICE_REMOTE_WAKEUP", False) | ||||
|     # prevent logging when buffer is full | ||||
|     zephyr_add_prj_conf("USB_CDC_ACM_LOG_LEVEL_WRN", True) | ||||
|     zephyr_add_overlay( | ||||
|         f""" | ||||
| &zephyr_udc0 {{ | ||||
|     cdc_acm_uart{id}: cdc_acm_uart{id} {{ | ||||
|         compatible = "zephyr,cdc-acm-uart"; | ||||
|     }}; | ||||
| }}; | ||||
| """ | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def zephyr_add_pm_static(section: Section): | ||||
|     CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section) | ||||
|  | ||||
|  | ||||
| def copy_files(): | ||||
|     want_opts = zephyr_data()[KEY_PRJ_CONF] | ||||
|  | ||||
|     prj_conf = ( | ||||
|         "\n".join( | ||||
|             f"{name}={_format_prj_conf_val(value[0])}" | ||||
|             for name, value in sorted(want_opts.items()) | ||||
|         ) | ||||
|         + "\n" | ||||
|     ) | ||||
|  | ||||
|     write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), prj_conf) | ||||
|  | ||||
|     write_file_if_changed( | ||||
|         CORE.relative_build_path("zephyr/app.overlay"), | ||||
|         zephyr_data()[KEY_OVERLAY], | ||||
|     ) | ||||
|  | ||||
|     if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT or zephyr_data()[ | ||||
|         KEY_BOARD | ||||
|     ] in ["xiao_ble"]: | ||||
|         fake_board_manifest = """ | ||||
| { | ||||
| "frameworks": [ | ||||
|     "zephyr" | ||||
| ], | ||||
| "name": "esphome nrf52", | ||||
| "upload": { | ||||
|     "maximum_ram_size": 248832, | ||||
|     "maximum_size": 815104 | ||||
| }, | ||||
| "url": "https://esphome.io/", | ||||
| "vendor": "esphome" | ||||
| } | ||||
| """ | ||||
|         write_file_if_changed( | ||||
|             CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"), | ||||
|             fake_board_manifest, | ||||
|         ) | ||||
|  | ||||
|     for filename, path in zephyr_data()[KEY_EXTRA_BUILD_FILES].items(): | ||||
|         copy_file_if_changed( | ||||
|             path, | ||||
|             CORE.relative_build_path(filename), | ||||
|         ) | ||||
|  | ||||
|     pm_static = "\n".join(str(item) for item in zephyr_data()[KEY_PM_STATIC]) | ||||
|     if pm_static: | ||||
|         write_file_if_changed( | ||||
|             CORE.relative_build_path("zephyr/pm_static.yml"), pm_static | ||||
|         ) | ||||
							
								
								
									
										14
									
								
								esphome/components/zephyr/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								esphome/components/zephyr/const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| from typing import Final | ||||
|  | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| BOOTLOADER_MCUBOOT = "mcuboot" | ||||
|  | ||||
| KEY_BOOTLOADER: Final = "bootloader" | ||||
| KEY_EXTRA_BUILD_FILES: Final = "extra_build_files" | ||||
| KEY_OVERLAY: Final = "overlay" | ||||
| KEY_PM_STATIC: Final = "pm_static" | ||||
| KEY_PRJ_CONF: Final = "prj_conf" | ||||
| KEY_ZEPHYR = "zephyr" | ||||
|  | ||||
| zephyr_ns = cg.esphome_ns.namespace("zephyr") | ||||
							
								
								
									
										86
									
								
								esphome/components/zephyr/core.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								esphome/components/zephyr/core.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| #ifdef USE_ZEPHYR | ||||
|  | ||||
| #include <zephyr/kernel.h> | ||||
| #include <zephyr/drivers/watchdog.h> | ||||
| #include <zephyr/sys/reboot.h> | ||||
| #include <zephyr/random/rand32.h> | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| static int wdt_channel_id = -1;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); | ||||
|  | ||||
| void yield() { ::k_yield(); } | ||||
| uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } | ||||
| uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } | ||||
| void delayMicroseconds(uint32_t us) { ::k_usleep(us); } | ||||
| void delay(uint32_t ms) { ::k_msleep(ms); } | ||||
|  | ||||
| void arch_init() { | ||||
|   if (device_is_ready(WDT)) { | ||||
|     static wdt_timeout_cfg wdt_config{}; | ||||
|     wdt_config.flags = WDT_FLAG_RESET_SOC; | ||||
|     wdt_config.window.max = 2000; | ||||
|     wdt_channel_id = wdt_install_timeout(WDT, &wdt_config); | ||||
|     if (wdt_channel_id >= 0) { | ||||
|       wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void arch_feed_wdt() { | ||||
|   if (wdt_channel_id >= 0) { | ||||
|     wdt_feed(WDT, wdt_channel_id); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } | ||||
| uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } | ||||
| uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } | ||||
| uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } | ||||
|  | ||||
| Mutex::Mutex() { | ||||
|   auto *mutex = new k_mutex(); | ||||
|   this->handle_ = mutex; | ||||
|   k_mutex_init(mutex); | ||||
| } | ||||
| Mutex::~Mutex() { delete static_cast<k_mutex *>(this->handle_); } | ||||
| void Mutex::lock() { k_mutex_lock(static_cast<k_mutex *>(this->handle_), K_FOREVER); } | ||||
| bool Mutex::try_lock() { return k_mutex_lock(static_cast<k_mutex *>(this->handle_), K_NO_WAIT) == 0; } | ||||
| void Mutex::unlock() { k_mutex_unlock(static_cast<k_mutex *>(this->handle_)); } | ||||
|  | ||||
| IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); } | ||||
| IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } | ||||
|  | ||||
| uint32_t random_uint32() { return rand(); }  // NOLINT(cert-msc30-c, cert-msc50-cpp) | ||||
| bool random_bytes(uint8_t *data, size_t len) { | ||||
|   sys_rand_get(data, len); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||
|   mac[0] = ((NRF_FICR->DEVICEADDR[1] & 0xFFFF) >> 8) | 0xC0; | ||||
|   mac[1] = NRF_FICR->DEVICEADDR[1] & 0xFFFF; | ||||
|   mac[2] = NRF_FICR->DEVICEADDR[0] >> 24; | ||||
|   mac[3] = NRF_FICR->DEVICEADDR[0] >> 16; | ||||
|   mac[4] = NRF_FICR->DEVICEADDR[0] >> 8; | ||||
|   mac[5] = NRF_FICR->DEVICEADDR[0]; | ||||
| } | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| void setup(); | ||||
| void loop(); | ||||
|  | ||||
| int main() { | ||||
|   setup(); | ||||
|   while (true) { | ||||
|     loop(); | ||||
|     esphome::yield(); | ||||
|   } | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										120
									
								
								esphome/components/zephyr/gpio.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								esphome/components/zephyr/gpio.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| #ifdef USE_ZEPHYR | ||||
| #include "gpio.h" | ||||
| #include <zephyr/drivers/gpio.h> | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace zephyr { | ||||
|  | ||||
| static const char *const TAG = "zephyr"; | ||||
|  | ||||
| static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) { | ||||
|   int ret = 0; | ||||
|   if (flags & gpio::FLAG_INPUT) { | ||||
|     ret |= GPIO_INPUT; | ||||
|   } | ||||
|   if (flags & gpio::FLAG_OUTPUT) { | ||||
|     ret |= GPIO_OUTPUT; | ||||
|     if (value != inverted) { | ||||
|       ret |= GPIO_OUTPUT_INIT_HIGH; | ||||
|     } else { | ||||
|       ret |= GPIO_OUTPUT_INIT_LOW; | ||||
|     } | ||||
|   } | ||||
|   if (flags & gpio::FLAG_PULLUP) { | ||||
|     ret |= GPIO_PULL_UP; | ||||
|   } | ||||
|   if (flags & gpio::FLAG_PULLDOWN) { | ||||
|     ret |= GPIO_PULL_DOWN; | ||||
|   } | ||||
|   if (flags & gpio::FLAG_OPEN_DRAIN) { | ||||
|     ret |= GPIO_OPEN_DRAIN; | ||||
|   } | ||||
|   return ret; | ||||
| } | ||||
|  | ||||
| struct ISRPinArg { | ||||
|   uint8_t pin; | ||||
|   bool inverted; | ||||
| }; | ||||
|  | ||||
| ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const { | ||||
|   auto *arg = new ISRPinArg{};  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|   arg->pin = this->pin_; | ||||
|   arg->inverted = this->inverted_; | ||||
|   return ISRInternalGPIOPin((void *) arg); | ||||
| } | ||||
|  | ||||
| void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { | ||||
|   // TODO | ||||
| } | ||||
|  | ||||
| void ZephyrGPIOPin::setup() { | ||||
|   const struct device *gpio = nullptr; | ||||
|   if (this->pin_ < 32) { | ||||
| #define GPIO0 DT_NODELABEL(gpio0) | ||||
| #if DT_NODE_HAS_STATUS(GPIO0, okay) | ||||
|     gpio = DEVICE_DT_GET(GPIO0); | ||||
| #else | ||||
| #error "gpio0 is disabled" | ||||
| #endif | ||||
|   } else { | ||||
| #define GPIO1 DT_NODELABEL(gpio1) | ||||
| #if DT_NODE_HAS_STATUS(GPIO1, okay) | ||||
|     gpio = DEVICE_DT_GET(GPIO1); | ||||
| #else | ||||
| #error "gpio1 is disabled" | ||||
| #endif | ||||
|   } | ||||
|   if (device_is_ready(gpio)) { | ||||
|     this->gpio_ = gpio; | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "gpio %u is not ready.", this->pin_); | ||||
|     return; | ||||
|   } | ||||
|   this->pin_mode(this->flags_); | ||||
| } | ||||
|  | ||||
| void ZephyrGPIOPin::pin_mode(gpio::Flags flags) { | ||||
|   if (nullptr == this->gpio_) { | ||||
|     return; | ||||
|   } | ||||
|   gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); | ||||
| } | ||||
|  | ||||
| std::string ZephyrGPIOPin::dump_summary() const { | ||||
|   char buffer[32]; | ||||
|   snprintf(buffer, sizeof(buffer), "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32); | ||||
|   return buffer; | ||||
| } | ||||
|  | ||||
| bool ZephyrGPIOPin::digital_read() { | ||||
|   if (nullptr == this->gpio_) { | ||||
|     return false; | ||||
|   } | ||||
|   return bool(gpio_pin_get(this->gpio_, this->pin_ % 32) != this->inverted_); | ||||
| } | ||||
|  | ||||
| void ZephyrGPIOPin::digital_write(bool value) { | ||||
|   // make sure that value is not ignored since it can be inverted e.g. on switch side | ||||
|   // that way init state should be correct | ||||
|   this->value_ = value; | ||||
|   if (nullptr == this->gpio_) { | ||||
|     return; | ||||
|   } | ||||
|   gpio_pin_set(this->gpio_, this->pin_ % 32, value != this->inverted_ ? 1 : 0); | ||||
| } | ||||
| void ZephyrGPIOPin::detach_interrupt() const { | ||||
|   // TODO | ||||
| } | ||||
|  | ||||
| }  // namespace zephyr | ||||
|  | ||||
| bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { | ||||
|   // TODO | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										38
									
								
								esphome/components/zephyr/gpio.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/zephyr/gpio.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ZEPHYR | ||||
| #include "esphome/core/hal.h" | ||||
| struct device; | ||||
| namespace esphome { | ||||
| namespace zephyr { | ||||
|  | ||||
| class ZephyrGPIOPin : public InternalGPIOPin { | ||||
|  public: | ||||
|   void set_pin(uint8_t pin) { this->pin_ = pin; } | ||||
|   void set_inverted(bool inverted) { this->inverted_ = inverted; } | ||||
|   void set_flags(gpio::Flags flags) { this->flags_ = flags; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void pin_mode(gpio::Flags flags) override; | ||||
|   bool digital_read() override; | ||||
|   void digital_write(bool value) override; | ||||
|   std::string dump_summary() const override; | ||||
|   void detach_interrupt() const override; | ||||
|   ISRInternalGPIOPin to_isr() const override; | ||||
|   uint8_t get_pin() const override { return this->pin_; } | ||||
|   bool is_inverted() const override { return this->inverted_; } | ||||
|   gpio::Flags get_flags() const override { return flags_; } | ||||
|  | ||||
|  protected: | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   const device *gpio_ = nullptr; | ||||
|   bool value_ = false; | ||||
| }; | ||||
|  | ||||
| }  // namespace zephyr | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ZEPHYR | ||||
							
								
								
									
										4
									
								
								esphome/components/zephyr/pre_build.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								esphome/components/zephyr/pre_build.py.script
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| Import("env") | ||||
|  | ||||
| board_config = env.BoardConfig() | ||||
| board_config.update("frameworks", ["arduino", "zephyr"]) | ||||
							
								
								
									
										156
									
								
								esphome/components/zephyr/preferences.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								esphome/components/zephyr/preferences.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| #ifdef USE_ZEPHYR | ||||
|  | ||||
| #include <zephyr/kernel.h> | ||||
| #include "esphome/core/preferences.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <zephyr/settings/settings.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace zephyr { | ||||
|  | ||||
| static const char *const TAG = "zephyr.preferences"; | ||||
|  | ||||
| #define ESPHOME_SETTINGS_KEY "esphome" | ||||
|  | ||||
| class ZephyrPreferenceBackend : public ESPPreferenceBackend { | ||||
|  public: | ||||
|   ZephyrPreferenceBackend(uint32_t type) { this->type_ = type; } | ||||
|   ZephyrPreferenceBackend(uint32_t type, std::vector<uint8_t> &&data) : data(std::move(data)) { this->type_ = type; } | ||||
|  | ||||
|   bool save(const uint8_t *data, size_t len) override { | ||||
|     this->data.resize(len); | ||||
|     std::memcpy(this->data.data(), data, len); | ||||
|     ESP_LOGVV(TAG, "save key: %u, len: %d", this->type_, len); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   bool load(uint8_t *data, size_t len) override { | ||||
|     if (len != this->data.size()) { | ||||
|       ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", get_key().c_str(), this->data.size(), len); | ||||
|       return false; | ||||
|     } | ||||
|     std::memcpy(data, this->data.data(), len); | ||||
|     ESP_LOGVV(TAG, "load key: %u, len: %d", this->type_, len); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   uint32_t get_type() const { return this->type_; } | ||||
|   std::string get_key() const { return str_sprintf(ESPHOME_SETTINGS_KEY "/%" PRIx32, this->type_); } | ||||
|  | ||||
|   std::vector<uint8_t> data; | ||||
|  | ||||
|  protected: | ||||
|   uint32_t type_ = 0; | ||||
| }; | ||||
|  | ||||
| class ZephyrPreferences : public ESPPreferences { | ||||
|  public: | ||||
|   void open() { | ||||
|     int err = settings_subsys_init(); | ||||
|     if (err) { | ||||
|       ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     static struct settings_handler settings_cb = { | ||||
|         .name = ESPHOME_SETTINGS_KEY, | ||||
|         .h_set = load_setting, | ||||
|         .h_export = export_settings, | ||||
|     }; | ||||
|  | ||||
|     err = settings_register(&settings_cb); | ||||
|     if (err) { | ||||
|       ESP_LOGE(TAG, "setting_register failed, err, %d", err); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     err = settings_load_subtree(ESPHOME_SETTINGS_KEY); | ||||
|     if (err) { | ||||
|       ESP_LOGE(TAG, "Cannot load settings, err: %d", err); | ||||
|       return; | ||||
|     } | ||||
|     ESP_LOGD(TAG, "Loaded %u settings.", this->backends_.size()); | ||||
|   } | ||||
|  | ||||
|   ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { | ||||
|     return make_preference(length, type); | ||||
|   } | ||||
|  | ||||
|   ESPPreferenceObject make_preference(size_t length, uint32_t type) override { | ||||
|     for (auto *backend : this->backends_) { | ||||
|       if (backend->get_type() == type) { | ||||
|         return ESPPreferenceObject(backend); | ||||
|       } | ||||
|     } | ||||
|     printf("type %u size %u\n", type, this->backends_.size()); | ||||
|     auto *pref = new ZephyrPreferenceBackend(type);  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|     ESP_LOGD(TAG, "Add new setting %s.", pref->get_key().c_str()); | ||||
|     this->backends_.push_back(pref); | ||||
|     return ESPPreferenceObject(pref); | ||||
|   } | ||||
|  | ||||
|   bool sync() override { | ||||
|     ESP_LOGD(TAG, "Save settings"); | ||||
|     int err = settings_save(); | ||||
|     if (err) { | ||||
|       ESP_LOGE(TAG, "Cannot save settings, err: %d", err); | ||||
|       return false; | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   bool reset() override { | ||||
|     ESP_LOGD(TAG, "Reset settings"); | ||||
|     for (auto *backend : this->backends_) { | ||||
|       // save empty delete data | ||||
|       backend->data.clear(); | ||||
|     } | ||||
|     sync(); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   std::vector<ZephyrPreferenceBackend *> backends_; | ||||
|  | ||||
|   static int load_setting(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { | ||||
|     auto type = parse_hex<uint32_t>(name); | ||||
|     if (!type.has_value()) { | ||||
|       std::string full_name(ESPHOME_SETTINGS_KEY); | ||||
|       full_name += "/"; | ||||
|       full_name += name; | ||||
|       // Delete unusable keys. Otherwise it will stay in flash forever. | ||||
|       settings_delete(full_name.c_str()); | ||||
|       return 1; | ||||
|     } | ||||
|     std::vector<uint8_t> data(len); | ||||
|     int err = read_cb(cb_arg, data.data(), len); | ||||
|  | ||||
|     ESP_LOGD(TAG, "load setting, name: %s(%u), len %u, err %u", name, *type, len, err); | ||||
|     auto *pref = new ZephyrPreferenceBackend(*type, std::move(data));  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|     static_cast<ZephyrPreferences *>(global_preferences)->backends_.push_back(pref); | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   static int export_settings(int (*cb)(const char *name, const void *value, size_t val_len)) { | ||||
|     for (auto *backend : static_cast<ZephyrPreferences *>(global_preferences)->backends_) { | ||||
|       auto name = backend->get_key(); | ||||
|       int err = cb(name.c_str(), backend->data.data(), backend->data.size()); | ||||
|       ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name.c_str(), backend->data.size(), err); | ||||
|     } | ||||
|     return 0; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| void setup_preferences() { | ||||
|   auto *prefs = new ZephyrPreferences();  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|   global_preferences = prefs; | ||||
|   prefs->open(); | ||||
| } | ||||
|  | ||||
| }  // namespace zephyr | ||||
|  | ||||
| ESPPreferences *global_preferences;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										13
									
								
								esphome/components/zephyr/preferences.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								esphome/components/zephyr/preferences.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ZEPHYR | ||||
|  | ||||
| namespace esphome { | ||||
| namespace zephyr { | ||||
|  | ||||
| void setup_preferences(); | ||||
|  | ||||
| }  // namespace zephyr | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -21,6 +21,7 @@ class Platform(StrEnum): | ||||
|     HOST = "host" | ||||
|     LIBRETINY_OLDSTYLE = "libretiny" | ||||
|     LN882X = "ln882x" | ||||
|     NRF52 = "nrf52" | ||||
|     RP2040 = "rp2040" | ||||
|     RTL87XX = "rtl87xx" | ||||
|  | ||||
| @@ -31,6 +32,7 @@ class Framework(StrEnum): | ||||
|     ARDUINO = "arduino" | ||||
|     ESP_IDF = "esp-idf" | ||||
|     NATIVE = "host" | ||||
|     ZEPHYR = "zephyr" | ||||
|  | ||||
|  | ||||
| class PlatformFramework(Enum): | ||||
| @@ -47,6 +49,9 @@ class PlatformFramework(Enum): | ||||
|     RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) | ||||
|     LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) | ||||
|  | ||||
|     # Zephyr framework platforms | ||||
|     NRF52_ZEPHYR = (Platform.NRF52, Framework.ZEPHYR) | ||||
|  | ||||
|     # Host platform (native) | ||||
|     HOST_NATIVE = (Platform.HOST, Framework.NATIVE) | ||||
|  | ||||
| @@ -58,6 +63,7 @@ PLATFORM_ESP8266 = Platform.ESP8266 | ||||
| PLATFORM_HOST = Platform.HOST | ||||
| PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE | ||||
| PLATFORM_LN882X = Platform.LN882X | ||||
| PLATFORM_NRF52 = Platform.NRF52 | ||||
| PLATFORM_RP2040 = Platform.RP2040 | ||||
| PLATFORM_RTL87XX = Platform.RTL87XX | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,7 @@ from esphome.const import ( | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_HOST, | ||||
|     PLATFORM_LN882X, | ||||
|     PLATFORM_NRF52, | ||||
|     PLATFORM_RP2040, | ||||
|     PLATFORM_RTL87XX, | ||||
| ) | ||||
| @@ -670,6 +671,10 @@ class EsphomeCore: | ||||
|     def is_libretiny(self): | ||||
|         return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x | ||||
|  | ||||
|     @property | ||||
|     def is_nrf52(self): | ||||
|         return self.target_platform == PLATFORM_NRF52 | ||||
|  | ||||
|     @property | ||||
|     def is_host(self): | ||||
|         return self.target_platform == PLATFORM_HOST | ||||
| @@ -686,6 +691,10 @@ class EsphomeCore: | ||||
|     def using_esp_idf(self): | ||||
|         return self.target_framework == "esp-idf" | ||||
|  | ||||
|     @property | ||||
|     def using_zephyr(self): | ||||
|         return self.target_framework == "zephyr" | ||||
|  | ||||
|     def add_job(self, func, *args, **kwargs) -> None: | ||||
|         self.event_loop.add_job(func, *args, **kwargs) | ||||
|  | ||||
|   | ||||
| @@ -321,7 +321,10 @@ void Application::disable_component_loop_(Component *component) { | ||||
|           // Decrement so we'll process the swapped component next | ||||
|           this->current_loop_index_--; | ||||
|           // Update the loop start time to current time so the swapped component | ||||
|           // gets correct timing instead of inheriting stale timing | ||||
|           // gets correct timing instead of inheriting stale timing. | ||||
|           // This prevents integer underflow in timing calculations by ensuring | ||||
|           // the swapped component starts with a fresh timing reference, avoiding | ||||
|           // errors caused by stale or wrapped timing values. | ||||
|           this->loop_component_start_time_ = millis(); | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -16,373 +16,186 @@ void ComponentIterator::begin(bool include_internal) { | ||||
|   this->at_ = 0; | ||||
|   this->include_internal_ = include_internal; | ||||
| } | ||||
|  | ||||
| template<typename PlatformItem> | ||||
| void ComponentIterator::process_platform_item_(const std::vector<PlatformItem *> &items, | ||||
|                                                bool (ComponentIterator::*on_item)(PlatformItem *)) { | ||||
|   if (this->at_ >= items.size()) { | ||||
|     this->advance_platform_(); | ||||
|   } else { | ||||
|     PlatformItem *item = items[this->at_]; | ||||
|     if ((item->is_internal() && !this->include_internal_) || (this->*on_item)(item)) { | ||||
|       this->at_++; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ComponentIterator::advance_platform_() { | ||||
|   this->state_ = static_cast<IteratorState>(static_cast<uint32_t>(this->state_) + 1); | ||||
|   this->at_ = 0; | ||||
| } | ||||
|  | ||||
| void ComponentIterator::advance() { | ||||
|   bool advance_platform = false; | ||||
|   bool success = true; | ||||
|   switch (this->state_) { | ||||
|     case IteratorState::NONE: | ||||
|       // not started | ||||
|       return; | ||||
|     case IteratorState::BEGIN: | ||||
|       if (this->on_begin()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         return; | ||||
|         advance_platform_(); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|     case IteratorState::BINARY_SENSOR: | ||||
|       if (this->at_ >= App.get_binary_sensors().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *binary_sensor = App.get_binary_sensors()[this->at_]; | ||||
|         if (binary_sensor->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_binary_sensor(binary_sensor); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_COVER | ||||
|     case IteratorState::COVER: | ||||
|       if (this->at_ >= App.get_covers().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *cover = App.get_covers()[this->at_]; | ||||
|         if (cover->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_cover(cover); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_FAN | ||||
|     case IteratorState::FAN: | ||||
|       if (this->at_ >= App.get_fans().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *fan = App.get_fans()[this->at_]; | ||||
|         if (fan->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_fan(fan); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIGHT | ||||
|     case IteratorState::LIGHT: | ||||
|       if (this->at_ >= App.get_lights().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *light = App.get_lights()[this->at_]; | ||||
|         if (light->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_light(light); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
|     case IteratorState::SENSOR: | ||||
|       if (this->at_ >= App.get_sensors().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *sensor = App.get_sensors()[this->at_]; | ||||
|         if (sensor->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_sensor(sensor); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SWITCH | ||||
|     case IteratorState::SWITCH: | ||||
|       if (this->at_ >= App.get_switches().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *a_switch = App.get_switches()[this->at_]; | ||||
|         if (a_switch->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_switch(a_switch); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BUTTON | ||||
|     case IteratorState::BUTTON: | ||||
|       if (this->at_ >= App.get_buttons().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *button = App.get_buttons()[this->at_]; | ||||
|         if (button->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_button(button); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|     case IteratorState::TEXT_SENSOR: | ||||
|       if (this->at_ >= App.get_text_sensors().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *text_sensor = App.get_text_sensors()[this->at_]; | ||||
|         if (text_sensor->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_text_sensor(text_sensor); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
|     case IteratorState ::SERVICE: | ||||
|       if (this->at_ >= api::global_api_server->get_user_services().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *service = api::global_api_server->get_user_services()[this->at_]; | ||||
|         success = this->on_service(service); | ||||
|       } | ||||
|     case IteratorState::SERVICE: | ||||
|       this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CAMERA | ||||
|     case IteratorState::CAMERA: | ||||
|       if (camera::Camera::instance() == nullptr) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         if (camera::Camera::instance()->is_internal() && !this->include_internal_) { | ||||
|           advance_platform = success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           advance_platform = success = this->on_camera(camera::Camera::instance()); | ||||
|         } | ||||
|     case IteratorState::CAMERA: { | ||||
|       camera::Camera *camera_instance = camera::Camera::instance(); | ||||
|       if (camera_instance != nullptr && (!camera_instance->is_internal() || this->include_internal_)) { | ||||
|         this->on_camera(camera_instance); | ||||
|       } | ||||
|       break; | ||||
|       advance_platform_(); | ||||
|     } break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CLIMATE | ||||
|     case IteratorState::CLIMATE: | ||||
|       if (this->at_ >= App.get_climates().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *climate = App.get_climates()[this->at_]; | ||||
|         if (climate->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_climate(climate); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
|     case IteratorState::NUMBER: | ||||
|       if (this->at_ >= App.get_numbers().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *number = App.get_numbers()[this->at_]; | ||||
|         if (number->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_number(number); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATE | ||||
|     case IteratorState::DATETIME_DATE: | ||||
|       if (this->at_ >= App.get_dates().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *date = App.get_dates()[this->at_]; | ||||
|         if (date->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_date(date); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_TIME | ||||
|     case IteratorState::DATETIME_TIME: | ||||
|       if (this->at_ >= App.get_times().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *time = App.get_times()[this->at_]; | ||||
|         if (time->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_time(time); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_times(), &ComponentIterator::on_time); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
|     case IteratorState::DATETIME_DATETIME: | ||||
|       if (this->at_ >= App.get_datetimes().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *datetime = App.get_datetimes()[this->at_]; | ||||
|         if (datetime->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_datetime(datetime); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT | ||||
|     case IteratorState::TEXT: | ||||
|       if (this->at_ >= App.get_texts().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *text = App.get_texts()[this->at_]; | ||||
|         if (text->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_text(text); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SELECT | ||||
|     case IteratorState::SELECT: | ||||
|       if (this->at_ >= App.get_selects().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *select = App.get_selects()[this->at_]; | ||||
|         if (select->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_select(select); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LOCK | ||||
|     case IteratorState::LOCK: | ||||
|       if (this->at_ >= App.get_locks().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *a_lock = App.get_locks()[this->at_]; | ||||
|         if (a_lock->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_lock(a_lock); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_VALVE | ||||
|     case IteratorState::VALVE: | ||||
|       if (this->at_ >= App.get_valves().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *valve = App.get_valves()[this->at_]; | ||||
|         if (valve->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_valve(valve); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|     case IteratorState::MEDIA_PLAYER: | ||||
|       if (this->at_ >= App.get_media_players().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *media_player = App.get_media_players()[this->at_]; | ||||
|         if (media_player->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_media_player(media_player); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|     case IteratorState::ALARM_CONTROL_PANEL: | ||||
|       if (this->at_ >= App.get_alarm_control_panels().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *a_alarm_control_panel = App.get_alarm_control_panels()[this->at_]; | ||||
|         if (a_alarm_control_panel->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_alarm_control_panel(a_alarm_control_panel); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_EVENT | ||||
|     case IteratorState::EVENT: | ||||
|       if (this->at_ >= App.get_events().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *event = App.get_events()[this->at_]; | ||||
|         if (event->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_event(event); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_UPDATE | ||||
|     case IteratorState::UPDATE: | ||||
|       if (this->at_ >= App.get_updates().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *update = App.get_updates()[this->at_]; | ||||
|         if (update->is_internal() && !this->include_internal_) { | ||||
|           success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           success = this->on_update(update); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
|     case IteratorState::MAX: | ||||
|       if (this->on_end()) { | ||||
|         this->state_ = IteratorState::NONE; | ||||
|       } | ||||
|       return; | ||||
|   } | ||||
|  | ||||
|   if (advance_platform) { | ||||
|     this->state_ = static_cast<IteratorState>(static_cast<uint8_t>(this->state_) + 1); | ||||
|     this->at_ = 0; | ||||
|   } else if (success) { | ||||
|     this->at_++; | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool ComponentIterator::on_end() { return true; } | ||||
| bool ComponentIterator::on_begin() { return true; } | ||||
| #ifdef USE_API_SERVICES | ||||
|   | ||||
| @@ -171,6 +171,11 @@ class ComponentIterator { | ||||
|   } state_{IteratorState::NONE}; | ||||
|   uint16_t at_{0};  // Supports up to 65,535 entities per type | ||||
|   bool include_internal_{false}; | ||||
|  | ||||
|   template<typename PlatformItem> | ||||
|   void process_platform_item_(const std::vector<PlatformItem *> &items, | ||||
|                               bool (ComponentIterator::*on_item)(PlatformItem *)); | ||||
|   void advance_platform_(); | ||||
| }; | ||||
|  | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <array> | ||||
| #include <cmath> | ||||
| #include <cstdint> | ||||
| #include <cstring> | ||||
| @@ -678,7 +679,7 @@ class InterruptLock { | ||||
|   ~InterruptLock(); | ||||
|  | ||||
|  protected: | ||||
| #if defined(USE_ESP8266) || defined(USE_RP2040) | ||||
| #if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||
|   uint32_t state_; | ||||
| #endif | ||||
| }; | ||||
|   | ||||
							
								
								
									
										1
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| """ESPHome tests package.""" | ||||
							
								
								
									
										0
									
								
								tests/component_tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/component_tests/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/component_tests/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -5,18 +5,30 @@ from __future__ import annotations | ||||
| from collections.abc import Callable, Generator | ||||
| from pathlib import Path | ||||
| import sys | ||||
| from typing import Any | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from esphome import config, final_validate | ||||
| from esphome.const import ( | ||||
|     KEY_CORE, | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
|     PlatformFramework, | ||||
| ) | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| # Add package root to python path | ||||
| here = Path(__file__).parent | ||||
| package_root = here.parent.parent | ||||
| sys.path.insert(0, package_root.as_posix()) | ||||
|  | ||||
| from esphome.__main__ import generate_cpp_contents  # noqa: E402 | ||||
| from esphome.config import read_config  # noqa: E402 | ||||
| from esphome.config import Config, read_config  # noqa: E402 | ||||
| from esphome.core import CORE  # noqa: E402 | ||||
|  | ||||
| from .types import SetCoreConfigCallable  # noqa: E402 | ||||
|  | ||||
|  | ||||
| @pytest.fixture(autouse=True) | ||||
| def config_path(request: pytest.FixtureRequest) -> Generator[None]: | ||||
| @@ -36,6 +48,59 @@ def config_path(request: pytest.FixtureRequest) -> Generator[None]: | ||||
|     CORE.config_path = original_path | ||||
|  | ||||
|  | ||||
| @pytest.fixture(autouse=True) | ||||
| def reset_core() -> Generator[None]: | ||||
|     """Reset CORE after each test.""" | ||||
|     yield | ||||
|     CORE.reset() | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def set_core_config() -> Generator[SetCoreConfigCallable]: | ||||
|     """Fixture to set up the core configuration for tests.""" | ||||
|  | ||||
|     def setter( | ||||
|         platform_framework: PlatformFramework, | ||||
|         /, | ||||
|         *, | ||||
|         core_data: ConfigType | None = None, | ||||
|         platform_data: ConfigType | None = None, | ||||
|     ) -> None: | ||||
|         platform, framework = platform_framework.value | ||||
|  | ||||
|         # Set base core configuration | ||||
|         CORE.data[KEY_CORE] = { | ||||
|             KEY_TARGET_PLATFORM: platform.value, | ||||
|             KEY_TARGET_FRAMEWORK: framework.value, | ||||
|         } | ||||
|  | ||||
|         # Update with any additional core data | ||||
|         if core_data: | ||||
|             CORE.data[KEY_CORE].update(core_data) | ||||
|  | ||||
|         # Set platform-specific data | ||||
|         if platform_data: | ||||
|             CORE.data[platform.value] = platform_data | ||||
|  | ||||
|         config.path_context.set([]) | ||||
|         final_validate.full_config.set(Config()) | ||||
|  | ||||
|     yield setter | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def set_component_config() -> Callable[[str, Any], None]: | ||||
|     """ | ||||
|     Fixture to set a component configuration in the mock config. | ||||
|     This must be used after the core configuration has been set up. | ||||
|     """ | ||||
|  | ||||
|     def setter(name: str, value: Any) -> None: | ||||
|         final_validate.full_config.get()[name] = value | ||||
|  | ||||
|     return setter | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]: | ||||
|     """Return a function to get absolute paths relative to the component's fixtures directory.""" | ||||
| @@ -60,7 +125,7 @@ def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Pat | ||||
|  | ||||
| @pytest.fixture | ||||
| def generate_main() -> Generator[Callable[[str | Path], str]]: | ||||
|     """Generates the C++ main.cpp file and returns it in string form.""" | ||||
|     """Generates the C++ main.cpp from a given yaml file and returns it in string form.""" | ||||
|  | ||||
|     def generator(path: str | Path) -> str: | ||||
|         CORE.config_path = str(path) | ||||
| @@ -69,5 +134,3 @@ def generate_main() -> Generator[Callable[[str | Path], str]]: | ||||
|         return CORE.cpp_main_section | ||||
|  | ||||
|     yield generator | ||||
|  | ||||
|     CORE.reset() | ||||
|   | ||||
							
								
								
									
										0
									
								
								tests/component_tests/deep_sleep/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/deep_sleep/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/component_tests/image/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/image/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/component_tests/mipi_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/mipi_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										25
									
								
								tests/component_tests/mipi_spi/fixtures/lvgl.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								tests/component_tests/mipi_spi/fixtures/lvgl.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| esphome: | ||||
|   name: c3-7735 | ||||
|  | ||||
| esp32: | ||||
|   board: lolin_c3_mini | ||||
|  | ||||
| spi: | ||||
|   mosi_pin: | ||||
|     number: GPIO2 | ||||
|     ignore_strapping_warning: true | ||||
|   clk_pin: GPIO1 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     data_rate: 20MHz | ||||
|     model: st7735 | ||||
|     cs_pin: | ||||
|       number: GPIO8 | ||||
|       ignore_strapping_warning: true | ||||
|     dc_pin: | ||||
|       number: GPIO3 | ||||
|     reset_pin: | ||||
|       number: GPIO4 | ||||
|  | ||||
| lvgl: | ||||
							
								
								
									
										20
									
								
								tests/component_tests/mipi_spi/fixtures/native.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								tests/component_tests/mipi_spi/fixtures/native.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| esphome: | ||||
|   name: jc3636w518 | ||||
|  | ||||
| esp32: | ||||
|   board: esp32-s3-devkitc-1 | ||||
|   framework: | ||||
|     type: esp-idf | ||||
|  | ||||
| psram: | ||||
|   mode: octal | ||||
|  | ||||
| spi: | ||||
|   id: display_qspi | ||||
|   type: quad | ||||
|   clk_pin: 9 | ||||
|   data_pins: [11, 12, 13, 14] | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: jc3636w518 | ||||
							
								
								
									
										387
									
								
								tests/component_tests/mipi_spi/test_init.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										387
									
								
								tests/component_tests/mipi_spi/test_init.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,387 @@ | ||||
| """Tests for mpip_spi configuration validation.""" | ||||
|  | ||||
| from collections.abc import Callable | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from esphome import config_validation as cv | ||||
| from esphome.components.esp32 import ( | ||||
|     KEY_BOARD, | ||||
|     KEY_ESP32, | ||||
|     KEY_VARIANT, | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32S3, | ||||
|     VARIANTS, | ||||
| ) | ||||
| from esphome.components.esp32.gpio import validate_gpio_pin | ||||
| from esphome.components.mipi_spi.display import ( | ||||
|     CONF_BUS_MODE, | ||||
|     CONF_NATIVE_HEIGHT, | ||||
|     CONFIG_SCHEMA, | ||||
|     FINAL_VALIDATE_SCHEMA, | ||||
|     MODELS, | ||||
|     dimension_schema, | ||||
| ) | ||||
| from esphome.const import ( | ||||
|     CONF_DC_PIN, | ||||
|     CONF_DIMENSIONS, | ||||
|     CONF_HEIGHT, | ||||
|     CONF_INIT_SEQUENCE, | ||||
|     CONF_WIDTH, | ||||
|     PlatformFramework, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.pins import internal_gpio_pin_number | ||||
| from esphome.types import ConfigType | ||||
| from tests.component_tests.types import SetCoreConfigCallable | ||||
|  | ||||
|  | ||||
| def run_schema_validation(config: ConfigType) -> None: | ||||
|     """Run schema validation on a configuration.""" | ||||
|     FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def choose_variant_with_pins() -> Callable[..., None]: | ||||
|     """ | ||||
|     Set the ESP32 variant for the given model based on pins. For ESP32 only since the other platforms | ||||
|     do not have variants. | ||||
|     """ | ||||
|  | ||||
|     def chooser(*pins: int | str | None) -> None: | ||||
|         for v in VARIANTS: | ||||
|             try: | ||||
|                 CORE.data[KEY_ESP32][KEY_VARIANT] = v | ||||
|                 for pin in pins: | ||||
|                     if pin is not None: | ||||
|                         pin = internal_gpio_pin_number(pin) | ||||
|                         validate_gpio_pin(pin) | ||||
|                 return | ||||
|             except cv.Invalid: | ||||
|                 continue | ||||
|  | ||||
|     return chooser | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     ("config", "error_match"), | ||||
|     [ | ||||
|         pytest.param( | ||||
|             "a string", | ||||
|             "expected a dictionary", | ||||
|             id="invalid_string_config", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"id": "display_id"}, | ||||
|             r"required key not provided @ data\['model'\]", | ||||
|             id="missing_model", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"id": "display_id", "model": "custom", "init_sequence": [[0x36, 0x01]]}, | ||||
|             r"required key not provided @ data\['dimensions'\]", | ||||
|             id="missing_dimensions", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             { | ||||
|                 "model": "custom", | ||||
|                 "dc_pin": 18, | ||||
|                 "dimensions": {"width": 320, "height": 240}, | ||||
|             }, | ||||
|             r"required key not provided @ data\['init_sequence'\]", | ||||
|             id="missing_init_sequence", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             { | ||||
|                 "id": "display_id", | ||||
|                 "model": "custom", | ||||
|                 "dimensions": {"width": 320, "height": 240}, | ||||
|                 "draw_rounding": 13, | ||||
|                 "init_sequence": [[0xA0, 0x01]], | ||||
|             }, | ||||
|             r"value must be a power of two for dictionary value @ data\['draw_rounding'\]", | ||||
|             id="invalid_draw_rounding", | ||||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_basic_configuration_errors( | ||||
|     config: str | ConfigType, | ||||
|     error_match: str, | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
| ) -> None: | ||||
|     """Test basic configuration validation errors""" | ||||
|  | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_IDF, | ||||
|         platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, | ||||
|     ) | ||||
|  | ||||
|     with pytest.raises(cv.Invalid, match=error_match): | ||||
|         run_schema_validation(config) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     ("rounding", "config", "error_match"), | ||||
|     [ | ||||
|         pytest.param( | ||||
|             4, | ||||
|             {"width": 320}, | ||||
|             r"required key not provided @ data\['height'\]", | ||||
|             id="missing_height", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             32, | ||||
|             {"width": 320, "height": 111}, | ||||
|             "Dimensions and offsets must be divisible by 32", | ||||
|             id="dimensions_not_divisible", | ||||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_dimension_validation( | ||||
|     rounding: int, | ||||
|     config: ConfigType, | ||||
|     error_match: str, | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
| ) -> None: | ||||
|     """Test dimension-related validation errors""" | ||||
|  | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_IDF, | ||||
|         platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, | ||||
|     ) | ||||
|  | ||||
|     with pytest.raises(cv.Invalid, match=error_match): | ||||
|         dimension_schema(rounding)(config) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     ("config", "error_match"), | ||||
|     [ | ||||
|         pytest.param( | ||||
|             { | ||||
|                 "model": "JC3248W535", | ||||
|                 "transform": {"mirror_x": False, "mirror_y": True, "swap_xy": True}, | ||||
|             }, | ||||
|             "Axis swapping not supported by this model", | ||||
|             id="axis_swapping_not_supported", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             { | ||||
|                 "model": "custom", | ||||
|                 "dimensions": {"width": 320, "height": 240}, | ||||
|                 "transform": {"mirror_x": False, "mirror_y": True, "swap_xy": False}, | ||||
|                 "init_sequence": [[0x36, 0x01]], | ||||
|             }, | ||||
|             r"transform is not supported when MADCTL \(0X36\) is in the init sequence", | ||||
|             id="transform_with_madctl", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             { | ||||
|                 "model": "custom", | ||||
|                 "dimensions": {"width": 320, "height": 240}, | ||||
|                 "init_sequence": [[0x3A, 0x01]], | ||||
|             }, | ||||
|             r"PIXFMT \(0X3A\) should not be in the init sequence, it will be set automatically", | ||||
|             id="pixfmt_in_init_sequence", | ||||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_transform_and_init_sequence_errors( | ||||
|     config: ConfigType, | ||||
|     error_match: str, | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
| ) -> None: | ||||
|     """Test transform and init sequence validation errors""" | ||||
|  | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_IDF, | ||||
|         platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, | ||||
|     ) | ||||
|  | ||||
|     with pytest.raises(cv.Invalid, match=error_match): | ||||
|         run_schema_validation(config) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     ("config", "error_match"), | ||||
|     [ | ||||
|         pytest.param( | ||||
|             {"model": "t4-s3", "dc_pin": 18}, | ||||
|             "DC pin is not supported in quad mode", | ||||
|             id="dc_pin_not_supported_quad_mode", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"model": "t4-s3", "color_depth": 18}, | ||||
|             "Unknown value '18', valid options are '16', '16bit", | ||||
|             id="invalid_color_depth_t4_s3", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"model": "t-embed", "color_depth": 24}, | ||||
|             "Unknown value '24', valid options are '16', '8", | ||||
|             id="invalid_color_depth_t_embed", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"model": "ili9488"}, | ||||
|             "DC pin is required in single mode", | ||||
|             id="dc_pin_required_single_mode", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"model": "wt32-sc01-plus", "brightness": 128}, | ||||
|             r"extra keys not allowed @ data\['brightness'\]", | ||||
|             id="brightness_not_supported", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"model": "T-DISPLAY-S3-PRO"}, | ||||
|             "PSRAM is required for this display", | ||||
|             id="psram_required", | ||||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_esp32s3_specific_errors( | ||||
|     config: ConfigType, | ||||
|     error_match: str, | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
| ) -> None: | ||||
|     """Test ESP32-S3 specific configuration errors""" | ||||
|  | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_IDF, | ||||
|         platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, | ||||
|     ) | ||||
|  | ||||
|     with pytest.raises(cv.Invalid, match=error_match): | ||||
|         run_schema_validation(config) | ||||
|  | ||||
|  | ||||
| def test_framework_specific_errors( | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
| ) -> None: | ||||
|     """Test framework-specific configuration errors""" | ||||
|  | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_ARDUINO, | ||||
|         platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, | ||||
|     ) | ||||
|  | ||||
|     with pytest.raises( | ||||
|         cv.Invalid, | ||||
|         match=r"This feature is only available with frameworks \['esp-idf'\]", | ||||
|     ): | ||||
|         run_schema_validation({"model": "wt32-sc01-plus"}) | ||||
|  | ||||
|  | ||||
| def test_custom_model_with_all_options( | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
| ) -> None: | ||||
|     """Test custom model configuration with all available options.""" | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_IDF, | ||||
|         platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, | ||||
|     ) | ||||
|  | ||||
|     run_schema_validation( | ||||
|         { | ||||
|             "model": "custom", | ||||
|             "pixel_mode": "18bit", | ||||
|             "color_depth": 8, | ||||
|             "id": "display_id", | ||||
|             "byte_order": "little_endian", | ||||
|             "bus_mode": "single", | ||||
|             "color_order": "rgb", | ||||
|             "dc_pin": 11, | ||||
|             "reset_pin": 12, | ||||
|             "enable_pin": 13, | ||||
|             "cs_pin": 14, | ||||
|             "init_sequence": [[0xA0, 0x01]], | ||||
|             "dimensions": { | ||||
|                 "width": 320, | ||||
|                 "height": 240, | ||||
|                 "offset_width": 32, | ||||
|                 "offset_height": 32, | ||||
|             }, | ||||
|             "invert_colors": True, | ||||
|             "transform": {"mirror_x": True, "mirror_y": True, "swap_xy": False}, | ||||
|             "spi_mode": "mode0", | ||||
|             "data_rate": "40MHz", | ||||
|             "use_axis_flips": True, | ||||
|             "draw_rounding": 4, | ||||
|             "spi_16": True, | ||||
|             "buffer_size": 0.25, | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_all_predefined_models( | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
|     set_component_config: Callable[[str, Any], None], | ||||
|     choose_variant_with_pins: Callable[..., None], | ||||
| ) -> None: | ||||
|     """Test all predefined display models validate successfully with appropriate defaults.""" | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_IDF, | ||||
|         platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, | ||||
|     ) | ||||
|  | ||||
|     # Enable PSRAM which is required for some models | ||||
|     set_component_config("psram", True) | ||||
|  | ||||
|     # Test all models, providing default values where necessary | ||||
|     for name, model in MODELS.items(): | ||||
|         config = {"model": name} | ||||
|  | ||||
|         # Get the pins required by this model and find a compatible variant | ||||
|         pins = [ | ||||
|             pin | ||||
|             for pin in [ | ||||
|                 model.get_default(pin, None) | ||||
|                 for pin in ("dc_pin", "reset_pin", "cs_pin") | ||||
|             ] | ||||
|             if pin is not None | ||||
|         ] | ||||
|         choose_variant_with_pins(pins) | ||||
|  | ||||
|         # Add required fields that don't have defaults | ||||
|         if ( | ||||
|             not model.get_default(CONF_DC_PIN) | ||||
|             and model.get_default(CONF_BUS_MODE) != "quad" | ||||
|         ): | ||||
|             config[CONF_DC_PIN] = 14 | ||||
|         if not model.get_default(CONF_NATIVE_HEIGHT): | ||||
|             config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320} | ||||
|         if model.initsequence is None: | ||||
|             config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]] | ||||
|  | ||||
|         run_schema_validation(config) | ||||
|  | ||||
|  | ||||
| def test_native_generation( | ||||
|     generate_main: Callable[[str | Path], str], | ||||
|     component_fixture_path: Callable[[str], Path], | ||||
| ) -> None: | ||||
|     """Test code generation for display.""" | ||||
|  | ||||
|     main_cpp = generate_main(component_fixture_path("native.yaml")) | ||||
|     assert ( | ||||
|         "mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1>()" | ||||
|         in main_cpp | ||||
|     ) | ||||
|     assert "set_init_sequence({240, 1, 8, 242" in main_cpp | ||||
|     assert "show_test_card();" in main_cpp | ||||
|     assert "set_write_only(true);" in main_cpp | ||||
|  | ||||
|  | ||||
| def test_lvgl_generation( | ||||
|     generate_main: Callable[[str | Path], str], | ||||
|     component_fixture_path: Callable[[str], Path], | ||||
| ) -> None: | ||||
|     """Test LVGL generation configuration.""" | ||||
|  | ||||
|     main_cpp = generate_main(component_fixture_path("lvgl.yaml")) | ||||
|     assert ( | ||||
|         "mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0>();" | ||||
|         in main_cpp | ||||
|     ) | ||||
|     assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp | ||||
|     assert "show_test_card();" not in main_cpp | ||||
|     assert "set_auto_clear(false);" in main_cpp | ||||
							
								
								
									
										0
									
								
								tests/component_tests/ota/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/ota/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/component_tests/packages/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/packages/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/component_tests/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/component_tests/text/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/text/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/component_tests/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								tests/component_tests/types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								tests/component_tests/types.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| """Type definitions for component tests.""" | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| from typing import Protocol | ||||
|  | ||||
| from esphome.const import PlatformFramework | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
|  | ||||
| class SetCoreConfigCallable(Protocol): | ||||
|     """Protocol for the set_core_config fixture setter function.""" | ||||
|  | ||||
|     def __call__(  # noqa: E704 | ||||
|         self, | ||||
|         platform_framework: PlatformFramework, | ||||
|         /, | ||||
|         *, | ||||
|         core_data: ConfigType | None = None, | ||||
|         platform_data: ConfigType | None = None, | ||||
|     ) -> None: ... | ||||
							
								
								
									
										0
									
								
								tests/component_tests/web_server/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/web_server/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										14
									
								
								tests/components/gpio/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								tests/components/gpio/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| binary_sensor: | ||||
|   - platform: gpio | ||||
|     pin: 2 | ||||
|     id: gpio_binary_sensor | ||||
|  | ||||
| output: | ||||
|   - platform: gpio | ||||
|     pin: 3 | ||||
|     id: gpio_output | ||||
|  | ||||
| switch: | ||||
|   - platform: gpio | ||||
|     pin: 4 | ||||
|     id: gpio_switch | ||||
							
								
								
									
										14
									
								
								tests/components/gpio/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								tests/components/gpio/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| binary_sensor: | ||||
|   - platform: gpio | ||||
|     pin: 2 | ||||
|     id: gpio_binary_sensor | ||||
|  | ||||
| output: | ||||
|   - platform: gpio | ||||
|     pin: 3 | ||||
|     id: gpio_output | ||||
|  | ||||
| switch: | ||||
|   - platform: gpio | ||||
|     pin: 4 | ||||
|     id: gpio_switch | ||||
							
								
								
									
										7
									
								
								tests/components/logger/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								tests/components/logger/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| esphome: | ||||
|   on_boot: | ||||
|     then: | ||||
|       - logger.log: Hello world | ||||
|  | ||||
| logger: | ||||
|   level: DEBUG | ||||
							
								
								
									
										7
									
								
								tests/components/logger/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								tests/components/logger/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| esphome: | ||||
|   on_boot: | ||||
|     then: | ||||
|       - logger.log: Hello world | ||||
|  | ||||
| logger: | ||||
|   level: DEBUG | ||||
| @@ -1,41 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 47 | ||||
|     data_pins: | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: octal_spi | ||||
|     type: octal | ||||
|     interface: hardware | ||||
|     clk_pin: | ||||
|       number: 0 | ||||
|     data_pins: | ||||
|       - 36 | ||||
|       - 37 | ||||
|       - 38 | ||||
|       - 39 | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 9 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: ESP32-2432S028 | ||||
| @@ -1,41 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 47 | ||||
|     data_pins: | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: octal_spi | ||||
|     type: octal | ||||
|     interface: hardware | ||||
|     clk_pin: | ||||
|       number: 0 | ||||
|     data_pins: | ||||
|       - 36 | ||||
|       - 37 | ||||
|       - 38 | ||||
|       - 39 | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 9 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: JC3248W535 | ||||
| @@ -1,19 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 36 | ||||
|     data_pins: | ||||
|       - number: 40 | ||||
|       - number: 41 | ||||
|       - number: 42 | ||||
|       - number: 43 | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 9 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: JC3636W518 | ||||
							
								
								
									
										18
									
								
								tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| substitutions: | ||||
|   clk_pin: GPIO16 | ||||
|   mosi_pin: GPIO17 | ||||
|  | ||||
| spi: | ||||
|   - id: spi_single | ||||
|     clk_pin: | ||||
|       number: ${clk_pin} | ||||
|     mosi_pin: | ||||
|       number: ${mosi_pin} | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: t-display-s3-pro | ||||
|  | ||||
| lvgl: | ||||
|  | ||||
| psram: | ||||
| @@ -1,9 +0,0 @@ | ||||
| spi: | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 9 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: Pico-ResTouch-LCD-3.5 | ||||
| @@ -1,41 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 47 | ||||
|     data_pins: | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: octal_spi | ||||
|     type: octal | ||||
|     interface: hardware | ||||
|     clk_pin: | ||||
|       number: 0 | ||||
|     data_pins: | ||||
|       - 36 | ||||
|       - 37 | ||||
|       - 38 | ||||
|       - 39 | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 9 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: S3BOX | ||||
| @@ -1,41 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 47 | ||||
|     data_pins: | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: octal_spi | ||||
|     type: octal | ||||
|     interface: hardware | ||||
|     clk_pin: | ||||
|       number: 0 | ||||
|     data_pins: | ||||
|       - 36 | ||||
|       - 37 | ||||
|       - 38 | ||||
|       - 39 | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 9 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: S3BOXLITE | ||||
| @@ -1,9 +0,0 @@ | ||||
| spi: | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 9 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: T-DISPLAY-S3-AMOLED-PLUS | ||||
| @@ -1,15 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 47 | ||||
|     data_pins: | ||||
|       - number: 40 | ||||
|       - number: 41 | ||||
|       - number: 42 | ||||
|       - number: 43 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: T-DISPLAY-S3-AMOLED | ||||
| @@ -1,9 +0,0 @@ | ||||
| spi: | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 40 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: T-DISPLAY-S3-PRO | ||||
| @@ -1,37 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 47 | ||||
|     data_pins: | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: octal_spi | ||||
|     type: octal | ||||
|     interface: hardware | ||||
|     clk_pin: | ||||
|       number: 0 | ||||
|     data_pins: | ||||
|       - 36 | ||||
|       - 37 | ||||
|       - 38 | ||||
|       - 39 | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: T-DISPLAY-S3 | ||||
| @@ -1,41 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 47 | ||||
|     data_pins: | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: octal_spi | ||||
|     type: octal | ||||
|     interface: hardware | ||||
|     clk_pin: | ||||
|       number: 0 | ||||
|     data_pins: | ||||
|       - 36 | ||||
|       - 37 | ||||
|       - 38 | ||||
|       - 39 | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 9 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: T-DISPLAY | ||||
| @@ -1,9 +0,0 @@ | ||||
| spi: | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 40 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: T-EMBED | ||||
| @@ -1,41 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 47 | ||||
|     data_pins: | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: octal_spi | ||||
|     type: octal | ||||
|     interface: hardware | ||||
|     clk_pin: | ||||
|       number: 0 | ||||
|     data_pins: | ||||
|       - 36 | ||||
|       - 37 | ||||
|       - 38 | ||||
|       - 39 | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: spi_id_3 | ||||
|     interface: any | ||||
|     clk_pin: 8 | ||||
|     mosi_pin: 9 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: T4-S3 | ||||
| @@ -1,37 +0,0 @@ | ||||
| spi: | ||||
|   - id: quad_spi | ||||
|     type: quad | ||||
|     interface: spi3 | ||||
|     clk_pin: | ||||
|       number: 47 | ||||
|     data_pins: | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|   - id: octal_spi | ||||
|     type: octal | ||||
|     interface: hardware | ||||
|     clk_pin: | ||||
|       number: 9 | ||||
|     data_pins: | ||||
|       - 36 | ||||
|       - 37 | ||||
|       - 38 | ||||
|       - 39 | ||||
|       - allow_other_uses: true | ||||
|         number: 40 | ||||
|       - allow_other_uses: true | ||||
|         number: 41 | ||||
|       - allow_other_uses: true | ||||
|         number: 42 | ||||
|       - allow_other_uses: true | ||||
|         number: 43 | ||||
|  | ||||
| display: | ||||
|   - platform: mipi_spi | ||||
|     model: WT32-SC01-PLUS | ||||
							
								
								
									
										1
									
								
								tests/components/time/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/time/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| time: | ||||
							
								
								
									
										1
									
								
								tests/components/time/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/time/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| time: | ||||
							
								
								
									
										10
									
								
								tests/components/uptime/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/components/uptime/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| sensor: | ||||
|   - platform: uptime | ||||
|     name: Uptime Sensor | ||||
|   - platform: uptime | ||||
|     name: Uptime Sensor Seconds | ||||
|     type: seconds | ||||
|  | ||||
| text_sensor: | ||||
|   - platform: uptime | ||||
|     name: Uptime Text | ||||
							
								
								
									
										10
									
								
								tests/components/uptime/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/components/uptime/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| sensor: | ||||
|   - platform: uptime | ||||
|     name: Uptime Sensor | ||||
|   - platform: uptime | ||||
|     name: Uptime Sensor Seconds | ||||
|     type: seconds | ||||
|  | ||||
| text_sensor: | ||||
|   - platform: uptime | ||||
|     name: Uptime Text | ||||
| @@ -0,0 +1,16 @@ | ||||
| esphome: | ||||
|   name: componenttestnrf52 | ||||
|   friendly_name: $component_name | ||||
|  | ||||
| nrf52: | ||||
|   board: adafruit_itsybitsy_nrf52840 | ||||
|   bootloader: adafruit_nrf52_sd140_v6 | ||||
|  | ||||
| logger: | ||||
|   level: VERY_VERBOSE | ||||
|  | ||||
| packages: | ||||
|   component_under_test: !include | ||||
|     file: $component_test_file | ||||
|     vars: | ||||
|       component_test_file: $component_test_file | ||||
| @@ -0,0 +1,15 @@ | ||||
| esphome: | ||||
|   name: componenttestnrf52 | ||||
|   friendly_name: $component_name | ||||
|  | ||||
| nrf52: | ||||
|   board: adafruit_feather_nrf52840 | ||||
|  | ||||
| logger: | ||||
|   level: VERY_VERBOSE | ||||
|  | ||||
| packages: | ||||
|   component_under_test: !include | ||||
|     file: $component_test_file | ||||
|     vars: | ||||
|       component_test_file: $component_test_file | ||||
		Reference in New Issue
	
	Block a user