mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +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/nfc/* @jesserockz @kbx81 | ||||||
| esphome/components/noblex/* @AGalfra | esphome/components/noblex/* @AGalfra | ||||||
| esphome/components/npi19/* @bakerkj | esphome/components/npi19/* @bakerkj | ||||||
|  | esphome/components/nrf52/* @tomaszduda23 | ||||||
| esphome/components/number/* @esphome/core | esphome/components/number/* @esphome/core | ||||||
| esphome/components/one_wire/* @ssieb | esphome/components/one_wire/* @ssieb | ||||||
| esphome/components/online_image/* @clydebarrow @guillempages | esphome/components/online_image/* @clydebarrow @guillempages | ||||||
| @@ -536,5 +537,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix | |||||||
| esphome/components/xl9535/* @mreditor97 | esphome/components/xl9535/* @mreditor97 | ||||||
| esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | ||||||
| esphome/components/xxtea/* @clydebarrow | esphome/components/xxtea/* @clydebarrow | ||||||
|  | esphome/components/zephyr/* @tomaszduda23 | ||||||
| esphome/components/zhlt01/* @cfeenstra1024 | esphome/components/zhlt01/* @cfeenstra1024 | ||||||
| esphome/components/zio_ultrasonic/* @kahrendt | esphome/components/zio_ultrasonic/* @kahrendt | ||||||
|   | |||||||
| @@ -51,82 +51,83 @@ SAMPLING_MODES = { | |||||||
|     "max": sampling_mode.MAX, |     "max": sampling_mode.MAX, | ||||||
| } | } | ||||||
|  |  | ||||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True) | ||||||
| adc2_channel_t = cg.global_ns.enum("adc2_channel_t") |  | ||||||
|  | adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True) | ||||||
|  |  | ||||||
| # pin to adc1 channel mapping | # pin to adc1 channel mapping | ||||||
| # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h | # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h | ||||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32: { |     VARIANT_ESP32: { | ||||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, |         36: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, |         37: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, |         38: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, |         39: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, |         32: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, |         33: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, |         34: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, |         35: adc_channel_t.ADC_CHANNEL_7, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C2: { |     VARIANT_ESP32C2: { | ||||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, |         0: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, |         1: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, |         2: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, |         3: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, |         4: adc_channel_t.ADC_CHANNEL_4, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C3: { |     VARIANT_ESP32C3: { | ||||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, |         0: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, |         1: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, |         2: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, |         3: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, |         4: adc_channel_t.ADC_CHANNEL_4, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C6: { |     VARIANT_ESP32C6: { | ||||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, |         0: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, |         1: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, |         2: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, |         3: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, |         4: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         5: adc1_channel_t.ADC1_CHANNEL_5, |         5: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         6: adc1_channel_t.ADC1_CHANNEL_6, |         6: adc_channel_t.ADC_CHANNEL_6, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32H2: { |     VARIANT_ESP32H2: { | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, |         1: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, |         2: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, |         3: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, |         4: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, |         5: adc_channel_t.ADC_CHANNEL_4, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32S2: { |     VARIANT_ESP32S2: { | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, |         1: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, |         2: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, |         3: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, |         4: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, |         5: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, |         6: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, |         7: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, |         8: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, |         9: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, |         10: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32S3: { |     VARIANT_ESP32S3: { | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, |         1: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, |         2: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, |         3: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, |         4: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, |         5: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, |         6: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, |         7: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, |         8: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, |         9: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, |         10: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -135,24 +136,24 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | |||||||
| ESP32_VARIANT_ADC2_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 |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32: { |     VARIANT_ESP32: { | ||||||
|         4: adc2_channel_t.ADC2_CHANNEL_0, |         4: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         0: adc2_channel_t.ADC2_CHANNEL_1, |         0: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         2: adc2_channel_t.ADC2_CHANNEL_2, |         2: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         15: adc2_channel_t.ADC2_CHANNEL_3, |         15: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         13: adc2_channel_t.ADC2_CHANNEL_4, |         13: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         12: adc2_channel_t.ADC2_CHANNEL_5, |         12: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         14: adc2_channel_t.ADC2_CHANNEL_6, |         14: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         27: adc2_channel_t.ADC2_CHANNEL_7, |         27: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         25: adc2_channel_t.ADC2_CHANNEL_8, |         25: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         26: adc2_channel_t.ADC2_CHANNEL_9, |         26: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C2: { |     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 |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C3: { |     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 |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C6: {},  # no ADC2 |     VARIANT_ESP32C6: {},  # no ADC2 | ||||||
| @@ -160,29 +161,29 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | |||||||
|     VARIANT_ESP32H2: {},  # no ADC2 |     VARIANT_ESP32H2: {},  # no ADC2 | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32S2: { |     VARIANT_ESP32S2: { | ||||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, |         11: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, |         12: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, |         13: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, |         14: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, |         15: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, |         16: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, |         17: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, |         18: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, |         19: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, |         20: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32S3: { |     VARIANT_ESP32S3: { | ||||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, |         11: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, |         12: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, |         13: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, |         14: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, |         15: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, |         16: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, |         17: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, |         18: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, |         19: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, |         20: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,11 +3,14 @@ | |||||||
| #include "esphome/components/sensor/sensor.h" | #include "esphome/components/sensor/sensor.h" | ||||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/defines.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| #include <esp_adc_cal.h> | #include "esp_adc/adc_cali.h" | ||||||
| #include "driver/adc.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 | #endif                      // USE_ESP32 | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -49,33 +52,72 @@ class Aggregator { | |||||||
|  |  | ||||||
| class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { | class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { | ||||||
|  public: |  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 | #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_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } | ||||||
|   void set_channel1(adc1_channel_t channel) { |  | ||||||
|     this->channel1_ = channel; |   /// Configure the ADC to use a specific channel on ADC1. | ||||||
|     this->channel2_ = ADC2_CHANNEL_MAX; |   /// 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_channel2(adc2_channel_t channel) { |   void set_channel(adc_unit_t unit, adc_channel_t channel) { | ||||||
|     this->channel2_ = channel; |     this->adc_unit_ = unit; | ||||||
|     this->channel1_ = ADC1_CHANNEL_MAX; |     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; } |   void set_autorange(bool autorange) { this->autorange_ = autorange; } | ||||||
| #endif  // USE_ESP32 | #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 | #ifdef USE_RP2040 | ||||||
|   void set_is_temperature() { this->is_temperature_ = true; } |   void set_is_temperature() { this->is_temperature_ = true; } | ||||||
| #endif  // USE_RP2040 | #endif  // USE_RP2040 | ||||||
| @@ -86,17 +128,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | |||||||
|   InternalGPIOPin *pin_; |   InternalGPIOPin *pin_; | ||||||
|   SamplingMode sampling_mode_{SamplingMode::AVG}; |   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 | #ifdef USE_RP2040 | ||||||
|   bool is_temperature_{false}; |   bool is_temperature_{false}; | ||||||
| #endif  // USE_RP2040 | #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 | }  // namespace adc | ||||||
|   | |||||||
| @@ -8,145 +8,308 @@ namespace adc { | |||||||
|  |  | ||||||
| static const char *const TAG = "adc.esp32"; | 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 | const LogString *attenuation_to_str(adc_atten_t attenuation) { | ||||||
| #if USE_ESP32_VARIANT_ESP32S2 |   switch (attenuation) { | ||||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; |     case ADC_ATTEN_DB_0: | ||||||
| #else |       return LOG_STR("0 dB"); | ||||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; |     case ADC_ATTEN_DB_2_5: | ||||||
| #endif  // USE_ESP32_VARIANT_ESP32S2 |       return LOG_STR("2.5 dB"); | ||||||
| #endif  // SOC_ADC_RTC_MAX_BITWIDTH |     case ADC_ATTEN_DB_6: | ||||||
|  |       return LOG_STR("6 dB"); | ||||||
| static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; |     case ADC_ATTEN_DB_12_COMPAT: | ||||||
| static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; |       return LOG_STR("12 dB"); | ||||||
|  |  | ||||||
| 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: |     default: | ||||||
|         break; |       return LOG_STR("Unknown Attenuation"); | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void ADCSensor::dump_config() { | const LogString *adc_unit_to_str(adc_unit_t unit) { | ||||||
|   static const char *const ATTEN_AUTO_STR = "auto"; |   switch (unit) { | ||||||
|   static const char *const ATTEN_0DB_STR = "0 db"; |     case ADC_UNIT_1: | ||||||
|   static const char *const ATTEN_2_5DB_STR = "2.5 db"; |       return LOG_STR("ADC1"); | ||||||
|   static const char *const ATTEN_6DB_STR = "6 db"; |     case ADC_UNIT_2: | ||||||
|   static const char *const ATTEN_12DB_STR = "12 db"; |       return LOG_STR("ADC2"); | ||||||
|   const char *atten_str = ATTEN_AUTO_STR; |     default: | ||||||
|  |       return LOG_STR("Unknown ADC Unit"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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_SENSOR("", "ADC Sensor", this); | ||||||
|   LOG_PIN("  Pin: ", this->pin_); |   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; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   ESP_LOGCONFIG(TAG, |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "  Channel:       %d\n" | ||||||
|  |                 "  Unit:          %s\n" | ||||||
|                 "  Attenuation:   %s\n" |                 "  Attenuation:   %s\n" | ||||||
|                 "  Samples:       %i\n" |                 "  Samples:       %i\n" | ||||||
|                 "  Sampling mode: %s", |                 "  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); |   LOG_UPDATE_INTERVAL(this); | ||||||
| } | } | ||||||
|  |  | ||||||
| float ADCSensor::sample() { | float ADCSensor::sample() { | ||||||
|   if (!this->autorange_) { |   if (this->autorange_) { | ||||||
|  |     return this->sample_autorange_(); | ||||||
|  |   } else { | ||||||
|  |     return this->sample_fixed_attenuation_(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | float ADCSensor::sample_fixed_attenuation_() { | ||||||
|   auto aggr = Aggregator(this->sampling_mode_); |   auto aggr = Aggregator(this->sampling_mode_); | ||||||
|  |  | ||||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { |   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||||
|       int raw = -1; |     int raw; | ||||||
|       if (this->channel1_ != ADC1_CHANNEL_MAX) { |     esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); | ||||||
|         raw = adc1_get_raw(this->channel1_); |  | ||||||
|       } else if (this->channel2_ != ADC2_CHANNEL_MAX) { |     if (err != ESP_OK) { | ||||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); |       ESP_LOGW(TAG, "ADC read failed with error %d", err); | ||||||
|  |       continue; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (raw == -1) { |     if (raw == -1) { | ||||||
|         return NAN; |       ESP_LOGW(TAG, "Invalid ADC reading"); | ||||||
|  |       continue; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     aggr.add_sample(raw); |     aggr.add_sample(raw); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   uint32_t final_value = aggr.aggregate(); | ||||||
|  |  | ||||||
|   if (this->output_raw_) { |   if (this->output_raw_) { | ||||||
|       return aggr.aggregate(); |     return final_value; | ||||||
|     } |  | ||||||
|     uint32_t mv = |  | ||||||
|         esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); |  | ||||||
|     return mv / 1000.0f; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; |   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; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { |   return final_value * 3.3f / 4095.0f; | ||||||
|     adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); | } | ||||||
|     raw12 = adc1_get_raw(this->channel1_); |  | ||||||
|     if (raw12 < ADC_MAX) { | float ADCSensor::sample_autorange_() { | ||||||
|       adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); |   // Auto-range mode | ||||||
|       raw6 = adc1_get_raw(this->channel1_); |   auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> { | ||||||
|       if (raw6 < ADC_MAX) { |     // First reconfigure the attenuation for this reading | ||||||
|         adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); |     adc_oneshot_chan_cfg_t config = { | ||||||
|         raw2 = adc1_get_raw(this->channel1_); |         .atten = atten, | ||||||
|         if (raw2 < ADC_MAX) { |         .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||||
|           adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); |     }; | ||||||
|           raw0 = adc1_get_raw(this->channel1_); |  | ||||||
|  |     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 | ||||||
|       } |       } | ||||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { |       return {-1, 0.0f}; | ||||||
|     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); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     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; |     return NAN; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); |   const int adc_half = 2048; | ||||||
|   uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); |   uint32_t c12 = std::min(raw12, adc_half); | ||||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); |   uint32_t c6 = adc_half - std::abs(raw6 - adc_half); | ||||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); |   uint32_t c2 = adc_half - std::abs(raw2 - adc_half); | ||||||
|  |   uint32_t c0 = std::min(4095 - raw0, adc_half); | ||||||
|   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); |  | ||||||
|   uint32_t csum = c12 + c6 + c2 + c0; |   uint32_t csum = c12 + c6 + c2 + c0; | ||||||
|  |  | ||||||
|   uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); |   if (csum == 0) { | ||||||
|   return mv_scaled / (float) (csum * 1000U); |     ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); | ||||||
|  |     return NAN; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace adc | }  // namespace adc | ||||||
|   | |||||||
| @@ -10,13 +10,11 @@ from esphome.const import ( | |||||||
|     CONF_NUMBER, |     CONF_NUMBER, | ||||||
|     CONF_PIN, |     CONF_PIN, | ||||||
|     CONF_RAW, |     CONF_RAW, | ||||||
|     CONF_WIFI, |  | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_VOLT, |     UNIT_VOLT, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
| import esphome.final_validate as fv |  | ||||||
|  |  | ||||||
| from . import ( | from . import ( | ||||||
|     ATTENUATION_MODES, |     ATTENUATION_MODES, | ||||||
| @@ -24,6 +22,7 @@ from . import ( | |||||||
|     ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, |     ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, | ||||||
|     SAMPLING_MODES, |     SAMPLING_MODES, | ||||||
|     adc_ns, |     adc_ns, | ||||||
|  |     adc_unit_t, | ||||||
|     validate_adc_pin, |     validate_adc_pin, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -57,21 +56,6 @@ def validate_config(config): | |||||||
|     return 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 = adc_ns.class_( | ||||||
|     "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler |     "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler | ||||||
| ) | ) | ||||||
| @@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|     validate_config, |     validate_config, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| FINAL_VALIDATE_SCHEMA = final_validate_config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     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_sample_count(config[CONF_SAMPLES])) | ||||||
|     cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) |     cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) | ||||||
|  |  | ||||||
|  |     if CORE.is_esp32: | ||||||
|         if attenuation := config.get(CONF_ATTENUATION): |         if attenuation := config.get(CONF_ATTENUATION): | ||||||
|             if attenuation == "auto": |             if attenuation == "auto": | ||||||
|                 cg.add(var.set_autorange(cg.global_ns.true)) |                 cg.add(var.set_autorange(cg.global_ns.true)) | ||||||
|             else: |             else: | ||||||
|                 cg.add(var.set_attenuation(attenuation)) |                 cg.add(var.set_attenuation(attenuation)) | ||||||
|  |  | ||||||
|     if CORE.is_esp32: |  | ||||||
|         variant = get_esp32_variant() |         variant = get_esp32_variant() | ||||||
|         pin_num = config[CONF_PIN][CONF_NUMBER] |         pin_num = config[CONF_PIN][CONF_NUMBER] | ||||||
|         if ( |         if ( | ||||||
| @@ -133,10 +115,10 @@ async def to_code(config): | |||||||
|             and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] |             and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] | ||||||
|         ): |         ): | ||||||
|             chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] |             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 ( |         elif ( | ||||||
|             variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL |             variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL | ||||||
|             and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] |             and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] | ||||||
|         ): |         ): | ||||||
|             chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] |             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 ListEntitiesServicesResponse encode_list_service_response() = 0; | ||||||
|  |  | ||||||
|   virtual bool execute_service(const ExecuteServiceRequest &req) = 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); | template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg); | ||||||
|   | |||||||
| @@ -3,8 +3,6 @@ | |||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/components/as3935/as3935.h" | #include "esphome/components/as3935/as3935.h" | ||||||
| #include "esphome/components/spi/spi.h" | #include "esphome/components/spi/spi.h" | ||||||
| #include "esphome/components/sensor/sensor.h" |  | ||||||
| #include "esphome/components/binary_sensor/binary_sensor.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace as3935_spi { | namespace as3935_spi { | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
|  |  | ||||||
| CONF_BYTE_ORDER = "byte_order" | CONF_BYTE_ORDER = "byte_order" | ||||||
|  | CONF_COLOR_DEPTH = "color_depth" | ||||||
| CONF_DRAW_ROUNDING = "draw_rounding" | CONF_DRAW_ROUNDING = "draw_rounding" | ||||||
| CONF_ON_STATE_CHANGE = "on_state_change" | CONF_ON_STATE_CHANGE = "on_state_change" | ||||||
| CONF_REQUEST_HEADERS = "request_headers" | CONF_REQUEST_HEADERS = "request_headers" | ||||||
|   | |||||||
| @@ -21,6 +21,11 @@ from esphome.components.libretiny.const import ( | |||||||
|     COMPONENT_LN882X, |     COMPONENT_LN882X, | ||||||
|     COMPONENT_RTL87XX, |     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 | from esphome.config_helpers import filter_source_files_from_platform | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
| @@ -41,6 +46,7 @@ from esphome.const import ( | |||||||
|     PLATFORM_ESP32, |     PLATFORM_ESP32, | ||||||
|     PLATFORM_ESP8266, |     PLATFORM_ESP8266, | ||||||
|     PLATFORM_LN882X, |     PLATFORM_LN882X, | ||||||
|  |     PLATFORM_NRF52, | ||||||
|     PLATFORM_RP2040, |     PLATFORM_RP2040, | ||||||
|     PLATFORM_RTL87XX, |     PLATFORM_RTL87XX, | ||||||
|     PlatformFramework, |     PlatformFramework, | ||||||
| @@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] | |||||||
|  |  | ||||||
| UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] | UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] | ||||||
|  |  | ||||||
|  | UART_SELECTION_NRF52 = [USB_CDC, UART0] | ||||||
|  |  | ||||||
| HARDWARE_UART_TO_UART_SELECTION = { | HARDWARE_UART_TO_UART_SELECTION = { | ||||||
|     UART0: logger_ns.UART_SELECTION_UART0, |     UART0: logger_ns.UART_SELECTION_UART0, | ||||||
|     UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, |     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) |             return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) | ||||||
|     if CORE.is_host: |     if CORE.is_host: | ||||||
|         raise cv.Invalid("Uart selection not valid for host platform") |         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 |     raise NotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -186,6 +196,7 @@ LoggerMessageTrigger = logger_ns.class_( | |||||||
|     automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), |     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" | CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
| @@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                 bk72xx=DEFAULT, |                 bk72xx=DEFAULT, | ||||||
|                 ln882x=DEFAULT, |                 ln882x=DEFAULT, | ||||||
|                 rtl87xx=DEFAULT, |                 rtl87xx=DEFAULT, | ||||||
|  |                 nrf52=USB_CDC, | ||||||
|             ): cv.All( |             ): cv.All( | ||||||
|                 cv.only_on( |                 cv.only_on( | ||||||
|                     [ |                     [ | ||||||
| @@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                         PLATFORM_BK72XX, |                         PLATFORM_BK72XX, | ||||||
|                         PLATFORM_LN882X, |                         PLATFORM_LN882X, | ||||||
|                         PLATFORM_RTL87XX, |                         PLATFORM_RTL87XX, | ||||||
|  |                         PLATFORM_NRF52, | ||||||
|                     ] |                     ] | ||||||
|                 ), |                 ), | ||||||
|                 uart_selection, |                 uart_selection, | ||||||
| @@ -358,6 +371,15 @@ async def to_code(config): | |||||||
|     except cv.Invalid: |     except cv.Invalid: | ||||||
|         pass |         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 |     # Register at end for safe mode | ||||||
|     await cg.register_component(log, config) |     await cg.register_component(log, config) | ||||||
|  |  | ||||||
| @@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( | |||||||
|             PlatformFramework.RTL87XX_ARDUINO, |             PlatformFramework.RTL87XX_ARDUINO, | ||||||
|             PlatformFramework.LN882X_ARDUINO, |             PlatformFramework.LN882X_ARDUINO, | ||||||
|         }, |         }, | ||||||
|  |         "logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, | ||||||
|         "task_log_buffer.cpp": { |         "task_log_buffer.cpp": { | ||||||
|             PlatformFramework.ESP32_ARDUINO, |             PlatformFramework.ESP32_ARDUINO, | ||||||
|             PlatformFramework.ESP32_IDF, |             PlatformFramework.ESP32_IDF, | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ | |||||||
| #include <memory>  // For unique_ptr | #include <memory>  // For unique_ptr | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #include "esphome/core/application.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/application.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace logger { | 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 |   this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];  // NOLINT | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||||
|   this->main_task_ = xTaskGetCurrentTaskHandle(); |   this->main_task_ = xTaskGetCurrentTaskHandle(); | ||||||
|  | #elif defined(USE_ZEPHYR) | ||||||
|  |   this->main_task_ = k_current_get(); | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||||
| @@ -172,6 +174,7 @@ void Logger::init_log_buffer(size_t total_buffer_size) { | |||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifndef USE_ZEPHYR | ||||||
| #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) | #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) | ||||||
| void Logger::loop() { | void Logger::loop() { | ||||||
| #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) | #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) | ||||||
| @@ -185,8 +188,13 @@ void Logger::loop() { | |||||||
|     } |     } | ||||||
|     opened = !opened; |     opened = !opened; | ||||||
|   } |   } | ||||||
|  | #endif | ||||||
|  |   this->process_messages_(); | ||||||
|  | } | ||||||
|  | #endif | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | void Logger::process_messages_() { | ||||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||||
|   // Process any buffered messages when available |   // Process any buffered messages when available | ||||||
|   if (this->log_buffer_->has_messages()) { |   if (this->log_buffer_->has_messages()) { | ||||||
| @@ -227,12 +235,11 @@ void Logger::loop() { | |||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
| #endif |  | ||||||
|  |  | ||||||
| void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } | 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; } | 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_; } | UARTSelection Logger::get_uart() const { return this->uart_; } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   | |||||||
| @@ -29,6 +29,11 @@ | |||||||
| #include <driver/uart.h> | #include <driver/uart.h> | ||||||
| #endif  // USE_ESP_IDF | #endif  // USE_ESP_IDF | ||||||
|  |  | ||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  | #include <zephyr/kernel.h> | ||||||
|  | struct device; | ||||||
|  | #endif | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
|  |  | ||||||
| namespace logger { | namespace logger { | ||||||
| @@ -56,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { | |||||||
|     "VV",  // VERY_VERBOSE |     "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 | /** Enum for logging UART selection | ||||||
|  * |  * | ||||||
|  * Advanced configuration (pin selection, etc) is not supported. |  * Advanced configuration (pin selection, etc) is not supported. | ||||||
| @@ -82,7 +87,7 @@ enum UARTSelection : uint8_t { | |||||||
|   UART_SELECTION_UART0_SWAP, |   UART_SELECTION_UART0_SWAP, | ||||||
| #endif  // USE_ESP8266 | #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. |  * @brief Logger component for all ESPHome logging. | ||||||
| @@ -107,7 +112,7 @@ class Logger : public Component { | |||||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||||
|   void init_log_buffer(size_t total_buffer_size); |   void init_log_buffer(size_t total_buffer_size); | ||||||
| #endif | #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; |   void loop() override; | ||||||
| #endif | #endif | ||||||
|   /// Manually set the baud rate for serial, set to 0 to disable. |   /// Manually set the baud rate for serial, set to 0 to disable. | ||||||
| @@ -122,7 +127,7 @@ class Logger : public Component { | |||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|   void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } |   void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } | ||||||
| #endif | #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; } |   void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } | ||||||
|   /// Get the UART used by the logger. |   /// Get the UART used by the logger. | ||||||
|   UARTSelection get_uart() const; |   UARTSelection get_uart() const; | ||||||
| @@ -157,6 +162,7 @@ class Logger : public Component { | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|  |   void process_messages_(); | ||||||
|   void write_msg_(const char *msg); |   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 |   // 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, |   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, |                                                         va_list args, char *buffer, uint16_t *buffer_at, | ||||||
|                                                         uint16_t buffer_size) { |                                                         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); |     this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); | ||||||
| #else | #else | ||||||
|     this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); |     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 | #ifdef USE_ARDUINO | ||||||
|   Stream *hw_serial_{nullptr}; |   Stream *hw_serial_{nullptr}; | ||||||
| #endif | #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 |   void *main_task_ = nullptr;  // Only used for thread name identification | ||||||
| #endif | #endif | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| @@ -256,7 +265,7 @@ class Logger : public Component { | |||||||
|   uint16_t tx_buffer_at_{0}; |   uint16_t tx_buffer_at_{0}; | ||||||
|   uint16_t tx_buffer_size_{0}; |   uint16_t tx_buffer_size_{0}; | ||||||
|   uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; |   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}; |   UARTSelection uart_{UART_SELECTION_UART0}; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_LIBRETINY | #ifdef USE_LIBRETINY | ||||||
| @@ -268,9 +277,13 @@ class Logger : public Component { | |||||||
|   bool global_recursion_guard_{false};  // Simple global recursion guard for single-task platforms |   bool global_recursion_guard_{false};  // Simple global recursion guard for single-task platforms | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||||
|   const char *HOT get_thread_name_() { |   const char *HOT get_thread_name_() { | ||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  |     k_tid_t current_task = k_current_get(); | ||||||
|  | #else | ||||||
|     TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); |     TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); | ||||||
|  | #endif | ||||||
|     if (current_task == main_task_) { |     if (current_task == main_task_) { | ||||||
|       return nullptr;  // Main task |       return nullptr;  // Main task | ||||||
|     } else { |     } else { | ||||||
| @@ -278,6 +291,8 @@ class Logger : public Component { | |||||||
|       return pcTaskGetName(current_task); |       return pcTaskGetName(current_task); | ||||||
| #elif defined(USE_LIBRETINY) | #elif defined(USE_LIBRETINY) | ||||||
|       return pcTaskGetTaskName(current_task); |       return pcTaskGetTaskName(current_task); | ||||||
|  | #elif defined(USE_ZEPHYR) | ||||||
|  |       return k_thread_name_get(current_task); | ||||||
| #endif | #endif | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -319,7 +334,7 @@ class Logger : public Component { | |||||||
|     const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; |     const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; | ||||||
|     const char *letter = esphome::logger::LOG_LEVEL_LETTERS[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) { |     if (thread_name != nullptr) { | ||||||
|       // Non-main task with thread name |       // 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, |       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" | DOMAIN = "mipi_spi" | ||||||
|  |  | ||||||
| CONF_DRAW_FROM_ORIGIN = "draw_from_origin" |  | ||||||
| CONF_SPI_16 = "spi_16" | CONF_SPI_16 = "spi_16" | ||||||
| CONF_PIXEL_MODE = "pixel_mode" | CONF_PIXEL_MODE = "pixel_mode" | ||||||
| CONF_COLOR_DEPTH = "color_depth" |  | ||||||
| CONF_BUS_MODE = "bus_mode" | CONF_BUS_MODE = "bus_mode" | ||||||
| CONF_USE_AXIS_FLIPS = "use_axis_flips" | CONF_USE_AXIS_FLIPS = "use_axis_flips" | ||||||
| CONF_NATIVE_WIDTH = "native_width" | CONF_NATIVE_WIDTH = "native_width" | ||||||
|   | |||||||
| @@ -3,11 +3,18 @@ import logging | |||||||
| from esphome import pins | from esphome import pins | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.components import display, spi | 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 | from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.config_validation import ALLOW_EXTRA | from esphome.config_validation import ALLOW_EXTRA | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_BRIGHTNESS, |     CONF_BRIGHTNESS, | ||||||
|  |     CONF_BUFFER_SIZE, | ||||||
|     CONF_COLOR_ORDER, |     CONF_COLOR_ORDER, | ||||||
|     CONF_CS_PIN, |     CONF_CS_PIN, | ||||||
|     CONF_DATA_RATE, |     CONF_DATA_RATE, | ||||||
| @@ -24,19 +31,19 @@ from esphome.const import ( | |||||||
|     CONF_MODEL, |     CONF_MODEL, | ||||||
|     CONF_OFFSET_HEIGHT, |     CONF_OFFSET_HEIGHT, | ||||||
|     CONF_OFFSET_WIDTH, |     CONF_OFFSET_WIDTH, | ||||||
|  |     CONF_PAGES, | ||||||
|     CONF_RESET_PIN, |     CONF_RESET_PIN, | ||||||
|     CONF_ROTATION, |     CONF_ROTATION, | ||||||
|     CONF_SWAP_XY, |     CONF_SWAP_XY, | ||||||
|     CONF_TRANSFORM, |     CONF_TRANSFORM, | ||||||
|     CONF_WIDTH, |     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 ( | from . import ( | ||||||
|     CONF_BUS_MODE, |     CONF_BUS_MODE, | ||||||
|     CONF_DRAW_FROM_ORIGIN, |  | ||||||
|     CONF_NATIVE_HEIGHT, |     CONF_NATIVE_HEIGHT, | ||||||
|     CONF_NATIVE_WIDTH, |     CONF_NATIVE_WIDTH, | ||||||
|     CONF_PIXEL_MODE, |     CONF_PIXEL_MODE, | ||||||
| @@ -55,6 +62,7 @@ from .models import ( | |||||||
|     MADCTL_XFLIP, |     MADCTL_XFLIP, | ||||||
|     MADCTL_YFLIP, |     MADCTL_YFLIP, | ||||||
|     DriverChip, |     DriverChip, | ||||||
|  |     adafruit, | ||||||
|     amoled, |     amoled, | ||||||
|     cyd, |     cyd, | ||||||
|     ili, |     ili, | ||||||
| @@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"] | |||||||
|  |  | ||||||
| LOGGER = logging.getLogger(DOMAIN) | LOGGER = logging.getLogger(DOMAIN) | ||||||
| mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") | mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") | ||||||
| MipiSpi = mipi_spi_ns.class_( | MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice) | ||||||
|     "MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice | MipiSpiBuffer = mipi_spi_ns.class_( | ||||||
|  |     "MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice | ||||||
| ) | ) | ||||||
| ColorOrder = display.display_ns.enum("ColorMode") | ColorOrder = display.display_ns.enum("ColorMode") | ||||||
| ColorBitness = display.display_ns.enum("ColorBitness") | ColorBitness = display.display_ns.enum("ColorBitness") | ||||||
| Model = mipi_spi_ns.enum("Model") | Model = mipi_spi_ns.enum("Model") | ||||||
|  |  | ||||||
|  | PixelMode = mipi_spi_ns.enum("PixelMode") | ||||||
|  | BusType = mipi_spi_ns.enum("BusType") | ||||||
|  |  | ||||||
| COLOR_ORDERS = { | COLOR_ORDERS = { | ||||||
|     MODE_RGB: ColorOrder.COLOR_ORDER_RGB, |     MODE_RGB: ColorOrder.COLOR_ORDER_RGB, | ||||||
|     MODE_BGR: ColorOrder.COLOR_ORDER_BGR, |     MODE_BGR: ColorOrder.COLOR_ORDER_BGR, | ||||||
| } | } | ||||||
|  |  | ||||||
| COLOR_DEPTHS = { | COLOR_DEPTHS = { | ||||||
|     8: ColorBitness.COLOR_BITNESS_332, |     8: PixelMode.PIXEL_MODE_8, | ||||||
|     16: ColorBitness.COLOR_BITNESS_565, |     16: PixelMode.PIXEL_MODE_16, | ||||||
|  |     18: PixelMode.PIXEL_MODE_18, | ||||||
| } | } | ||||||
|  |  | ||||||
| DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema | 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 | MODELS = DriverChip.models | ||||||
| # These statements are noops, but serve to suppress linting of side-effect-only imports | # This loop is a noop, but suppresses linting of side-effect-only imports | ||||||
| for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): | for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit): | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
| PixelMode = mipi_spi_ns.enum("PixelMode") |  | ||||||
|  |  | ||||||
| PIXEL_MODE_18BIT = "18bit" | DISPLAY_18BIT = "18bit" | ||||||
| PIXEL_MODE_16BIT = "16bit" | DISPLAY_16BIT = "16bit" | ||||||
|  |  | ||||||
| PIXEL_MODES = { | DISPLAY_PIXEL_MODES = { | ||||||
|     PIXEL_MODE_16BIT: 0x55, |     DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16), | ||||||
|     PIXEL_MODE_18BIT: 0x66, |     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 validate_dimension(rounding): | ||||||
|     def validator(value): |     def validator(value): | ||||||
|         value = cv.positive_int(value) |         value = cv.positive_int(value) | ||||||
| @@ -158,25 +235,27 @@ 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( |     transform = cv.Schema( | ||||||
|         { |         { | ||||||
|             cv.Required(CONF_MIRROR_X): cv.boolean, |             cv.Required(CONF_MIRROR_X): cv.boolean, | ||||||
|             cv.Required(CONF_MIRROR_Y): 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 |     # CUSTOM model will need to provide a custom init sequence | ||||||
| @@ -185,14 +264,21 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | |||||||
|         if model.initsequence is None |         if model.initsequence is None | ||||||
|         else cv.Optional(CONF_INIT_SEQUENCE) |         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_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 = ( |     color_depth = ( | ||||||
|         ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") |         ("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 = ( |     schema = ( | ||||||
|         display.FULL_DISPLAY_SCHEMA.extend( |         display.FULL_DISPLAY_SCHEMA.extend( | ||||||
|             spi.spi_device_schema( |             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( |                 model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( | ||||||
|                     COLOR_ORDERS, upper=True |                     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_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), | ||||||
|                 model.option(CONF_DRAW_ROUNDING, 2): power_of_two, |                 model.option(CONF_DRAW_ROUNDING, 2): power_of_two, | ||||||
|                 model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( |                 model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of( | ||||||
|                     cv.one_of(*pixel_modes, lower=True), |                     *pixel_modes, lower=True | ||||||
|                     cv.int_range(0, 255, min_included=True, max_included=True), |  | ||||||
|                 ), |                 ), | ||||||
|                 cv.Optional(CONF_TRANSFORM): transform, |                 cv.Optional(CONF_TRANSFORM): transform, | ||||||
|                 cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( |                 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), |                 cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), | ||||||
|                 iseqconf: cv.ensure_list(map_sequence), |                 iseqconf: cv.ensure_list(map_sequence), | ||||||
|  |                 cv.Optional(CONF_BUFFER_SIZE): cv.All( | ||||||
|  |                     cv.percentage, cv.Range(0.12, 1.0) | ||||||
|  |                 ), | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|         .extend( |         .extend({model.option(x): cv.boolean for x in other_options}) | ||||||
|             { |  | ||||||
|                 model.option(x): cv.boolean |  | ||||||
|                 for x in [ |  | ||||||
|                     CONF_DRAW_FROM_ORIGIN, |  | ||||||
|                     CONF_SPI_16, |  | ||||||
|                     CONF_INVERT_COLORS, |  | ||||||
|                     CONF_USE_AXIS_FLIPS, |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|     ) |     ) | ||||||
|     if brightness := model.get_default(CONF_BRIGHTNESS): |     if brightness := model.get_default(CONF_BRIGHTNESS): | ||||||
|         schema = schema.extend( |         schema = schema.extend( | ||||||
| @@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | |||||||
|     return schema |     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. |     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. |     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) |     rotation = config.get(CONF_ROTATION, 0) | ||||||
|     return rotation and ( |     return rotation and ( | ||||||
|         model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 |         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 |     # First get the model and bus mode | ||||||
|     config = cv.Schema( |     config = cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -288,29 +376,94 @@ def config_schema(config): | |||||||
|         extra=ALLOW_EXTRA, |         extra=ALLOW_EXTRA, | ||||||
|     )(config) |     )(config) | ||||||
|     bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) |     bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) | ||||||
|     swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True |     config = model_schema(config)(config) | ||||||
|     config = model_schema(bus_mode, model, swapsies)(config) |  | ||||||
|     # Check for invalid combinations of MADCTL config |     # Check for invalid combinations of MADCTL config | ||||||
|     if init_sequence := config.get(CONF_INIT_SEQUENCE): |     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( |             raise cv.Invalid( | ||||||
|                 f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" |                 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: |     if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: | ||||||
|         raise cv.Invalid("DC pin is not supported in quad mode") |         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: |     if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: | ||||||
|         raise cv.Invalid(f"DC pin is required in {bus_mode} mode") |         raise cv.Invalid(f"DC pin is required in {bus_mode} mode") | ||||||
|  |     denominator(config) | ||||||
|     return config |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = config_schema | CONFIG_SCHEMA = customise_schema | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_transform(model, config): | def requires_buffer(config): | ||||||
|     can_transform = rotation_as_transform(model, 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( |     transform = config.get( | ||||||
|         CONF_TRANSFORM, |         CONF_TRANSFORM, | ||||||
|         { |         { | ||||||
| @@ -350,16 +503,13 @@ def get_sequence(model, config): | |||||||
|     sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] |     sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] | ||||||
|     commands = [x[0] for x in sequence] |     commands = [x[0] for x in sequence] | ||||||
|     # Set pixel format if not already in the custom sequence |     # Set pixel format if not already in the custom sequence | ||||||
|     if PIXFMT not in commands: |     pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]] | ||||||
|         pixel_mode = config[CONF_PIXEL_MODE] |     sequence.append((PIXFMT, pixel_mode[0])) | ||||||
|         if not isinstance(pixel_mode, int): |  | ||||||
|             pixel_mode = PIXEL_MODES[pixel_mode] |  | ||||||
|         sequence.append((PIXFMT, pixel_mode)) |  | ||||||
|     # Does the chip use the flipping bits for mirroring rather than the reverse order bits? |     # Does the chip use the flipping bits for mirroring rather than the reverse order bits? | ||||||
|     use_flip = config[CONF_USE_AXIS_FLIPS] |     use_flip = config[CONF_USE_AXIS_FLIPS] | ||||||
|     if MADCTL not in commands: |     if MADCTL not in commands: | ||||||
|         madctl = 0 |         madctl = 0 | ||||||
|         transform = get_transform(model, config) |         transform = get_transform(config) | ||||||
|         if transform.get(CONF_TRANSFORM): |         if transform.get(CONF_TRANSFORM): | ||||||
|             LOGGER.info("Using hardware transform to implement rotation") |             LOGGER.info("Using hardware transform to implement rotation") | ||||||
|         if transform.get(CONF_MIRROR_X): |         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): | async def to_code(config): | ||||||
|     model = MODELS[config[CONF_MODEL]] |     model = MODELS[config[CONF_MODEL]] | ||||||
|     transform = get_transform(model, config) |     var_id = config[CONF_ID] | ||||||
|     if CONF_DIMENSIONS in config: |     var_id.type, templateargs = get_instance(config) | ||||||
|         # Explicit dimensions, just use as is |     var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) | ||||||
|         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 |  | ||||||
|     ) |  | ||||||
|     cg.add(var.set_init_sequence(get_sequence(model, config))) |     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: |         if CONF_TRANSFORM in config: | ||||||
|             LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") |             LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") | ||||||
|         else: |         else: | ||||||
|             config[CONF_ROTATION] = 0 |             config[CONF_ROTATION] = 0 | ||||||
|     cg.add(var.set_model(config[CONF_MODEL])) |     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_draw_rounding(config[CONF_DRAW_ROUNDING])) | ||||||
|     cg.add(var.set_spi_16(config[CONF_SPI_16])) |  | ||||||
|     if enable_pin := config.get(CONF_ENABLE_PIN): |     if enable_pin := config.get(CONF_ENABLE_PIN): | ||||||
|         enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] |         enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] | ||||||
|         cg.add(var.set_enable_pins(enable)) |         cg.add(var.set_enable_pins(enable)) | ||||||
| @@ -472,4 +621,5 @@ async def to_code(config): | |||||||
|         cg.add(var.set_writer(lambda_)) |         cg.add(var.set_writer(lambda_)) | ||||||
|     await display.register_display(var, config) |     await display.register_display(var, config) | ||||||
|     await spi.register_spi_device(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)) |     cg.add(var.set_write_only(True)) | ||||||
|   | |||||||
| @@ -2,489 +2,5 @@ | |||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace mipi_spi { | namespace mipi_spi {}  // 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 esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -4,40 +4,39 @@ | |||||||
|  |  | ||||||
| #include "esphome/components/spi/spi.h" | #include "esphome/components/spi/spi.h" | ||||||
| #include "esphome/components/display/display.h" | #include "esphome/components/display/display.h" | ||||||
| #include "esphome/components/display/display_buffer.h" |  | ||||||
| #include "esphome/components/display/display_color_utils.h" | #include "esphome/components/display/display_color_utils.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace mipi_spi { | namespace mipi_spi { | ||||||
|  |  | ||||||
| constexpr static const char *const TAG = "display.mipi_spi"; | constexpr static const char *const TAG = "display.mipi_spi"; | ||||||
| static const uint8_t SW_RESET_CMD = 0x01; | static constexpr uint8_t SW_RESET_CMD = 0x01; | ||||||
| static const uint8_t SLEEP_OUT = 0x11; | static constexpr uint8_t SLEEP_OUT = 0x11; | ||||||
| static const uint8_t NORON = 0x13; | static constexpr uint8_t NORON = 0x13; | ||||||
| static const uint8_t INVERT_OFF = 0x20; | static constexpr uint8_t INVERT_OFF = 0x20; | ||||||
| static const uint8_t INVERT_ON = 0x21; | static constexpr uint8_t INVERT_ON = 0x21; | ||||||
| static const uint8_t ALL_ON = 0x23; | static constexpr uint8_t ALL_ON = 0x23; | ||||||
| static const uint8_t WRAM = 0x24; | static constexpr uint8_t WRAM = 0x24; | ||||||
| static const uint8_t MIPI = 0x26; | static constexpr uint8_t MIPI = 0x26; | ||||||
| static const uint8_t DISPLAY_ON = 0x29; | static constexpr uint8_t DISPLAY_ON = 0x29; | ||||||
| static const uint8_t RASET = 0x2B; | static constexpr uint8_t RASET = 0x2B; | ||||||
| static const uint8_t CASET = 0x2A; | static constexpr uint8_t CASET = 0x2A; | ||||||
| static const uint8_t WDATA = 0x2C; | static constexpr uint8_t WDATA = 0x2C; | ||||||
| static const uint8_t TEON = 0x35; | static constexpr uint8_t TEON = 0x35; | ||||||
| static const uint8_t MADCTL_CMD = 0x36; | static constexpr uint8_t MADCTL_CMD = 0x36; | ||||||
| static const uint8_t PIXFMT = 0x3A; | static constexpr uint8_t PIXFMT = 0x3A; | ||||||
| static const uint8_t BRIGHTNESS = 0x51; | static constexpr uint8_t BRIGHTNESS = 0x51; | ||||||
| static const uint8_t SWIRE1 = 0x5A; | static constexpr uint8_t SWIRE1 = 0x5A; | ||||||
| static const uint8_t SWIRE2 = 0x5B; | static constexpr uint8_t SWIRE2 = 0x5B; | ||||||
| static const uint8_t PAGESEL = 0xFE; | static constexpr uint8_t PAGESEL = 0xFE; | ||||||
|  |  | ||||||
| static const uint8_t MADCTL_MY = 0x80;     // Bit 7 Bottom to top | static constexpr uint8_t MADCTL_MY = 0x80;     // Bit 7 Bottom to top | ||||||
| static const uint8_t MADCTL_MX = 0x40;     // Bit 6 Right to left | static constexpr uint8_t MADCTL_MX = 0x40;     // Bit 6 Right to left | ||||||
| static const uint8_t MADCTL_MV = 0x20;     // Bit 5 Swap axes | static constexpr uint8_t MADCTL_MV = 0x20;     // Bit 5 Swap axes | ||||||
| static const uint8_t MADCTL_RGB = 0x00;    // Bit 3 Red-Green-Blue pixel order | static constexpr 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 constexpr uint8_t MADCTL_BGR = 0x08;    // Bit 3 Blue-Green-Red pixel order | ||||||
| static const uint8_t MADCTL_XFLIP = 0x02;  // Mirror the display horizontally | static constexpr 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_YFLIP = 0x01;  // Mirror the display vertically | ||||||
|  |  | ||||||
| static const uint8_t DELAY_FLAG = 0xFF; | static const uint8_t DELAY_FLAG = 0xFF; | ||||||
| // store a 16 bit value in a buffer, big endian. | // 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; |   buf[1] = value; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Buffer mode, conveniently also the number of bytes in a pixel | ||||||
| enum PixelMode { | enum PixelMode { | ||||||
|   PIXEL_MODE_16, |   PIXEL_MODE_8 = 1, | ||||||
|   PIXEL_MODE_18, |   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, |                 public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, | ||||||
|                                       spi::DATA_RATE_1MHZ> { |                                       spi::DATA_RATE_1MHZ> { | ||||||
|  public: |  public: | ||||||
|   MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) |   MipiSpi() {} | ||||||
|       : width_(width), |   void update() override { this->stop_poller(); } | ||||||
|         height_(height), |   void draw_pixel_at(int x, int y, Color color) override {} | ||||||
|         offset_width_(offset_width), |  | ||||||
|         offset_height_(offset_height), |  | ||||||
|         color_depth_(color_depth) {} |  | ||||||
|   void set_model(const char *model) { this->model_ = model; } |   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_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_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; } |   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->brightness_ = brightness; | ||||||
|     this->reset_params_(); |     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; } |   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_width_internal() override { return WIDTH; } | ||||||
|   int get_height_internal() override { return this->height_; } |   int get_height_internal() override { return HEIGHT; } | ||||||
|   bool can_proceed() override { return this->setup_complete_; } |  | ||||||
|   void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; } |   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_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: |  protected: | ||||||
|   bool check_buffer_() { |   /* METHODS */ | ||||||
|     if (this->is_failed()) |   // convenience functions to write commands with or without data | ||||||
|       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); |  | ||||||
|  |  | ||||||
|   void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } |   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 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}; |   GPIOPin *reset_pin_{nullptr}; | ||||||
|   std::vector<GPIOPin *> enable_pins_{}; |   std::vector<GPIOPin *> enable_pins_{}; | ||||||
|   GPIOPin *dc_pin_{nullptr}; |   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_{}; |   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}; |   unsigned draw_rounding_{2}; | ||||||
|   optional<uint8_t> brightness_{}; |   optional<uint8_t> brightness_{}; | ||||||
|   const char *model_{"Unknown"}; |   const char *model_{"Unknown"}; | ||||||
|   std::vector<uint8_t> init_sequence_{}; |   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 mipi_spi | ||||||
| }  // namespace esphome | }  // 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 = {} | models = {} | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | import esphome.config_validation as cv | ||||||
|  |  | ||||||
| from . import DriverChip | from . import DriverChip | ||||||
| from .ili import ILI9488_A | from .ili import ILI9488_A | ||||||
|  |  | ||||||
| @@ -128,6 +130,7 @@ DriverChip( | |||||||
|  |  | ||||||
| ILI9488_A.extend( | ILI9488_A.extend( | ||||||
|     "PICO-RESTOUCH-LCD-3.5", |     "PICO-RESTOUCH-LCD-3.5", | ||||||
|  |     swap_xy=cv.UNDEFINED, | ||||||
|     spi_16=True, |     spi_16=True, | ||||||
|     pixel_mode="16bit", |     pixel_mode="16bit", | ||||||
|     mirror_x=True, |     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 | import re | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
| from esphome import pins | from esphome import pins | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| @@ -139,6 +140,27 @@ def get_hw_interface_list(): | |||||||
|     return [] |     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 | # Given an SPI name, return the index of it in the available list | ||||||
| def get_spi_index(name): | def get_spi_index(name): | ||||||
|     for i, ilist in enumerate(get_hw_interface_list()): |     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( |             cv.Optional(CONF_FORCE_SW): cv.invalid( | ||||||
|                 "force_sw is deprecated - use interface: software" |                 "force_sw is deprecated - use interface: software" | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( |             cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator( | ||||||
|                 *sum(get_hw_interface_list(), ["software", "hardware", "any"]), |                 ["software", "hardware", "any"] | ||||||
|                 lower=True, |  | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_DATA_PINS): cv.invalid( |             cv.Optional(CONF_DATA_PINS): cv.invalid( | ||||||
|                 "'data_pins' should be used with 'type: quad or octal' only" |                 "'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.ensure_list(pins.internal_gpio_output_pin_number), | ||||||
|                     cv.Length(min=pin_count, max=pin_count), |                     cv.Length(min=pin_count, max=pin_count), | ||||||
|                 ), |                 ), | ||||||
|                 cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( |                 cv.Optional( | ||||||
|                     *sum(get_hw_interface_list(), ["hardware"]), |                     CONF_INTERFACE, default="hardware" | ||||||
|                     lower=True, |                 ): one_of_interface_validator(["hardware"]), | ||||||
|                 ), |  | ||||||
|                 cv.Optional(CONF_MISO_PIN): cv.invalid( |                 cv.Optional(CONF_MISO_PIN): cv.invalid( | ||||||
|                     f"'miso_pin' should not be used with {mode} SPI" |                     f"'miso_pin' should not be used with {mode} SPI" | ||||||
|                 ), |                 ), | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import tzlocal | |||||||
| from esphome import automation | from esphome import automation | ||||||
| from esphome.automation import Condition | from esphome.automation import Condition | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
|  | from esphome.components.zephyr import zephyr_add_prj_conf | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_AT, |     CONF_AT, | ||||||
| @@ -25,7 +26,7 @@ from esphome.const import ( | |||||||
|     CONF_TIMEZONE, |     CONF_TIMEZONE, | ||||||
|     CONF_TRIGGER_ID, |     CONF_TRIGGER_ID, | ||||||
| ) | ) | ||||||
| from esphome.core import coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -341,6 +342,8 @@ async def register_time(time_var, config): | |||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(100.0) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|  |     if CORE.using_zephyr: | ||||||
|  |         zephyr_add_prj_conf("POSIX_CLOCK", True) | ||||||
|     cg.add_define("USE_TIME") |     cg.add_define("USE_TIME") | ||||||
|     cg.add_global(time_ns.using) |     cg.add_global(time_ns.using) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,13 +2,15 @@ | |||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #ifdef USE_HOST | #ifdef USE_HOST | ||||||
| #include <sys/time.h> | #include <sys/time.h> | ||||||
|  | #elif defined(USE_ZEPHYR) | ||||||
|  | #include <zephyr/posix/time.h> | ||||||
| #else | #else | ||||||
| #include "lwip/opt.h" | #include "lwip/opt.h" | ||||||
| #endif | #endif | ||||||
| #ifdef USE_ESP8266 | #ifdef USE_ESP8266 | ||||||
| #include "sys/time.h" | #include "sys/time.h" | ||||||
| #endif | #endif | ||||||
| #ifdef USE_RP2040 | #if defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||||
| #include <sys/time.h> | #include <sys/time.h> | ||||||
| #endif | #endif | ||||||
| #include <cerrno> | #include <cerrno> | ||||||
| @@ -22,11 +24,22 @@ static const char *const TAG = "time"; | |||||||
|  |  | ||||||
| RealTimeClock::RealTimeClock() = default; | RealTimeClock::RealTimeClock() = default; | ||||||
| void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | ||||||
|  |   ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); | ||||||
|   // Update UTC epoch time. |   // 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 { |   struct timeval timev { | ||||||
|     .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, |     .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, | ||||||
|   }; |   }; | ||||||
|   ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); |  | ||||||
|   struct timezone tz = {0, 0}; |   struct timezone tz = {0, 0}; | ||||||
|   int ret = settimeofday(&timev, &tz); |   int ret = settimeofday(&timev, &tz); | ||||||
|   if (ret == EINVAL) { |   if (ret == EINVAL) { | ||||||
| @@ -43,7 +56,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | |||||||
|   if (ret != 0) { |   if (ret != 0) { | ||||||
|     ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); |     ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); | ||||||
|   } |   } | ||||||
|  | #endif | ||||||
|   auto time = this->now(); |   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, |   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); |            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" |     HOST = "host" | ||||||
|     LIBRETINY_OLDSTYLE = "libretiny" |     LIBRETINY_OLDSTYLE = "libretiny" | ||||||
|     LN882X = "ln882x" |     LN882X = "ln882x" | ||||||
|  |     NRF52 = "nrf52" | ||||||
|     RP2040 = "rp2040" |     RP2040 = "rp2040" | ||||||
|     RTL87XX = "rtl87xx" |     RTL87XX = "rtl87xx" | ||||||
|  |  | ||||||
| @@ -31,6 +32,7 @@ class Framework(StrEnum): | |||||||
|     ARDUINO = "arduino" |     ARDUINO = "arduino" | ||||||
|     ESP_IDF = "esp-idf" |     ESP_IDF = "esp-idf" | ||||||
|     NATIVE = "host" |     NATIVE = "host" | ||||||
|  |     ZEPHYR = "zephyr" | ||||||
|  |  | ||||||
|  |  | ||||||
| class PlatformFramework(Enum): | class PlatformFramework(Enum): | ||||||
| @@ -47,6 +49,9 @@ class PlatformFramework(Enum): | |||||||
|     RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) |     RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) | ||||||
|     LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) |     LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) | ||||||
|  |  | ||||||
|  |     # Zephyr framework platforms | ||||||
|  |     NRF52_ZEPHYR = (Platform.NRF52, Framework.ZEPHYR) | ||||||
|  |  | ||||||
|     # Host platform (native) |     # Host platform (native) | ||||||
|     HOST_NATIVE = (Platform.HOST, Framework.NATIVE) |     HOST_NATIVE = (Platform.HOST, Framework.NATIVE) | ||||||
|  |  | ||||||
| @@ -58,6 +63,7 @@ PLATFORM_ESP8266 = Platform.ESP8266 | |||||||
| PLATFORM_HOST = Platform.HOST | PLATFORM_HOST = Platform.HOST | ||||||
| PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE | PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE | ||||||
| PLATFORM_LN882X = Platform.LN882X | PLATFORM_LN882X = Platform.LN882X | ||||||
|  | PLATFORM_NRF52 = Platform.NRF52 | ||||||
| PLATFORM_RP2040 = Platform.RP2040 | PLATFORM_RP2040 = Platform.RP2040 | ||||||
| PLATFORM_RTL87XX = Platform.RTL87XX | PLATFORM_RTL87XX = Platform.RTL87XX | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ from esphome.const import ( | |||||||
|     PLATFORM_ESP8266, |     PLATFORM_ESP8266, | ||||||
|     PLATFORM_HOST, |     PLATFORM_HOST, | ||||||
|     PLATFORM_LN882X, |     PLATFORM_LN882X, | ||||||
|  |     PLATFORM_NRF52, | ||||||
|     PLATFORM_RP2040, |     PLATFORM_RP2040, | ||||||
|     PLATFORM_RTL87XX, |     PLATFORM_RTL87XX, | ||||||
| ) | ) | ||||||
| @@ -670,6 +671,10 @@ class EsphomeCore: | |||||||
|     def is_libretiny(self): |     def is_libretiny(self): | ||||||
|         return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x |         return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def is_nrf52(self): | ||||||
|  |         return self.target_platform == PLATFORM_NRF52 | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def is_host(self): |     def is_host(self): | ||||||
|         return self.target_platform == PLATFORM_HOST |         return self.target_platform == PLATFORM_HOST | ||||||
| @@ -686,6 +691,10 @@ class EsphomeCore: | |||||||
|     def using_esp_idf(self): |     def using_esp_idf(self): | ||||||
|         return self.target_framework == "esp-idf" |         return self.target_framework == "esp-idf" | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def using_zephyr(self): | ||||||
|  |         return self.target_framework == "zephyr" | ||||||
|  |  | ||||||
|     def add_job(self, func, *args, **kwargs) -> None: |     def add_job(self, func, *args, **kwargs) -> None: | ||||||
|         self.event_loop.add_job(func, *args, **kwargs) |         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 |           // Decrement so we'll process the swapped component next | ||||||
|           this->current_loop_index_--; |           this->current_loop_index_--; | ||||||
|           // Update the loop start time to current time so the swapped component |           // 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(); |           this->loop_component_start_time_ = millis(); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -16,373 +16,186 @@ void ComponentIterator::begin(bool include_internal) { | |||||||
|   this->at_ = 0; |   this->at_ = 0; | ||||||
|   this->include_internal_ = include_internal; |   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() { | void ComponentIterator::advance() { | ||||||
|   bool advance_platform = false; |  | ||||||
|   bool success = true; |  | ||||||
|   switch (this->state_) { |   switch (this->state_) { | ||||||
|     case IteratorState::NONE: |     case IteratorState::NONE: | ||||||
|       // not started |       // not started | ||||||
|       return; |       return; | ||||||
|     case IteratorState::BEGIN: |     case IteratorState::BEGIN: | ||||||
|       if (this->on_begin()) { |       if (this->on_begin()) { | ||||||
|         advance_platform = true; |         advance_platform_(); | ||||||
|       } else { |  | ||||||
|         return; |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|  |  | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|     case IteratorState::BINARY_SENSOR: |     case IteratorState::BINARY_SENSOR: | ||||||
|       if (this->at_ >= App.get_binary_sensors().size()) { |       this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_COVER | #ifdef USE_COVER | ||||||
|     case IteratorState::COVER: |     case IteratorState::COVER: | ||||||
|       if (this->at_ >= App.get_covers().size()) { |       this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_FAN | #ifdef USE_FAN | ||||||
|     case IteratorState::FAN: |     case IteratorState::FAN: | ||||||
|       if (this->at_ >= App.get_fans().size()) { |       this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_LIGHT | #ifdef USE_LIGHT | ||||||
|     case IteratorState::LIGHT: |     case IteratorState::LIGHT: | ||||||
|       if (this->at_ >= App.get_lights().size()) { |       this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|     case IteratorState::SENSOR: |     case IteratorState::SENSOR: | ||||||
|       if (this->at_ >= App.get_sensors().size()) { |       this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|     case IteratorState::SWITCH: |     case IteratorState::SWITCH: | ||||||
|       if (this->at_ >= App.get_switches().size()) { |       this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_BUTTON | #ifdef USE_BUTTON | ||||||
|     case IteratorState::BUTTON: |     case IteratorState::BUTTON: | ||||||
|       if (this->at_ >= App.get_buttons().size()) { |       this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|     case IteratorState::TEXT_SENSOR: |     case IteratorState::TEXT_SENSOR: | ||||||
|       if (this->at_ >= App.get_text_sensors().size()) { |       this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
|     case IteratorState ::SERVICE: |     case IteratorState::SERVICE: | ||||||
|       if (this->at_ >= api::global_api_server->get_user_services().size()) { |       this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *service = api::global_api_server->get_user_services()[this->at_]; |  | ||||||
|         success = this->on_service(service); |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
|     case IteratorState::CAMERA: |     case IteratorState::CAMERA: { | ||||||
|       if (camera::Camera::instance() == nullptr) { |       camera::Camera *camera_instance = camera::Camera::instance(); | ||||||
|         advance_platform = true; |       if (camera_instance != nullptr && (!camera_instance->is_internal() || this->include_internal_)) { | ||||||
|       } else { |         this->on_camera(camera_instance); | ||||||
|         if (camera::Camera::instance()->is_internal() && !this->include_internal_) { |  | ||||||
|           advance_platform = success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           advance_platform = success = this->on_camera(camera::Camera::instance()); |  | ||||||
|       } |       } | ||||||
|       } |       advance_platform_(); | ||||||
|       break; |     } break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_CLIMATE | #ifdef USE_CLIMATE | ||||||
|     case IteratorState::CLIMATE: |     case IteratorState::CLIMATE: | ||||||
|       if (this->at_ >= App.get_climates().size()) { |       this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|     case IteratorState::NUMBER: |     case IteratorState::NUMBER: | ||||||
|       if (this->at_ >= App.get_numbers().size()) { |       this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_DATE | #ifdef USE_DATETIME_DATE | ||||||
|     case IteratorState::DATETIME_DATE: |     case IteratorState::DATETIME_DATE: | ||||||
|       if (this->at_ >= App.get_dates().size()) { |       this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_TIME | #ifdef USE_DATETIME_TIME | ||||||
|     case IteratorState::DATETIME_TIME: |     case IteratorState::DATETIME_TIME: | ||||||
|       if (this->at_ >= App.get_times().size()) { |       this->process_platform_item_(App.get_times(), &ComponentIterator::on_time); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_DATETIME | #ifdef USE_DATETIME_DATETIME | ||||||
|     case IteratorState::DATETIME_DATETIME: |     case IteratorState::DATETIME_DATETIME: | ||||||
|       if (this->at_ >= App.get_datetimes().size()) { |       this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_TEXT | #ifdef USE_TEXT | ||||||
|     case IteratorState::TEXT: |     case IteratorState::TEXT: | ||||||
|       if (this->at_ >= App.get_texts().size()) { |       this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|     case IteratorState::SELECT: |     case IteratorState::SELECT: | ||||||
|       if (this->at_ >= App.get_selects().size()) { |       this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_LOCK | #ifdef USE_LOCK | ||||||
|     case IteratorState::LOCK: |     case IteratorState::LOCK: | ||||||
|       if (this->at_ >= App.get_locks().size()) { |       this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_VALVE | #ifdef USE_VALVE | ||||||
|     case IteratorState::VALVE: |     case IteratorState::VALVE: | ||||||
|       if (this->at_ >= App.get_valves().size()) { |       this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_MEDIA_PLAYER | #ifdef USE_MEDIA_PLAYER | ||||||
|     case IteratorState::MEDIA_PLAYER: |     case IteratorState::MEDIA_PLAYER: | ||||||
|       if (this->at_ >= App.get_media_players().size()) { |       this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_ALARM_CONTROL_PANEL | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|     case IteratorState::ALARM_CONTROL_PANEL: |     case IteratorState::ALARM_CONTROL_PANEL: | ||||||
|       if (this->at_ >= App.get_alarm_control_panels().size()) { |       this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_EVENT | #ifdef USE_EVENT | ||||||
|     case IteratorState::EVENT: |     case IteratorState::EVENT: | ||||||
|       if (this->at_ >= App.get_events().size()) { |       this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_UPDATE | #ifdef USE_UPDATE | ||||||
|     case IteratorState::UPDATE: |     case IteratorState::UPDATE: | ||||||
|       if (this->at_ >= App.get_updates().size()) { |       this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|     case IteratorState::MAX: |     case IteratorState::MAX: | ||||||
|       if (this->on_end()) { |       if (this->on_end()) { | ||||||
|         this->state_ = IteratorState::NONE; |         this->state_ = IteratorState::NONE; | ||||||
|       } |       } | ||||||
|       return; |       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_end() { return true; } | ||||||
| bool ComponentIterator::on_begin() { return true; } | bool ComponentIterator::on_begin() { return true; } | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
|   | |||||||
| @@ -171,6 +171,11 @@ class ComponentIterator { | |||||||
|   } state_{IteratorState::NONE}; |   } state_{IteratorState::NONE}; | ||||||
|   uint16_t at_{0};  // Supports up to 65,535 entities per type |   uint16_t at_{0};  // Supports up to 65,535 entities per type | ||||||
|   bool include_internal_{false}; |   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 | }  // namespace esphome | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <array> | ||||||
| #include <cmath> | #include <cmath> | ||||||
| #include <cstdint> | #include <cstdint> | ||||||
| #include <cstring> | #include <cstring> | ||||||
| @@ -678,7 +679,7 @@ class InterruptLock { | |||||||
|   ~InterruptLock(); |   ~InterruptLock(); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| #if defined(USE_ESP8266) || defined(USE_RP2040) | #if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||||
|   uint32_t state_; |   uint32_t state_; | ||||||
| #endif | #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 collections.abc import Callable, Generator | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import sys | import sys | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
| import pytest | 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 | # Add package root to python path | ||||||
| here = Path(__file__).parent | here = Path(__file__).parent | ||||||
| package_root = here.parent.parent | package_root = here.parent.parent | ||||||
| sys.path.insert(0, package_root.as_posix()) | sys.path.insert(0, package_root.as_posix()) | ||||||
|  |  | ||||||
| from esphome.__main__ import generate_cpp_contents  # noqa: E402 | 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 esphome.core import CORE  # noqa: E402 | ||||||
|  |  | ||||||
|  | from .types import SetCoreConfigCallable  # noqa: E402 | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(autouse=True) | @pytest.fixture(autouse=True) | ||||||
| def config_path(request: pytest.FixtureRequest) -> Generator[None]: | 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 |     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 | @pytest.fixture | ||||||
| def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]: | def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]: | ||||||
|     """Return a function to get absolute paths relative to the component's fixtures directory.""" |     """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 | @pytest.fixture | ||||||
| def generate_main() -> Generator[Callable[[str | Path], str]]: | 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: |     def generator(path: str | Path) -> str: | ||||||
|         CORE.config_path = str(path) |         CORE.config_path = str(path) | ||||||
| @@ -69,5 +134,3 @@ def generate_main() -> Generator[Callable[[str | Path], str]]: | |||||||
|         return CORE.cpp_main_section |         return CORE.cpp_main_section | ||||||
|  |  | ||||||
|     yield generator |     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