diff --git a/src/esphome/api/api.proto b/src/esphome/api/api.proto index c8678e54..1b5fae75 100644 --- a/src/esphome/api/api.proto +++ b/src/esphome/api/api.proto @@ -356,3 +356,20 @@ message ExecuteServiceRequest { fixed32 key = 1; repeated ExecuteServiceArgument args = 2; } + +message ListEntitiesCameraResponse { + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; +} + +message CameraImageResponse { + fixed32 key = 1; + bytes data = 2; + bool done = 3; +} +message CameraImageRequest { + bool single = 1; + bool stream = 2; +} diff --git a/src/esphome/api/api_message.h b/src/esphome/api/api_message.h index 8ce496dd..3ede60bc 100644 --- a/src/esphome/api/api_message.h +++ b/src/esphome/api/api_message.h @@ -33,6 +33,7 @@ enum class APIMessageType { LIST_ENTITIES_SWITCH_RESPONSE = 17, LIST_ENTITIES_TEXT_SENSOR_RESPONSE = 18, LIST_ENTITIES_SERVICE_RESPONSE = 41, + LIST_ENTITIES_CAMERA_RESPONSE = 43, LIST_ENTITIES_DONE_RESPONSE = 19, SUBSCRIBE_STATES_REQUEST = 20, @@ -43,6 +44,7 @@ enum class APIMessageType { SENSOR_STATE_RESPONSE = 25, SWITCH_STATE_RESPONSE = 26, TEXT_SENSOR_STATE_RESPONSE = 27, + CAMERA_IMAGE_RESPONSE = 44, SUBSCRIBE_LOGS_REQUEST = 28, SUBSCRIBE_LOGS_RESPONSE = 29, @@ -51,6 +53,7 @@ enum class APIMessageType { FAN_COMMAND_REQUEST = 31, LIGHT_COMMAND_REQUEST = 32, SWITCH_COMMAND_REQUEST = 33, + CAMERA_IMAGE_REQUEST = 45, SUBSCRIBE_SERVICE_CALLS_REQUEST = 34, SERVICE_CALL_RESPONSE = 35, diff --git a/src/esphome/api/api_server.cpp b/src/esphome/api/api_server.cpp index 4ade4e9f..b1e61ab6 100644 --- a/src/esphome/api/api_server.cpp +++ b/src/esphome/api/api_server.cpp @@ -53,6 +53,16 @@ void APIServer::setup() { }); this->last_connected_ = millis(); + +#ifdef USE_ESP32_CAMERA + if (global_esp32_camera != nullptr) { + global_esp32_camera->add_image_callback([this](std::shared_ptr image) { + for (auto *c : this->clients_) + if (!c->remove_) + c->send_camera_state(image); + }); + } +#endif } void APIServer::loop() { // Partition clients into remove and active @@ -385,6 +395,7 @@ void APIConnection::read_message_(uint32_t size, uint32_t type, uint8_t *msg) { case APIMessageType::LIST_ENTITIES_SWITCH_RESPONSE: case APIMessageType::LIST_ENTITIES_TEXT_SENSOR_RESPONSE: case APIMessageType::LIST_ENTITIES_SERVICE_RESPONSE: + case APIMessageType::LIST_ENTITIES_CAMERA_RESPONSE: case APIMessageType::LIST_ENTITIES_DONE_RESPONSE: // Invalid break; @@ -401,6 +412,7 @@ void APIConnection::read_message_(uint32_t size, uint32_t type, uint8_t *msg) { case APIMessageType::SENSOR_STATE_RESPONSE: case APIMessageType::SWITCH_STATE_RESPONSE: case APIMessageType::TEXT_SENSOR_STATE_RESPONSE: + case APIMessageType::CAMERA_IMAGE_RESPONSE: // Invalid break; case APIMessageType::SUBSCRIBE_LOGS_REQUEST: { @@ -484,6 +496,14 @@ void APIConnection::read_message_(uint32_t size, uint32_t type, uint8_t *msg) { this->on_execute_service_(req); break; } + case APIMessageType::CAMERA_IMAGE_REQUEST: { +#ifdef USE_ESP32_CAMERA + CameraImageRequest req; + req.decode(msg, size); + this->on_camera_image_request_(req); +#endif + break; + } } } void APIConnection::on_hello_request_(const HelloRequest &req) { @@ -714,6 +734,31 @@ void APIConnection::loop() { this->sent_ping_ = true; this->send_ping_request(); } + +#ifdef USE_ESP32_CAMERA + if (this->image_reader_.available()) { + uint32_t space = this->client_->space(); + // reserve 15 bytes for metadata, and at least 64 bytes of data + if (space >= 15 + 64) { + uint32_t to_send = std::min(space - 15, this->image_reader_.available()); + auto buffer = this->get_buffer(); + // fixed32 key = 1; + buffer.encode_fixed32(1, global_esp32_camera->get_object_id_hash()); + // bytes data = 2; + buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); + // bool done = 3; + bool done = this->image_reader_.available() == to_send; + buffer.encode_bool(3, done); + bool success = this->send_buffer(APIMessageType::CAMERA_IMAGE_RESPONSE); + if (success) { + this->image_reader_.consume_data(to_send); + } + if (success && done) { + this->image_reader_.return_image(); + } + } + } +#endif } #ifdef USE_BINARY_SENSOR @@ -1014,6 +1059,31 @@ APIBuffer APIConnection::get_buffer() { void APIConnection::send_time_request() { this->send_empty_message(APIMessageType::GET_TIME_REQUEST); } #endif +#ifdef USE_ESP32_CAMERA +void APIConnection::send_camera_state(std::shared_ptr image) { + if (!this->state_subscription_) + return; + if (this->image_reader_.available()) + return; + this->image_reader_.set_image(image); +} +#endif + +#ifdef USE_ESP32_CAMERA +void APIConnection::on_camera_image_request_(const CameraImageRequest &req) { + if (global_esp32_camera == nullptr) + return; + + ESP_LOGV(TAG, "on_camera_image_request_ stream=%s single=%s", YESNO(req.get_stream()), YESNO(req.get_single())); + if (req.get_single()) { + global_esp32_camera->request_image(); + } + if (req.get_stream()) { + global_esp32_camera->request_stream(); + } +} +#endif + } // namespace api ESPHOME_NAMESPACE_END diff --git a/src/esphome/api/api_server.h b/src/esphome/api/api_server.h index 992cde80..29255157 100644 --- a/src/esphome/api/api_server.h +++ b/src/esphome/api/api_server.h @@ -63,6 +63,9 @@ class APIConnection { #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state); +#endif +#ifdef USE_ESP32_CAMERA + void send_camera_state(std::shared_ptr image); #endif bool send_log_message(int level, const char *tag, const char *line); bool send_disconnect_request(const char *reason); @@ -111,6 +114,9 @@ class APIConnection { void on_subscribe_home_assistant_states_request_(const SubscribeHomeAssistantStatesRequest &req); void on_home_assistant_state_response_(const HomeAssistantStateResponse &req); void on_execute_service_(const ExecuteServiceRequest &req); +#ifdef USE_ESP32_CAMERA + void on_camera_image_request_(const CameraImageRequest &req); +#endif enum class ConnectionState { WAITING_FOR_HELLO, @@ -128,6 +134,9 @@ class APIConnection { std::string client_info_; ListEntitiesIterator list_entities_iterator_; InitialStateIterator initial_state_iterator_; +#ifdef USE_ESP32_CAMERA + CameraImageReader image_reader_; +#endif bool state_subscription_{false}; int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; diff --git a/src/esphome/api/command_messages.cpp b/src/esphome/api/command_messages.cpp index fe61ad4b..46f60ea3 100644 --- a/src/esphome/api/command_messages.cpp +++ b/src/esphome/api/command_messages.cpp @@ -283,6 +283,26 @@ uint32_t SwitchCommandRequest::get_key() const { return this->key_; } bool SwitchCommandRequest::get_state() const { return this->state_; } #endif +#ifdef USE_ESP32_CAMERA +bool CameraImageRequest::get_single() const { return this->single_; } +bool CameraImageRequest::get_stream() const { return this->stream_; } +bool CameraImageRequest::decode_varint(uint32_t field_id, uint32_t value) { + switch (field_id) { + case 1: + // bool single = 1; + this->single_ = value; + return true; + case 2: + // bool stream = 2; + this->stream_ = value; + return true; + default: + return false; + } +} +APIMessageType CameraImageRequest::message_type() const { return APIMessageType::CAMERA_IMAGE_REQUEST; } +#endif + } // namespace api ESPHOME_NAMESPACE_END diff --git a/src/esphome/api/command_messages.h b/src/esphome/api/command_messages.h index 1b434d4e..6d5ec386 100644 --- a/src/esphome/api/command_messages.h +++ b/src/esphome/api/command_messages.h @@ -107,6 +107,20 @@ class SwitchCommandRequest : public APIMessage { }; #endif +#ifdef USE_ESP32_CAMERA +class CameraImageRequest : public APIMessage { + public: + bool decode_varint(uint32_t field_id, uint32_t value) override; + bool get_single() const; + bool get_stream() const; + APIMessageType message_type() const override; + + protected: + bool single_{false}; + bool stream_{false}; +}; +#endif + } // namespace api ESPHOME_NAMESPACE_END diff --git a/src/esphome/api/list_entities.cpp b/src/esphome/api/list_entities.cpp index afccad23..4c9093a1 100644 --- a/src/esphome/api/list_entities.cpp +++ b/src/esphome/api/list_entities.cpp @@ -136,6 +136,16 @@ bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SERVICE_RESPONSE); } +#ifdef USE_ESP32_CAMERA +bool ListEntitiesIterator::on_camera(ESP32Camera *camera) { + auto buffer = this->client_->get_buffer(); + buffer.encode_nameable(camera); + // string unique_id = 4; + buffer.encode_string(4, get_default_unique_id("camera", camera)); + return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_CAMERA_RESPONSE); +} +#endif + APIMessageType ListEntitiesRequest::message_type() const { return APIMessageType::LIST_ENTITIES_REQUEST; } } // namespace api diff --git a/src/esphome/api/list_entities.h b/src/esphome/api/list_entities.h index 2a1b38d6..995e83a2 100644 --- a/src/esphome/api/list_entities.h +++ b/src/esphome/api/list_entities.h @@ -44,6 +44,9 @@ class ListEntitiesIterator : public ComponentIterator { bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; #endif bool on_service(UserServiceDescriptor *service) override; +#ifdef USE_ESP32_CAMERA + bool on_camera(ESP32Camera *camera) override; +#endif bool on_end() override; protected: diff --git a/src/esphome/api/util.cpp b/src/esphome/api/util.cpp index cddf04eb..0b30fb9c 100644 --- a/src/esphome/api/util.cpp +++ b/src/esphome/api/util.cpp @@ -34,6 +34,9 @@ void APIBuffer::encode_bool(uint32_t field, bool value) { void APIBuffer::encode_string(uint32_t field, const std::string &value) { this->encode_string(field, value.data(), value.size()); } +void APIBuffer::encode_bytes(uint32_t field, const uint8_t *data, size_t len) { + this->encode_string(field, reinterpret_cast(data), len); +} void APIBuffer::encode_string(uint32_t field, const char *string, size_t len) { if (len == 0) return; @@ -299,6 +302,20 @@ void ComponentIterator::advance() { success = this->on_service(service); } break; +#ifdef USE_ESP32_CAMERA + case IteratorState::CAMERA: + if (global_esp32_camera == nullptr) { + advance_platform = true; + } else { + if (global_esp32_camera->is_internal()) { + advance_platform = success = true; + break; + } else { + advance_platform = success = this->on_camera(global_esp32_camera); + } + } + break; +#endif case IteratorState::MAX: if (this->on_end()) { this->state_ = IteratorState::NONE; @@ -316,6 +333,9 @@ void ComponentIterator::advance() { bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } bool ComponentIterator::on_service(UserServiceDescriptor *service) { return true; } +#ifdef USE_ESP32_CAMERA +bool ComponentIterator::on_camera(ESP32Camera *camera) { return true; } +#endif } // namespace api diff --git a/src/esphome/api/util.h b/src/esphome/api/util.h index 02008f30..066c77c8 100644 --- a/src/esphome/api/util.h +++ b/src/esphome/api/util.h @@ -8,6 +8,7 @@ #include "esphome/helpers.h" #include "esphome/component.h" #include "esphome/controller.h" +#include "esphome/esp32_camera.h" ESPHOME_NAMESPACE_BEGIN @@ -26,6 +27,7 @@ class APIBuffer { void encode_bool(uint32_t field, bool value); void encode_string(uint32_t field, const std::string &value); void encode_string(uint32_t field, const char *string, size_t len); + void encode_bytes(uint32_t field, const uint8_t *data, size_t len); void encode_fixed32(uint32_t field, uint32_t value); void encode_float(uint32_t field, float value); void encode_nameable(Nameable *nameable); @@ -78,6 +80,9 @@ class ComponentIterator { virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif virtual bool on_service(UserServiceDescriptor *service); +#ifdef USE_ESP32_CAMERA + virtual bool on_camera(ESP32Camera *camera); +#endif virtual bool on_end(); protected: @@ -106,6 +111,9 @@ class ComponentIterator { TEXT_SENSOR, #endif SERVICE, +#ifdef USE_ESP32_CAMERA + CAMERA, +#endif MAX, } state_{IteratorState::NONE}; size_t at_{0}; diff --git a/src/esphome/application.h b/src/esphome/application.h index ca999537..e61c9519 100644 --- a/src/esphome/application.h +++ b/src/esphome/application.h @@ -21,6 +21,7 @@ #include "esphome/log_component.h" #include "esphome/ota_component.h" #include "esphome/power_supply_component.h" +#include "esphome/servo.h" #include "esphome/spi_component.h" #include "esphome/status_led.h" #include "esphome/uart_component.h" diff --git a/src/esphome/defines.h b/src/esphome/defines.h index e6efde8b..628fa769 100644 --- a/src/esphome/defines.h +++ b/src/esphome/defines.h @@ -84,6 +84,7 @@ #define USE_TEMPLATE_COVER #ifdef ARDUINO_ARCH_ESP32 #define USE_ESP32_HALL_SENSOR +#define USE_ESP32_CAMERA #endif #define USE_DUTY_CYCLE_SENSOR #define USE_STATUS_LED @@ -138,6 +139,7 @@ #define USE_MQTT #define USE_COPY_OUTPUT #define USE_WIFI_INFO_TEXT_SENSOR +#define USE_SERVO #endif #ifdef USE_REMOTE_RECEIVER diff --git a/src/esphome/display/nextion.cpp b/src/esphome/display/nextion.cpp index 9126cef3..925e4c08 100644 --- a/src/esphome/display/nextion.cpp +++ b/src/esphome/display/nextion.cpp @@ -32,41 +32,17 @@ void Nextion::send_command_no_ack(const char *command) { } bool Nextion::ack_() { - uint8_t bytes[4] = { - 0x00, - }; - if (!this->read_array(bytes, 4)) { - ESP_LOGW(TAG, "Nextion returned no ACK data!"); - return false; - } - - if (bytes[1] != 0xFF || bytes[2] != 0xFF || bytes[3] != 0xFF) { - ESP_LOGW(TAG, "Nextion returned invalid ACK data!"); - return false; - } + if (!this->wait_for_ack_) + return true; - switch (bytes[0]) { - case 0x01: // successful execution of instruction - return true; - case 0x00: // invalid instruction - case 0x02: // component ID invalid - case 0x03: // page ID invalid - case 0x04: // picture ID invalid - case 0x05: // font ID invalid - case 0x11: // baud rate setting invalid - case 0x12: // curve control ID number or channel number is invalid - case 0x1A: // variable name invalid - case 0x1B: // variable operation invalid - case 0x1C: // failed to assign - case 0x1D: // operate EEPROM failed - case 0x1E: // parameter quantity invalid - case 0x1F: // IO operation failed - case 0x20: // undefined escape characters - case 0x23: // too long variable name - default: - ESP_LOGW(TAG, "Nextion returned NACK with code 0x%02X", bytes[0]); + uint32_t start = millis(); + while (!this->read_until_ack_()) { + if (millis() - start > 100) { + ESP_LOGW(TAG, "Waiting for ACK timed out!"); return false; + } } + return true; } void Nextion::set_component_text(const char *component, const char *text) { this->send_command_printf("%s.txt=\"%s\"", component, text); @@ -74,8 +50,8 @@ void Nextion::set_component_text(const char *component, const char *text) { void Nextion::set_component_value(const char *component, int value) { this->send_command_printf("%s.val=%d", component, value); } -void Nextion::set_component_picture(const char *component, const char *picture) { - this->send_command_printf("%s.pic=\"%s\"", component, picture); +void Nextion::display_picture(int picture_id, int x_start, int y_start) { + this->send_command_printf("pic %d %d %d", picture_id, x_start, y_start); } void Nextion::set_component_background_color(const char *component, const char *color) { this->send_command_printf("%s.bco=\"%s\"", component, color); @@ -122,9 +98,6 @@ void Nextion::disable_component_touch(const char *component) { this->send_comman void Nextion::add_waveform_data(int component_id, uint8_t channel_number, uint8_t value) { this->send_command_printf("add %d,%u,%u", component_id, channel_number, value); } -void Nextion::display_picture(int picture_id, int x_start, int y_start) { - this->send_command_printf("pic %d,%d,%d", x_start, y_start, picture_id); -} void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { this->send_command_printf("fill %d,%d,%d,%d,%s", x1, y1, width, height, color); } @@ -140,7 +113,7 @@ void Nextion::circle(int center_x, int center_y, int radius, const char *color) void Nextion::filled_circle(int center_x, int center_y, int radius, const char *color) { this->send_command_printf("cirs %d,%d,%d,%s", center_x, center_y, radius, color); } -void Nextion::loop() { +bool Nextion::read_until_ack_() { while (this->available() >= 4) { // flush preceding filler bytes uint8_t temp; @@ -181,6 +154,53 @@ void Nextion::loop() { bool invalid_data_length = false; switch (event) { + case 0x01: // successful execution of instruction (ACK) + return true; + case 0x00: // invalid instruction + ESP_LOGW(TAG, "Nextion reported invalid instruction!"); + break; + case 0x02: // component ID invalid + ESP_LOGW(TAG, "Nextion reported component ID invalid!"); + break; + case 0x03: // page ID invalid + ESP_LOGW(TAG, "Nextion reported page ID invalid!"); + break; + case 0x04: // picture ID invalid + ESP_LOGW(TAG, "Nextion reported picture ID invalid!"); + break; + case 0x05: // font ID invalid + ESP_LOGW(TAG, "Nextion reported font ID invalid!"); + break; + case 0x11: // baud rate setting invalid + ESP_LOGW(TAG, "Nextion reported baud rate invalid!"); + break; + case 0x12: // curve control ID number or channel number is invalid + ESP_LOGW(TAG, "Nextion reported control/channel ID invalid!"); + break; + case 0x1A: // variable name invalid + ESP_LOGW(TAG, "Nextion reported variable name invalid!"); + break; + case 0x1B: // variable operation invalid + ESP_LOGW(TAG, "Nextion reported variable operation invalid!"); + break; + case 0x1C: // failed to assign + ESP_LOGW(TAG, "Nextion reported failed to assign variable!"); + break; + case 0x1D: // operate EEPROM failed + ESP_LOGW(TAG, "Nextion reported operating EEPROM failed!"); + break; + case 0x1E: // parameter quantity invalid + ESP_LOGW(TAG, "Nextion reported parameter quantity invalid!"); + break; + case 0x1F: // IO operation failed + ESP_LOGW(TAG, "Nextion reported component I/O operation invalid!"); + break; + case 0x20: // undefined escape characters + ESP_LOGW(TAG, "Nextion reported undefined escape characters!"); + break; + case 0x23: // too long variable name + ESP_LOGW(TAG, "Nextion reported too long variable name!"); + break; case 0x65: { // touch event return data if (data_length != 3) { invalid_data_length = true; @@ -226,6 +246,13 @@ void Nextion::loop() { ESP_LOGW(TAG, "Invalid data length from nextion!"); } } + + return false; +} +void Nextion::loop() { + while (this->available() >= 4) { + this->read_until_ack_(); + } } #ifdef USE_TIME void Nextion::set_nextion_rtc_time(time::ESPTime time) { @@ -258,6 +285,7 @@ void Nextion::set_component_text_printf(const char *component, const char *forma if (ret > 0) this->set_component_text(component, buffer); } +void Nextion::set_wait_for_ack(bool wait_for_ack) { this->wait_for_ack_ = wait_for_ack; } void NextionTouchComponent::process(uint8_t page_id, uint8_t component_id, bool on) { if (this->page_id_ == page_id && this->component_id_ == component_id) { diff --git a/src/esphome/display/nextion.h b/src/esphome/display/nextion.h index c9531450..6f912f40 100644 --- a/src/esphome/display/nextion.h +++ b/src/esphome/display/nextion.h @@ -196,7 +196,7 @@ class Nextion : public PollingComponent, public UARTDevice { void set_writer(const nextion_writer_t &writer); /** - * Manually send a raw command to the display and don't wait for an acknolwedgement packet. + * Manually send a raw command to the display and don't wait for an acknowledgement packet. * @param command The command to write, for example "vis b0,0". */ void send_command_no_ack(const char *command); @@ -208,10 +208,15 @@ class Nextion : public PollingComponent, public UARTDevice { */ bool send_command_printf(const char *format, ...) __attribute__((format(printf, 2, 3))); + void set_wait_for_ack(bool wait_for_ack); + protected: bool ack_(); + bool read_until_ack_(); + std::vector touch_; optional writer_; + bool wait_for_ack_{true}; }; class NextionTouchComponent : public binary_sensor::BinarySensor { diff --git a/src/esphome/esp32_camera.cpp b/src/esphome/esp32_camera.cpp new file mode 100644 index 00000000..b156625c --- /dev/null +++ b/src/esphome/esp32_camera.cpp @@ -0,0 +1,307 @@ +#include "esphome/defines.h" + +#ifdef USE_ESP32_CAMERA + +#include "esphome/esp32_camera.h" +#include "esphome/log.h" + +ESPHOME_NAMESPACE_BEGIN + +static const char *TAG = "esp32_camera"; + +void ESP32Camera::setup() { + global_esp32_camera = this; + + this->last_update_ = millis(); + esp_err_t err = esp_camera_init(&this->config_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_camera_init failed: %s", esp_err_to_name(err)); + this->init_error_ = err; + this->mark_failed(); + return; + } + + sensor_t *s = esp_camera_sensor_get(); + s->set_vflip(s, this->vertical_flip_); + s->set_hmirror(s, this->horizontal_mirror_); + s->set_contrast(s, this->contrast_); + s->set_brightness(s, this->brightness_); + s->set_saturation(s, this->saturation_); + s->set_colorbar(s, this->test_pattern_); + this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); + this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); + xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, + "framebuffer_task", // name + 1024, // stack size + nullptr, // task pv params + 0, // priority + nullptr, // handle + 1 // core + ); +} +void ESP32Camera::dump_config() { + auto conf = this->config_; + ESP_LOGCONFIG(TAG, "ESP32 Camera:"); + ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str()); + ESP_LOGCONFIG(TAG, " Board Has PSRAM: %s", YESNO(psramFound())); + ESP_LOGCONFIG(TAG, " Data Pins: D0:%d D1:%d D2:%d D3:%d D4:%d D5:%d D6:%d D7:%d", conf.pin_d0, conf.pin_d1, + conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7); + ESP_LOGCONFIG(TAG, " VSYNC Pin: %d", conf.pin_vsync); + ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href); + ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk); + ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz); + ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sscb_sda, conf.pin_sscb_scl); + ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset); + switch (this->config_.frame_size) { + case FRAMESIZE_QQVGA: + ESP_LOGCONFIG(TAG, " Resolution: 160x120 (QQVGA)"); + break; + case FRAMESIZE_QQVGA2: + ESP_LOGCONFIG(TAG, " Resolution: 128x160 (QQVGA2)"); + break; + case FRAMESIZE_QCIF: + ESP_LOGCONFIG(TAG, " Resolution: 176x155 (QCIF)"); + break; + case FRAMESIZE_HQVGA: + ESP_LOGCONFIG(TAG, " Resolution: 240x176 (HQVGA)"); + break; + case FRAMESIZE_QVGA: + ESP_LOGCONFIG(TAG, " Resolution: 320x240 (QVGA)"); + break; + case FRAMESIZE_CIF: + ESP_LOGCONFIG(TAG, " Resolution: 400x296 (CIF)"); + break; + case FRAMESIZE_VGA: + ESP_LOGCONFIG(TAG, " Resolution: 640x480 (VGA)"); + break; + case FRAMESIZE_SVGA: + ESP_LOGCONFIG(TAG, " Resolution: 800x600 (SVGA)"); + break; + case FRAMESIZE_XGA: + ESP_LOGCONFIG(TAG, " Resolution: 1024x768 (XGA)"); + break; + case FRAMESIZE_SXGA: + ESP_LOGCONFIG(TAG, " Resolution: 1280x1024 (SXGA)"); + break; + case FRAMESIZE_UXGA: + ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)"); + break; + } + + if (this->is_failed()) { + ESP_LOGE(TAG, " Setup Failed: %s", esp_err_to_name(this->init_error_)); + return; + } + + sensor_t *s = esp_camera_sensor_get(); + auto st = s->status; + ESP_LOGCONFIG(TAG, " JPEG Quality: %u", st.quality); + // ESP_LOGCONFIG(TAG, " Framebuffer Count: %u", conf.fb_count); + ESP_LOGCONFIG(TAG, " Contrast: %d", st.contrast); + ESP_LOGCONFIG(TAG, " Brightness: %d", st.brightness); + ESP_LOGCONFIG(TAG, " Saturation: %d", st.saturation); + ESP_LOGCONFIG(TAG, " Vertical Flip: %s", ONOFF(st.vflip)); + ESP_LOGCONFIG(TAG, " Horizontal Mirror: %s", ONOFF(st.hmirror)); + // ESP_LOGCONFIG(TAG, " Special Effect: %u", st.special_effect); + // ESP_LOGCONFIG(TAG, " White Balance Mode: %u", st.wb_mode); + // ESP_LOGCONFIG(TAG, " Auto White Balance: %u", st.awb); + // ESP_LOGCONFIG(TAG, " Auto White Balance Gain: %u", st.awb_gain); + // ESP_LOGCONFIG(TAG, " Auto Exposure Control: %u", st.aec); + // ESP_LOGCONFIG(TAG, " Auto Exposure Control 2: %u", st.aec2); + // ESP_LOGCONFIG(TAG, " Auto Exposure Level: %d", st.ae_level); + // ESP_LOGCONFIG(TAG, " Auto Exposure Value: %u", st.aec_value); + // ESP_LOGCONFIG(TAG, " AGC: %u", st.agc); + // ESP_LOGCONFIG(TAG, " AGC Gain: %u", st.agc_gain); + // ESP_LOGCONFIG(TAG, " Gain Ceiling: %u", st.gainceiling); + // ESP_LOGCONFIG(TAG, " BPC: %u", st.bpc); + // ESP_LOGCONFIG(TAG, " WPC: %u", st.wpc); + // ESP_LOGCONFIG(TAG, " RAW_GMA: %u", st.raw_gma); + // ESP_LOGCONFIG(TAG, " Lens Correction: %u", st.lenc); + // ESP_LOGCONFIG(TAG, " DCW: %u", st.dcw); + ESP_LOGCONFIG(TAG, " Test Pattern: %s", YESNO(st.colorbar)); +} +void ESP32Camera::loop() { + // check if we can return the image + if (this->can_return_image_()) { + // return image + auto *fb = this->current_image_->get_raw_buffer(); + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + this->current_image_.reset(); + } + + // Check if we should fetch a new image + if (!this->has_requested_image_()) + return; + if (this->current_image_.use_count() > 1) { + // image is still in use + return; + } + const uint32_t now = millis(); + if (now - this->last_update_ <= this->max_update_interval_) + return; + + // request new image + camera_fb_t *fb; + if (xQueueReceive(this->framebuffer_get_queue_, &fb, 0L) != pdTRUE) { + // no frame ready + ESP_LOGVV(TAG, "No frame ready"); + return; + } + + if (fb == nullptr) { + ESP_LOGW(TAG, "Got invalid frame from camera!"); + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + return; + } + this->current_image_ = std::make_shared(fb); + + ESP_LOGD(TAG, "Got Image: %p len=%u width=%u height=%u format=%u", fb->buf, fb->len, fb->width, fb->height, + fb->format); + this->new_image_callback_.call(this->current_image_); + this->last_update_ = now; + this->single_requester_ = false; +} +void ESP32Camera::framebuffer_task(void *pv) { + while (true) { + camera_fb_t *framebuffer = esp_camera_fb_get(); + xQueueSend(global_esp32_camera->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); + // return is no-op for config with 1 fb + xQueueReceive(global_esp32_camera->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); + esp_camera_fb_return(framebuffer); + } +} +ESP32Camera::ESP32Camera(const std::string &name) : Nameable(name) { + this->config_.pin_pwdn = -1; + this->config_.pin_reset = -1; + this->config_.pin_xclk = -1; + this->config_.ledc_timer = LEDC_TIMER_0; + this->config_.ledc_channel = LEDC_CHANNEL_0; + this->config_.pixel_format = PIXFORMAT_JPEG; + this->config_.frame_size = FRAMESIZE_VGA; // 640x480 + this->config_.jpeg_quality = 10; + this->config_.fb_count = 1; + + global_esp32_camera = this; +} +void ESP32Camera::set_data_pins(std::array pins) { + this->config_.pin_d0 = pins[0]; + this->config_.pin_d1 = pins[1]; + this->config_.pin_d2 = pins[2]; + this->config_.pin_d3 = pins[3]; + this->config_.pin_d4 = pins[4]; + this->config_.pin_d5 = pins[5]; + this->config_.pin_d6 = pins[6]; + this->config_.pin_d7 = pins[7]; +} +void ESP32Camera::set_vsync_pin(uint8_t pin) { this->config_.pin_vsync = pin; } +void ESP32Camera::set_href_pin(uint8_t pin) { this->config_.pin_href = pin; } +void ESP32Camera::set_pixel_clock_pin(uint8_t pin) { this->config_.pin_pclk = pin; } +void ESP32Camera::set_external_clock(uint8_t pin, uint32_t frequency) { + this->config_.pin_xclk = pin; + this->config_.xclk_freq_hz = frequency; +} +void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { + this->config_.pin_sscb_sda = sda; + this->config_.pin_sscb_scl = scl; +} +void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) { + switch (size) { + case ESP32_CAMERA_SIZE_160X120: + this->config_.frame_size = FRAMESIZE_QQVGA; + break; + case ESP32_CAMERA_SIZE_128X160: + this->config_.frame_size = FRAMESIZE_QQVGA2; + break; + case ESP32_CAMERA_SIZE_176X144: + this->config_.frame_size = FRAMESIZE_QCIF; + break; + case ESP32_CAMERA_SIZE_240X176: + this->config_.frame_size = FRAMESIZE_HQVGA; + break; + case ESP32_CAMERA_SIZE_320X240: + this->config_.frame_size = FRAMESIZE_QVGA; + break; + case ESP32_CAMERA_SIZE_400X296: + this->config_.frame_size = FRAMESIZE_CIF; + break; + case ESP32_CAMERA_SIZE_640X480: + this->config_.frame_size = FRAMESIZE_VGA; + break; + case ESP32_CAMERA_SIZE_800X600: + this->config_.frame_size = FRAMESIZE_SVGA; + break; + case ESP32_CAMERA_SIZE_1024X768: + this->config_.frame_size = FRAMESIZE_XGA; + break; + case ESP32_CAMERA_SIZE_1280X1024: + this->config_.frame_size = FRAMESIZE_SXGA; + break; + case ESP32_CAMERA_SIZE_1600X1200: + this->config_.frame_size = FRAMESIZE_UXGA; + break; + } +} +void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; } +void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; } +void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } +void ESP32Camera::add_image_callback(std::function)> &&f) { + this->new_image_callback_.add(std::move(f)); +} +void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; } +void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } +void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; } +void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; } +void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; } +float ESP32Camera::get_setup_priority() const { return setup_priority::POST_HARDWARE; } +uint32_t ESP32Camera::hash_base() { return 3010542557UL; } +void ESP32Camera::request_image() { this->single_requester_ = true; } +void ESP32Camera::request_stream() { this->last_stream_request_ = millis(); } +bool ESP32Camera::has_requested_image_() const { + if (this->single_requester_) + // single request + return true; + + uint32_t now = millis(); + if (now - this->last_stream_request_ < 5000) + // stream request + return true; + + if (this->idle_update_interval_ != 0 && now - this->last_update_ > this->idle_update_interval_) + // idle update + return true; + + return false; +} +bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; } +void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) { + this->max_update_interval_ = max_update_interval; +} +void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { + this->idle_update_interval_ = idle_update_interval; +} +void ESP32Camera::set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; } + +ESP32Camera *global_esp32_camera; + +void CameraImageReader::set_image(std::shared_ptr image) { + this->image_ = image; + this->offset_ = 0; +} +size_t CameraImageReader::available() const { + if (!this->image_) + return 0; + + return this->image_->get_data_length() - this->offset_; +} +void CameraImageReader::return_image() { this->image_.reset(); } +void CameraImageReader::consume_data(size_t consumed) { this->offset_ += consumed; } +uint8_t *CameraImageReader::peek_data_buffer() { return this->image_->get_data_buffer() + this->offset_; } + +camera_fb_t *CameraImage::get_raw_buffer() { return this->buffer_; } +uint8_t *CameraImage::get_data_buffer() { return this->buffer_->buf; } +size_t CameraImage::get_data_length() { return this->buffer_->len; } +CameraImage::CameraImage(camera_fb_t *buffer) : buffer_(buffer) {} + +ESPHOME_NAMESPACE_END + +#endif // USE_ESP32_CAMERA diff --git a/src/esphome/esp32_camera.h b/src/esphome/esp32_camera.h new file mode 100644 index 00000000..86dbc750 --- /dev/null +++ b/src/esphome/esp32_camera.h @@ -0,0 +1,115 @@ +#ifndef ESPHOME_CORE_ESP32_CAMERA_H +#define ESPHOME_CORE_ESP32_CAMERA_H + +#include "esphome/defines.h" + +#ifdef USE_ESP32_CAMERA + +#include "esphome/component.h" +#include + +ESPHOME_NAMESPACE_BEGIN + +class ESP32Camera; + +class CameraImage { + public: + CameraImage(camera_fb_t *buffer); + camera_fb_t *get_raw_buffer(); + uint8_t *get_data_buffer(); + size_t get_data_length(); + + protected: + camera_fb_t *buffer_; +}; + +class CameraImageReader { + public: + void set_image(std::shared_ptr image); + size_t available() const; + uint8_t *peek_data_buffer(); + void consume_data(size_t consumed); + void return_image(); + + protected: + std::shared_ptr image_; + size_t offset_{0}; +}; + +enum ESP32CameraFrameSize { + ESP32_CAMERA_SIZE_160X120, // QQVGA + ESP32_CAMERA_SIZE_128X160, // QQVGA2 + ESP32_CAMERA_SIZE_176X144, // QCIF + ESP32_CAMERA_SIZE_240X176, // HQVGA + ESP32_CAMERA_SIZE_320X240, // QVGA + ESP32_CAMERA_SIZE_400X296, // CIF + ESP32_CAMERA_SIZE_640X480, // VGA + ESP32_CAMERA_SIZE_800X600, // SVGA + ESP32_CAMERA_SIZE_1024X768, // XGA + ESP32_CAMERA_SIZE_1280X1024, // SXGA + ESP32_CAMERA_SIZE_1600X1200, // UXGA +}; + +class ESP32Camera : public Component, public Nameable { + public: + ESP32Camera(const std::string &name); + void set_data_pins(std::array pins); + void set_vsync_pin(uint8_t pin); + void set_href_pin(uint8_t pin); + void set_pixel_clock_pin(uint8_t pin); + void set_external_clock(uint8_t pin, uint32_t frequency); + void set_i2c_pins(uint8_t sda, uint8_t scl); + void set_frame_size(ESP32CameraFrameSize size); + void set_jpeg_quality(uint8_t quality); + void set_reset_pin(uint8_t pin); + void set_power_down_pin(uint8_t pin); + void set_vertical_flip(bool vertical_flip); + void set_horizontal_mirror(bool horizontal_mirror); + void set_contrast(int contrast); + void set_brightness(int brightness); + void set_saturation(int saturation); + void set_max_update_interval(uint32_t max_update_interval); + void set_idle_update_interval(uint32_t idle_update_interval); + void set_test_pattern(bool test_pattern); + void setup() override; + void loop() override; + void dump_config() override; + void add_image_callback(std::function)> &&f); + float get_setup_priority() const override; + void request_stream(); + void request_image(); + + protected: + uint32_t hash_base() override; + bool has_requested_image_() const; + bool can_return_image_() const; + + static void framebuffer_task(void *pv); + + camera_config_t config_{}; + bool vertical_flip_{true}; + bool horizontal_mirror_{true}; + int contrast_{0}; + int brightness_{0}; + int saturation_{0}; + bool test_pattern_{false}; + + esp_err_t init_error_{ESP_OK}; + std::shared_ptr current_image_; + uint32_t last_stream_request_{0}; + bool single_requester_{false}; + QueueHandle_t framebuffer_get_queue_; + QueueHandle_t framebuffer_return_queue_; + CallbackManager)> new_image_callback_; + uint32_t max_update_interval_{1000}; + uint32_t idle_update_interval_{15000}; + uint32_t last_update_{0}; +}; + +extern ESP32Camera *global_esp32_camera; + +ESPHOME_NAMESPACE_END + +#endif // USE_ESP32_CAMERA + +#endif // ESPHOME_CORE_ESP32_CAMERA_H diff --git a/src/esphome/esphal.cpp b/src/esphome/esphal.cpp index 78e2b0bc..57ba3539 100644 --- a/src/esphome/esphal.cpp +++ b/src/esphome/esphal.cpp @@ -201,7 +201,17 @@ void ICACHE_RAM_ATTR ISRInternalGPIOPin::clear_interrupt() { } GPIOPin *GPIOPin::copy() const { return new GPIOPin(*this); } -void ICACHE_RAM_ATTR HOT GPIOPin::pin_mode(uint8_t mode) { pinMode(this->pin_, mode); } +void ICACHE_RAM_ATTR HOT GPIOPin::pin_mode(uint8_t mode) { +#ifdef ARDUINO_ARCH_ESP8266 + if (this->pin_ == 16 && mode == INPUT_PULLUP) { + // pullups are not available on GPIO16, manually override with + // input mode. + pinMode(16, INPUT); + return; + } +#endif + pinMode(this->pin_, mode); +} GPIOOutputPin::GPIOOutputPin(uint8_t pin, uint8_t mode, bool inverted) : GPIOPin(pin, mode, inverted) {} diff --git a/src/esphome/ota_component.cpp b/src/esphome/ota_component.cpp index d0e35c87..ac930a56 100644 --- a/src/esphome/ota_component.cpp +++ b/src/esphome/ota_component.cpp @@ -311,7 +311,7 @@ void OTAComponent::handle_() { goto error; } #endif - ESP_LOGW(TAG, "Preparing OTA partition failed!"); + ESP_LOGW(TAG, "Preparing OTA partition failed! '%s'", ss.c_str()); error_code = OTA_RESPONSE_ERROR_UPDATE_PREPARE; goto error; } diff --git a/src/esphome/sensor/rotary_encoder.cpp b/src/esphome/sensor/rotary_encoder.cpp index 94b9c2c5..ebd83613 100644 --- a/src/esphome/sensor/rotary_encoder.cpp +++ b/src/esphome/sensor/rotary_encoder.cpp @@ -22,13 +22,13 @@ static const uint16_t STATE_S3 = 0x0C; static const uint16_t STATE_CCW = 0x00; static const uint16_t STATE_CW = 0x10; static const uint16_t STATE_HAS_INCREMENTED = 0x0700; -static const uint16_t STATE_INCREMENT_COUNTER_4_2_1 = 0x0700; -static const uint16_t STATE_INCREMENT_COUNTER_4_2 = 0x0300; -static const uint16_t STATE_INCREMENT_COUNTER_4 = 0x0100; +static const uint16_t STATE_INCREMENT_COUNTER_4 = 0x0700; +static const uint16_t STATE_INCREMENT_COUNTER_2 = 0x0300; +static const uint16_t STATE_INCREMENT_COUNTER_1 = 0x0100; static const uint16_t STATE_HAS_DECREMENTED = 0x7000; -static const uint16_t STATE_DECREMENT_COUNTER_4_2_1 = 0x7000; -static const uint16_t STATE_DECREMENT_COUNTER_4_2 = 0x3000; -static const uint16_t STATE_DECREMENT_COUNTER_4 = 0x1000; +static const uint16_t STATE_DECREMENT_COUNTER_4 = 0x7000; +static const uint16_t STATE_DECREMENT_COUNTER_2 = 0x3000; +static const uint16_t STATE_DECREMENT_COUNTER_1 = 0x1000; // State explanation: 8-bit uint // Bit 0 (0x01) encodes Pin A HIGH/LOW (reset before each read) @@ -38,46 +38,46 @@ static const uint16_t STATE_DECREMENT_COUNTER_4 = 0x1000; static const uint16_t STATE_LOOKUP_TABLE[32] = { // act state S0 in CCW direction - STATE_CCW | STATE_S0, // 0x00: stay here - STATE_CW | STATE_S1 | STATE_INCREMENT_COUNTER_4, // 0x01: goto CW+S1 and increment counter (dir change) - STATE_CCW | STATE_S0, // 0x02: stay here - STATE_CCW | STATE_S3 | STATE_DECREMENT_COUNTER_4_2_1, // 0x03: goto CCW+S3 and decrement counter + STATE_CCW | STATE_S0, // 0x00: stay here + STATE_CW | STATE_S1 | STATE_INCREMENT_COUNTER_1, // 0x01: goto CW+S1 and increment counter (dir change) + STATE_CCW | STATE_S0, // 0x02: stay here + STATE_CCW | STATE_S3 | STATE_DECREMENT_COUNTER_4, // 0x03: goto CCW+S3 and decrement counter // act state S1 in CCW direction - STATE_CCW | STATE_S1, // 0x04: stay here - STATE_CCW | STATE_S1, // 0x05: stay here - STATE_CCW | STATE_S0 | STATE_DECREMENT_COUNTER_4, // 0x06: goto CCW+S0 and decrement counter - STATE_CW | STATE_S2 | STATE_INCREMENT_COUNTER_4_2_1, // 0x07: goto CW+S2 and increment counter (dir change) + STATE_CCW | STATE_S1, // 0x04: stay here + STATE_CCW | STATE_S1, // 0x05: stay here + STATE_CCW | STATE_S0 | STATE_DECREMENT_COUNTER_1, // 0x06: goto CCW+S0 and decrement counter + STATE_CW | STATE_S2 | STATE_INCREMENT_COUNTER_4, // 0x07: goto CW+S2 and increment counter (dir change) // act state S2 in CCW direction - STATE_CCW | STATE_S1 | STATE_DECREMENT_COUNTER_4_2, // 0x08: goto CCW+S1 and decrement counter - STATE_CCW | STATE_S2, // 0x09: stay here - STATE_CW | STATE_S3 | STATE_INCREMENT_COUNTER_4, // 0x0A: goto CW+S3 and increment counter (dir change) - STATE_CCW | STATE_S2, // 0x0B: stay here + STATE_CCW | STATE_S1 | STATE_DECREMENT_COUNTER_2, // 0x08: goto CCW+S1 and decrement counter + STATE_CCW | STATE_S2, // 0x09: stay here + STATE_CW | STATE_S3 | STATE_INCREMENT_COUNTER_1, // 0x0A: goto CW+S3 and increment counter (dir change) + STATE_CCW | STATE_S2, // 0x0B: stay here // act state S3 in CCW direction - STATE_CW | STATE_S0 | STATE_INCREMENT_COUNTER_4_2, // 0x0C: goto CW+S0 and increment counter (dir change) - STATE_CCW | STATE_S2 | STATE_DECREMENT_COUNTER_4, // 0x0D: goto CCW+S2 and decrement counter - STATE_CCW | STATE_S3, // 0x0E: stay here - STATE_CCW | STATE_S3, // 0x0F: stay here + STATE_CW | STATE_S0 | STATE_INCREMENT_COUNTER_2, // 0x0C: goto CW+S0 and increment counter (dir change) + STATE_CCW | STATE_S2 | STATE_DECREMENT_COUNTER_1, // 0x0D: goto CCW+S2 and decrement counter + STATE_CCW | STATE_S3, // 0x0E: stay here + STATE_CCW | STATE_S3, // 0x0F: stay here // act state S0 in CW direction - STATE_CW | STATE_S0, // 0x10: stay here - STATE_CW | STATE_S1 | STATE_INCREMENT_COUNTER_4, // 0x11: goto CW+S1 and increment counter - STATE_CW | STATE_S0, // 0x12: stay here - STATE_CCW | STATE_S3 | STATE_DECREMENT_COUNTER_4_2_1, // 0x13: goto CCW+S3 and decrement counter (dir change) + STATE_CW | STATE_S0, // 0x10: stay here + STATE_CW | STATE_S1 | STATE_INCREMENT_COUNTER_1, // 0x11: goto CW+S1 and increment counter + STATE_CW | STATE_S0, // 0x12: stay here + STATE_CCW | STATE_S3 | STATE_DECREMENT_COUNTER_4, // 0x13: goto CCW+S3 and decrement counter (dir change) // act state S1 in CW direction - STATE_CW | STATE_S1, // 0x14: stay here - STATE_CW | STATE_S1, // 0x15: stay here - STATE_CCW | STATE_S0 | STATE_DECREMENT_COUNTER_4, // 0x16: goto CCW+S0 and decrement counter (dir change) - STATE_CW | STATE_S2 | STATE_INCREMENT_COUNTER_4_2_1, // 0x17: goto CW+S2 and increment counter + STATE_CW | STATE_S1, // 0x14: stay here + STATE_CW | STATE_S1, // 0x15: stay here + STATE_CCW | STATE_S0 | STATE_DECREMENT_COUNTER_1, // 0x16: goto CCW+S0 and decrement counter (dir change) + STATE_CW | STATE_S2 | STATE_INCREMENT_COUNTER_4, // 0x17: goto CW+S2 and increment counter // act state S2 in CW direction - STATE_CCW | STATE_S1 | STATE_DECREMENT_COUNTER_4_2, // 0x18: goto CCW+S1 and decrement counter (dir change) - STATE_CW | STATE_S2, // 0x19: stay here - STATE_CW | STATE_S3 | STATE_INCREMENT_COUNTER_4, // 0x1A: goto CW+S3 and increment counter + STATE_CCW | STATE_S1 | STATE_DECREMENT_COUNTER_2, // 0x18: goto CCW+S1 and decrement counter (dir change) + STATE_CW | STATE_S2, // 0x19: stay here + STATE_CW | STATE_S3 | STATE_INCREMENT_COUNTER_1, // 0x1A: goto CW+S3 and increment counter STATE_CW | STATE_S2, // act state S3 in CW direction - STATE_CW | STATE_S0 | STATE_INCREMENT_COUNTER_4_2, // 0x1C: goto CW+S0 and increment counter - STATE_CCW | STATE_S2 | STATE_DECREMENT_COUNTER_4, // 0x1D: goto CCW+S2 and decrement counter (dir change) - STATE_CW | STATE_S3, // 0x1E: stay here - STATE_CW | STATE_S3 // 0x1F: stay here + STATE_CW | STATE_S0 | STATE_INCREMENT_COUNTER_2, // 0x1C: goto CW+S0 and increment counter + STATE_CCW | STATE_S2 | STATE_DECREMENT_COUNTER_1, // 0x1D: goto CCW+S2 and decrement counter (dir change) + STATE_CW | STATE_S3, // 0x1E: stay here + STATE_CW | STATE_S3 // 0x1F: stay here }; void ICACHE_RAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore *arg) { diff --git a/src/esphome/sensor/rotary_encoder.h b/src/esphome/sensor/rotary_encoder.h index b38b5511..1fda51af 100644 --- a/src/esphome/sensor/rotary_encoder.h +++ b/src/esphome/sensor/rotary_encoder.h @@ -17,9 +17,9 @@ namespace sensor { /// All possible resolutions for the rotary encoder enum RotaryEncoderResolution { ROTARY_ENCODER_1_PULSE_PER_CYCLE = - 0x1100, /// increment counter by 1 with every A-B cycle, slow response but accurate + 0x4400, /// increment counter by 1 with every A-B cycle, slow response but accurate ROTARY_ENCODER_2_PULSES_PER_CYCLE = 0x2200, /// increment counter by 2 with every A-B cycle - ROTARY_ENCODER_4_PULSES_PER_CYCLE = 0x4400, /// increment counter by 4 with every A-B cycle, most inaccurate + ROTARY_ENCODER_4_PULSES_PER_CYCLE = 0x1100, /// increment counter by 4 with every A-B cycle, most inaccurate }; struct RotaryEncoderSensorStore { diff --git a/src/esphome/sensor/total_daily_energy.cpp b/src/esphome/sensor/total_daily_energy.cpp index 31a043dd..1d054c5f 100644 --- a/src/esphome/sensor/total_daily_energy.cpp +++ b/src/esphome/sensor/total_daily_energy.cpp @@ -30,7 +30,7 @@ float TotalDailyEnergy::get_setup_priority() const { return setup_priority::HARD uint32_t TotalDailyEnergy::update_interval() { return this->parent_->update_interval(); } std::string TotalDailyEnergy::unit_of_measurement() { return this->parent_->get_unit_of_measurement() + "h"; } std::string TotalDailyEnergy::icon() { return this->parent_->get_icon(); } -int8_t TotalDailyEnergy::accuracy_decimals() { return this->parent_->get_accuracy_decimals() + 1; } +int8_t TotalDailyEnergy::accuracy_decimals() { return this->parent_->get_accuracy_decimals() + 2; } void TotalDailyEnergy::process_new_state_(float state) { if (isnan(state)) return; diff --git a/src/esphome/servo.cpp b/src/esphome/servo.cpp new file mode 100644 index 00000000..2f43dee0 --- /dev/null +++ b/src/esphome/servo.cpp @@ -0,0 +1,41 @@ +#include "esphome/defines.h" + +#ifdef USE_SERVO + +#include "esphome/servo.h" +#include "esphome/log.h" + +ESPHOME_NAMESPACE_BEGIN + +static const char *TAG = "servo"; + +void Servo::write(float value) { + if (value < -1.0) + value = -1.0; + else if (value > 1.0) + value = 1.0; + + float level; + if (value < 0.0) + level = lerp(this->idle_level_, this->min_level_, -value); + else + level = lerp(this->idle_level_, this->max_level_, value); + + this->output_->set_level(level); +} +void Servo::setup() { this->write(0.0f); } +void Servo::dump_config() { + ESP_LOGCONFIG(TAG, "Servo:"); + ESP_LOGCONFIG(TAG, " Idle Level: %.1f%%", this->idle_level_ * 100.0f); + ESP_LOGCONFIG(TAG, " Min Level: %.1f%%", this->min_level_ * 100.0f); + ESP_LOGCONFIG(TAG, " Max Level: %.1f%%", this->max_level_ * 100.0f); +} +float Servo::get_setup_priority() const { return setup_priority::HARDWARE_LATE; } +void Servo::set_min_level(float min_level) { this->min_level_ = min_level; } +void Servo::set_idle_level(float idle_level) { this->idle_level_ = idle_level; } +void Servo::set_max_level(float max_level) { this->max_level_ = max_level; } +Servo::Servo(output::FloatOutput *output) : output_(output) {} + +ESPHOME_NAMESPACE_END + +#endif // USE_SERVO diff --git a/src/esphome/servo.h b/src/esphome/servo.h new file mode 100644 index 00000000..e369529a --- /dev/null +++ b/src/esphome/servo.h @@ -0,0 +1,52 @@ +#ifndef ESPHOME_CORE_SERVO_H +#define ESPHOME_CORE_SERVO_H + +#include "esphome/defines.h" + +#ifdef USE_SERVO + +#include "esphome/component.h" +#include "esphome/output/float_output.h" + +ESPHOME_NAMESPACE_BEGIN + +class Servo : public Component { + public: + Servo(output::FloatOutput *output); + void write(float value); + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void set_min_level(float min_level); + void set_idle_level(float idle_level); + void set_max_level(float max_level); + + protected: + output::FloatOutput *output_; + float min_level_ = 0.0300f; + float idle_level_ = 0.0750f; + float max_level_ = 0.1200f; +}; + +template class ServoWriteAction : public Action { + public: + ServoWriteAction(Servo *servo); + template void set_value(V val) { this->value_ = val; } + void play(Ts... x) override; + + protected: + Servo *servo_; + TemplatableValue value_; +}; + +template ServoWriteAction::ServoWriteAction(Servo *servo) : servo_(servo) {} +template void ServoWriteAction::play(Ts... x) { + this->servo_->write(this->value_.value(x...)); + this->play_next(x...); +} + +ESPHOME_NAMESPACE_END + +#endif // USE_SERVO + +#endif // ESPHOME_CORE_SERVO_H