diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..7804a627 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI +on: + push: + paths: + - "components/**" + - "ci/**" + workflow_dispatch: + +jobs: + validate: + strategy: + matrix: + esp: ["esp32", "esp8266"] + name: Validate ${{ matrix.esp }} config + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Setup Python + uses: actions/setup-python@master + with: + python-version: "3.x" + - name: Install ESPHome + run: | + python -m pip install --upgrade pip + pip install -U esphome + pip install -U pillow + + - name: Validate ${{ matrix.esp }} default Config + run: esphome config ci/${{ matrix.esp }}.yaml + + - name: Validate ${{ matrix.esp }} manual Config + run: esphome config ci/${{ matrix.esp }}_manual.yaml + build: + strategy: + matrix: + esp: ["esp32", "esp8266"] + name: Build ${{ matrix.esp }} firmware + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Setup Python + uses: actions/setup-python@master + with: + python-version: "3.x" + - name: Install ESPHome + run: | + python -m pip install --upgrade pip + pip install -U esphome + pip install -U pillow + - name: Build ${{ matrix.esp }} default config + run: esphome compile ci/${{ matrix.esp }}.yaml + + - name: Build ${{ matrix.esp }} manual config + run: esphome compile ci/${{ matrix.esp }}_manual.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index a9f3b68d..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Build ESP -on: push -jobs: - main: - name: Main - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Setup Python - uses: actions/setup-python@master - with: - python-version: '3.x' - - name: Install ESPHome - run: | - python -m pip install --upgrade pip - pip install -U esphome - pip install -U pillow - - name: Validate ESP32 Config - run: esphome config peopleCounter32Dev.yaml - - name: Build ESP32 - run: esphome compile peopleCounter32Dev.yaml - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c42b0f0f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Release + +on: + push: + # Sequence of patterns matched against refs/tags + tags: + - "[0-9]+.[0-9]+.[0-9]+" +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Create release + uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: ${{ github.ref }} + prerelease: false + title: ${{ github.ref }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 09f48b4a..99a132cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.5.0 + +- Manual ROI configuration fixed +- Sensor initialization fixed +- Fix setup priorities to ensure proper boot up +- Code formatting +- Cleanup + ## 1.4.1 - Timing budget test by @Lyr3x in #60 diff --git a/README.md b/README.md index 0816541b..e3923139 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # RooDe -[![Build](https://github.com/Lyr3x/Roode/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/Lyr3x/Roode/blob/master/.github/workflows/main.yml) - -People counter working with any smart home system which supports ESPHome and therefore Home Assistant. All necessary entities are created automatically. +[![GitHub release](https://img.shields.io/github/v/tag/Lyr3x/Roode?style=flat-square)](https://GitHub.com/Lyr3x/Roode/releases/) +[![Build](https://img.shields.io/github/workflow/status/Lyr3x/Roode/CI?style=flat-square)](https://github.com/Lyr3x/Roode/blob/master/.github/workflows/ci.yml) +[![Maintenance](https://img.shields.io/maintenance/yes/2023?style=flat-square)](https://GitHub.com/Lyr3x/Roode/graphs/commit-activity) [![Roode community](https://img.shields.io/discord/879407995837087804.svg?label=Discord&logo=Discord&colorB=7289da&style=for-the-badge)](https://discord.gg/hU9SvSXMHs) +People counter working with any smart home system which supports ESPHome/MQTT like Home Assistant. All necessary entities are created automatically. + - [Hardware Recommendation](#hardware-recommendation) - [Wiring](#wiring) - [ESP32](#esp32) @@ -27,6 +29,7 @@ People counter working with any smart home system which supports ESPHome and the - **Pololu** <-- Recommended - GY-53 - Black PCB chinese sensor + - Pimoroni - 1A Power Supply **Do not use an USB port of your computer!** - Encolsure (see .stl files) - will be updated soon! Pins: @@ -71,21 +74,24 @@ Ps=0 (when connected to GND): In the IIC mode, the user can operate the chip by Roode is provided as an external_component which means it is easy to setup in any ESPHome sensor configuration file. Other than base ESPHome configuration the only config that's needed for Roode is + ```yaml external_components: - - source: github://Lyr3x/Roode + - source: github://Lyr3x/Roode@master refresh: always - +vl53l1x: roode: ``` + This uses the recommended default configuration. -However, we offer a lot of flexibility. Here's the full configuration spelled out. +However, we offer a lot of flexibility. Here's the full configuration spelled out. ```yml external_components: - source: github://Lyr3x/Roode refresh: always + ref: master # VL53L1X sensor configuration is separate from Roode people counting algorithm vl53l1x: @@ -124,20 +130,20 @@ roode: # The current default is roi: { height: 16, width: 6 } # We have an experiential automatic mode that can be enabled with - roi: auto + # roi: auto # or only automatic for one dimension - roi: { height: 16, width: auto } + # roi: { height: 16, width: auto } # The detection thresholds for determining whether a measurement should count as a person crossing. # A reading must be greater than the minimum and less than the maximum to count as a crossing. # These can be given as absolute distances or as percentages. - # Percentages are based on the automatically determined idle or resting distance. + # Percentages are based on the automatically determined idle or resting distance. detection_thresholds: - min: 0% # default minimum is any distance + min: 0% # default minimum is any distance max: 85% # default maximum is 85% # an example of absolute units - min: 50mm - max: 234cm + # min: 50mm + # max: 234cm # The people counting algorithm works by splitting the sensor's capability reading area into two zones. # This allows for detecting whether a crossing is an entry or exit based on which zones was crossed first. @@ -145,14 +151,14 @@ roode: # Flip the entry/exit zones. If Roode seems to be counting backwards, set this to true. invert: false - # Entry/Exit zones can set overrides for individual ROI & detection thresholds here. + # Entry/Exit zones can set overrides for individual ROI & detection thresholds here. # If omitted, they use the options configured above. entry: - # Entry zone will automatically configure ROI, regardless of ROI above. + # Entry zone will automatically configure ROI, regardless of ROI above. roi: auto exit: roi: - # Exit zone will have a height of 8 and a width of number set above or default or auto + # Exit zone will have a height of 8 and a width of number set above or default or auto height: 8 # Additionally, zones can manually set their center point. # Usually though, this is left for Roode to automatically determine. @@ -165,17 +171,23 @@ roode: max: 70% ``` +Also feel free to check out running examples for: +- [Wemos D1 mini with ESP32](peopleCounter32.yaml) +- [Wemos D1 mini with ESP8266](peopleCounter8266.yaml) + ### Sensors #### People Counter The most important one is the people counter. + ```yaml number: - platform: roode people_counter: name: People Count ``` + Regardless of how close we can get, people counting will never be perfect. This allows the current people count to be adjusted easily via Home Assistant. diff --git a/ci/common.yaml b/ci/common.yaml new file mode 100644 index 00000000..70ece37f --- /dev/null +++ b/ci/common.yaml @@ -0,0 +1,64 @@ +substitutions: + devicename: ci + friendly_name: $devicename + +external_components: + refresh: always + source: ../components + +esphome: + name: $devicename + +button: + - platform: restart + name: $friendly_name Restart + entity_category: config + - platform: template + name: $friendly_name Recalibrate + on_press: + - lambda: id(roode_platform)->recalibration(); + entity_category: config + +number: + - platform: roode + people_counter: + name: $friendly_name people counter + +binary_sensor: + - platform: roode + presence_sensor: + name: $friendly_name presence + +sensor: + - platform: roode + id: roode_sensors + distance_entry: + name: $friendly_name distance zone 0 + distance_exit: + name: $friendly_name distance zone 1 + max_threshold_entry: + name: $friendly_name max zone 0 + max_threshold_exit: + name: $friendly_name max zone 1 + min_threshold_entry: + name: $friendly_name min zone 0 + min_threshold_exit: + name: $friendly_name min zone 1 + roi_height_entry: + name: $friendly_name ROI height zone 0 + roi_width_entry: + name: $friendly_name ROI width zone 0 + roi_height_exit: + name: $friendly_name ROI height zone 1 + roi_width_exit: + name: $friendly_name ROI width zone 1 + sensor_status: + name: Sensor Status + +text_sensor: + - platform: roode + version: + name: $friendly_name version + - platform: roode + entry_exit_event: + name: $friendly_name last direction diff --git a/ci/esp32.yaml b/ci/esp32.yaml new file mode 100644 index 00000000..bd9ba973 --- /dev/null +++ b/ci/esp32.yaml @@ -0,0 +1,17 @@ +<<: !include common.yaml + +esp32: + board: wemos_d1_mini32 + framework: + type: arduino + +i2c: + sda: 21 + scl: 22 + +# VL53L1X sensor configuration is separate from Roode people counting algorithm +vl53l1x: + calibration: + +roode: + id: roode_platform diff --git a/ci/esp32_manual.yaml b/ci/esp32_manual.yaml new file mode 100644 index 00000000..f475534c --- /dev/null +++ b/ci/esp32_manual.yaml @@ -0,0 +1,21 @@ +<<: !include esp32.yaml + +vl53l1x: + calibration: + ranging: short + +roode: + id: roode_platform + sampling: 1 + roi: { height: 16, width: 6 } + detection_thresholds: + max: 85% + zones: + entry: + roi: { height: 15, width: 6 } + detection_thresholds: + max: 80% + exit: + roi: { height: 14, width: 6 } + detection_thresholds: + max: 75% diff --git a/ci/esp8266.yaml b/ci/esp8266.yaml new file mode 100644 index 00000000..7cd426a5 --- /dev/null +++ b/ci/esp8266.yaml @@ -0,0 +1,16 @@ +<<: !include common.yaml +esphome: + name: $devicename + platform: ESP8266 + board: d1_mini + +i2c: + sda: 4 + scl: 5 + +# VL53L1X sensor configuration is separate from Roode people counting algorithm +vl53l1x: + calibration: + +roode: + id: roode_platform diff --git a/ci/esp8266_manual.yaml b/ci/esp8266_manual.yaml new file mode 100644 index 00000000..623c17c1 --- /dev/null +++ b/ci/esp8266_manual.yaml @@ -0,0 +1,21 @@ +<<: !include esp8266.yaml + +vl53l1x: + calibration: + ranging: short + +roode: + id: roode_platform + sampling: 1 + roi: { height: 16, width: 6 } + detection_thresholds: + max: 85% + zones: + entry: + roi: { height: 15, width: 6 } + detection_thresholds: + max: 80% + exit: + roi: { height: 14, width: 6 } + detection_thresholds: + max: 75% diff --git a/components/persisted_number/persisted_number.cpp b/components/persisted_number/persisted_number.cpp index 976507a5..2dd0c97f 100644 --- a/components/persisted_number/persisted_number.cpp +++ b/components/persisted_number/persisted_number.cpp @@ -5,27 +5,27 @@ namespace esphome { namespace number { auto PersistedNumber::control(float newValue) -> void { - this->publish_state(newValue); - if (this->restore_value_) { - this->pref_.save(&newValue); - } + this->publish_state(newValue); + if (this->restore_value_) { + this->pref_.save(&newValue); + } } auto PersistedNumber::setup() -> void { - float value; - if (!this->restore_value_) { - value = this->traits.get_min_value(); + float value; + if (!this->restore_value_) { + value = this->traits.get_min_value(); + } else { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (this->pref_.load(&value)) { + ESP_LOGI("number", "'%s': Restored state %f", this->get_name().c_str(), value); } else { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); - if (this->pref_.load(&value)) { - ESP_LOGI("number", "'%s': Restored state %f", this->get_name().c_str(), value); - } else { - ESP_LOGI("number", "'%s': No previous state found", this->get_name().c_str()); - value = this->traits.get_min_value(); - } + ESP_LOGI("number", "'%s': No previous state found", this->get_name().c_str()); + value = this->traits.get_min_value(); } - this->publish_state(value); + } + this->publish_state(value); } -} -} +} // namespace number +} // namespace esphome diff --git a/components/persisted_number/persisted_number.h b/components/persisted_number/persisted_number.h index 48a4ebd3..aadbb565 100644 --- a/components/persisted_number/persisted_number.h +++ b/components/persisted_number/persisted_number.h @@ -8,17 +8,17 @@ namespace esphome { namespace number { class PersistedNumber : public number::Number, public Component { -public: - float get_setup_priority() const override { return setup_priority::HARDWARE; } - void set_restore_value(bool restore) { this->restore_value_ = restore; } - void setup() override; + public: + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void set_restore_value(bool restore) { this->restore_value_ = restore; } + void setup() override; -protected: - void control(float value) override; + protected: + void control(float value) override; - bool restore_value_{false}; - ESPPreferenceObject pref_; + bool restore_value_{false}; + ESPPreferenceObject pref_; }; -} // namespace roode -} // namespace esphome +} // namespace number +} // namespace esphome diff --git a/components/roode/__init__.py b/components/roode/__init__.py index bcc738d5..692eed22 100644 --- a/components/roode/__init__.py +++ b/components/roode/__init__.py @@ -82,7 +82,7 @@ } ), } -) +).extend(cv.COMPONENT_SCHEMA) async def to_code(config: Dict): diff --git a/components/roode/roode.cpp b/components/roode/roode.cpp index f2547d98..ca0448c6 100644 --- a/components/roode/roode.cpp +++ b/components/roode/roode.cpp @@ -4,8 +4,12 @@ namespace esphome { namespace roode { void Roode::dump_config() { ESP_LOGCONFIG(TAG, "Roode:"); + ESP_LOGCONFIG(TAG, " Sample size: %d", samples); LOG_UPDATE_INTERVAL(this); + entry->dump_config(); + exit->dump_config(); } + void Roode::setup() { ESP_LOGI(SETUP, "Booting Roode %s", VERSION); if (version_sensor != nullptr) { @@ -13,6 +17,12 @@ void Roode::setup() { } ESP_LOGI(SETUP, "Using sampling with sampling size: %d", samples); + if (this->distanceSensor->is_failed()) { + this->mark_failed(); + ESP_LOGE(TAG, "Roode cannot be setup without a valid VL53L1X sensor"); + return; + } + calibrate_zones(); } @@ -27,7 +37,7 @@ void Roode::update() { void Roode::loop() { // unsigned long start = micros(); - get_alternating_zone_distances(); + this->current_zone->readDistance(distanceSensor); // uint16_t samplingDistance = sampling(this->current_zone); path_tracking(this->current_zone); handle_sensor_status(); @@ -40,7 +50,6 @@ void Roode::loop() { } bool Roode::handle_sensor_status() { - ESP_LOGV(TAG, "Sensor status: %d, Last sensor status: %d", sensor_status, last_sensor_status); bool check_status = false; if (last_sensor_status != sensor_status && sensor_status == VL53L1_ERROR_NONE) { if (status_sensor != nullptr) { @@ -59,12 +68,6 @@ bool Roode::handle_sensor_status() { return check_status; } -VL53L1_Error Roode::get_alternating_zone_distances() { - this->current_zone->readDistance(distanceSensor); - App.feed_wdt(); - return sensor_status; -} - void Roode::path_tracking(Zone *zone) { static int PathTrack[] = {0, 0, 0, 0}; static int PathTrackFillingSize = 1; // init this to 1 as we start from state @@ -231,6 +234,9 @@ void Roode::calibrateDistance() { entry->calibrateThreshold(distanceSensor, number_attempts); exit->calibrateThreshold(distanceSensor, number_attempts); + if (distanceSensor->get_ranging_mode_override().has_value()) { + return; + } auto *mode = determine_raning_mode(entry->threshold->idle, exit->threshold->idle); if (mode != initial) { distanceSensor->set_ranging_mode(mode); diff --git a/components/roode/roode.h b/components/roode/roode.h index b4a969f5..95e9bcd3 100644 --- a/components/roode/roode.h +++ b/components/roode/roode.h @@ -18,7 +18,7 @@ namespace esphome { namespace roode { #define NOBODY 0 #define SOMEONE 1 -#define VERSION "v1.4.1-beta" +#define VERSION "1.5.0" static const char *const TAG = "Roode"; static const char *const SETUP = "Setup"; static const char *const CALIBRATION = "Sensor Calibration"; @@ -58,6 +58,8 @@ class Roode : public PollingComponent { void update() override; void loop() override; void dump_config() override; + /** Roode uses data from sensors */ + float get_setup_priority() const override { return setup_priority::PROCESSOR; }; TofSensor *get_tof_sensor() { return this->distanceSensor; } void set_tof_sensor(TofSensor *sensor) { this->distanceSensor = sensor; } @@ -118,7 +120,6 @@ class Roode : public PollingComponent { text_sensor::TextSensor *version_sensor; text_sensor::TextSensor *entry_exit_event_sensor; - VL53L1_Error get_alternating_zone_distances(); VL53L1_Error last_sensor_status = VL53L1_ERROR_NONE; VL53L1_Error sensor_status = VL53L1_ERROR_NONE; void path_tracking(Zone *zone); diff --git a/components/roode/zone.cpp b/components/roode/zone.cpp index b246ae99..12524097 100644 --- a/components/roode/zone.cpp +++ b/components/roode/zone.cpp @@ -2,6 +2,15 @@ namespace esphome { namespace roode { + +void Zone::dump_config() const { + ESP_LOGCONFIG(TAG, " %s", id == 0U ? "Entry" : "Exit"); + ESP_LOGCONFIG(TAG, " ROI: { width: %d, height: %d, center: %d }", roi->width, roi->height, roi->center); + ESP_LOGCONFIG(TAG, " Threshold: { min: %dmm (%d%%), max: %dmm (%d%%), idle: %dmm }", threshold->min, + threshold->min_percentage.value_or((threshold->min * 100) / threshold->idle), threshold->max, + threshold->max_percentage.value_or((threshold->max * 100) / threshold->idle), threshold->idle); +} + VL53L1_Error Zone::readDistance(TofSensor *distanceSensor) { last_sensor_status = sensor_status; @@ -28,6 +37,8 @@ void Zone::reset_roi(uint8_t default_center) { roi->width = roi_override->width ?: 6; roi->height = roi_override->height ?: 16; roi->center = roi_override->center ?: default_center; + ESP_LOGD(TAG, "%s ROI reset: { width: %d, height: %d, center: %d }", id == 0U ? "Entry" : "Exit", roi->width, + roi->height, roi->center); } void Zone::calibrateThreshold(TofSensor *distanceSensor, int number_attempts) { @@ -65,7 +76,7 @@ void Zone::roi_calibration(uint16_t entry_threshold, uint16_t exit_threshold, Or } else { // now we set the position of the center of the two zones if (orientation == Parallel) { - switch (ROI_size) { + switch (this->roi->width) { case 4: this->roi->center = this->id == 0U ? 150 : 247; break; @@ -79,7 +90,7 @@ void Zone::roi_calibration(uint16_t entry_threshold, uint16_t exit_threshold, Or break; } } else { - switch (ROI_size) { + switch (this->roi->width) { case 4: this->roi->center = this->id == 0U ? 193 : 58; break; diff --git a/components/roode/zone.h b/components/roode/zone.h index 0f9d6439..bcb20bb1 100644 --- a/components/roode/zone.h +++ b/components/roode/zone.h @@ -30,6 +30,7 @@ struct Threshold { class Zone { public: explicit Zone(uint8_t id) : id{id} {}; + void dump_config() const; VL53L1_Error readDistance(TofSensor *distanceSensor); void reset_roi(uint8_t default_center); void calibrateThreshold(TofSensor *distanceSensor, int number_attempts); diff --git a/components/vl53l1x/__init__.py b/components/vl53l1x/__init__.py index 9a8b25b9..fad429ad 100644 --- a/components/vl53l1x/__init__.py +++ b/components/vl53l1x/__init__.py @@ -12,6 +12,7 @@ CONF_INTERRUPT, CONF_OFFSET, CONF_PINS, + CONF_TIMEOUT, ) import esphome.pins as pins @@ -72,28 +73,38 @@ def none_to_empty(value): return cv.Any(cv.Schema(*args, **kwargs), none_to_empty) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(VL53L1X), - cv.Optional(CONF_PINS, default={}): NullableSchema( - { - cv.Optional(CONF_XSHUT): pins.gpio_output_pin_schema, - cv.Optional(CONF_INTERRUPT): pins.internal_gpio_input_pin_schema, - } - ), - cv.Optional(CONF_CALIBRATION, default={}): NullableSchema( - { - cv.Optional(CONF_RANGING_MODE, default=CONF_AUTO): cv.enum( - RANGING_MODES - ), - cv.Optional(CONF_XTALK): cv.All( - int_with_unit("corrected photon count as cps (counts per second)", "(cps)"), cv.uint16_t - ), - cv.Optional(CONF_OFFSET): cv.All(distance_as_mm, int16_t), - } - ), - } -).extend(i2c.i2c_device_schema(0x29)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(VL53L1X), + cv.Optional( + CONF_TIMEOUT, default="2s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_PINS, default={}): NullableSchema( + { + cv.Optional(CONF_XSHUT): pins.gpio_output_pin_schema, + cv.Optional(CONF_INTERRUPT): pins.internal_gpio_input_pin_schema, + } + ), + cv.Optional(CONF_CALIBRATION, default={}): NullableSchema( + { + cv.Optional(CONF_RANGING_MODE, default=CONF_AUTO): cv.enum( + RANGING_MODES + ), + cv.Optional(CONF_XTALK): cv.All( + int_with_unit( + "corrected photon count as cps (counts per second)", "(cps)" + ), + cv.uint16_t, + ), + cv.Optional(CONF_OFFSET): cv.All(distance_as_mm, int16_t), + } + ), + } + ) + .extend(i2c.i2c_device_schema(0x29)) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config: Dict): @@ -118,6 +129,7 @@ async def to_code(config: Dict): frequency / 1000, ) + cg.add(vl53l1x.set_timeout(config[CONF_TIMEOUT])) await setup_hardware(vl53l1x, config) await setup_calibration(vl53l1x, config[CONF_CALIBRATION]) diff --git a/components/vl53l1x/roi.h b/components/vl53l1x/roi.h index c5c6ad4f..6ead8032 100644 --- a/components/vl53l1x/roi.h +++ b/components/vl53l1x/roi.h @@ -10,6 +10,9 @@ struct ROI { void set_width(uint8_t val) { this->width = val; } void set_height(uint8_t val) { this->height = val; } void set_center(uint8_t val) { this->center = val; } + + bool operator==(const ROI &rhs) const { return width == rhs.width && height == rhs.height && center == rhs.center; } + bool operator!=(const ROI &rhs) const { return !(rhs == *this); } }; } // namespace vl53l1x diff --git a/components/vl53l1x/vl53l1x.cpp b/components/vl53l1x/vl53l1x.cpp index f1afa940..e53c280d 100644 --- a/components/vl53l1x/vl53l1x.cpp +++ b/components/vl53l1x/vl53l1x.cpp @@ -20,31 +20,31 @@ void VL53L1X::dump_config() { } void VL53L1X::setup() { + ESP_LOGD(TAG, "Beginning setup"); + // TODO use xshut_pin, if given, to change address - auto status = this->sensor.Begin(this->address_); + auto status = this->init(); if (status != VL53L1_ERROR_NONE) { - // If the sensor could not be initialized print out the error code. -7 is timeout - ESP_LOGE(TAG, "Could not initialize the sensor, error code: %d", status); this->mark_failed(); return; } - this->address_ = sensor.GetI2CAddress(); + ESP_LOGD(TAG, "Device initialized"); if (this->offset.has_value()) { - ESP_LOGI(TAG, "Setting sensor offset calibration to %d", this->offset.value()); + ESP_LOGI(TAG, "Setting offset calibration to %d", this->offset.value()); status = this->sensor.SetOffsetInMm(this->offset.value()); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE(TAG, "Could not set sensor offset calibration, error code: %d", status); + ESP_LOGE(TAG, "Could not set offset calibration, error code: %d", status); this->mark_failed(); return; } } if (this->xtalk.has_value()) { - ESP_LOGI(TAG, "Setting sensor xtalk calibration to %d", this->xtalk.value()); + ESP_LOGI(TAG, "Setting crosstalk calibration to %d", this->xtalk.value()); status = this->sensor.SetXTalk(this->xtalk.value()); if (status != VL53L1_ERROR_NONE) { - ESP_LOGE(TAG, "Could not set sensor offset calibration, error code: %d", status); + ESP_LOGE(TAG, "Could not set crosstalk calibration, error code: %d", status); this->mark_failed(); return; } @@ -53,6 +53,78 @@ void VL53L1X::setup() { ESP_LOGI(TAG, "Setup complete"); } +VL53L1_Error VL53L1X::init() { + ESP_LOGD(TAG, "Trying to initialize"); + + VL53L1_Error status; + + // If address is non-default, set and try again. + if (address_ != (sensor.GetI2CAddress() >> 1)) { + ESP_LOGD(TAG, "Setting different address"); + status = sensor.SetI2CAddress(address_ << 1); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to change address. Error: %d", status); + return status; + } + } + + status = wait_for_boot(); + if (status != VL53L1_ERROR_NONE) { + return status; + } + + ESP_LOGD(TAG, "Found device, initializing..."); + status = sensor.Init(); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Could not initialize device, error code: %d", status); + return status; + } + + return status; +} + +VL53L1_Error VL53L1X::wait_for_boot() { + // Wait for firmware to copy NVM device_state into registers + delayMicroseconds(1200); + + uint8_t device_state; + VL53L1_Error status; + auto start = millis(); + while ((millis() - start) < this->timeout) { + status = get_device_state(&device_state); + if (status != VL53L1_ERROR_NONE) { + return status; + } + if ((device_state & 0x01) == 0x01) { + ESP_LOGD(TAG, "Finished waiting for boot. Device state: %d", device_state); + return VL53L1_ERROR_NONE; + } + App.feed_wdt(); + } + + ESP_LOGW(TAG, "Timed out waiting for boot. state: %d", device_state); + return VL53L1_ERROR_TIME_OUT; +} + +VL53L1_Error VL53L1X::get_device_state(uint8_t *device_state) { + VL53L1_Error status = sensor.GetBootState(device_state); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to read device state. error: %d", status); + return status; + } + + // Our own logic...device_state is 255 when unable to complete read + // Not sure why and why other libraries don't account for this. + // Maybe somehow this is supposed to be 0, and it is getting messed up in I2C layer. + if (*device_state == 255) { + *device_state = 98; // Unknown + } + + ESP_LOGV(TAG, "Device state: %d", *device_state); + + return VL53L1_ERROR_NONE; +} + void VL53L1X::set_ranging_mode(const RangingMode *mode) { if (this->is_failed()) { ESP_LOGE(TAG, "Cannot set ranging mode while component is failed"); @@ -80,17 +152,26 @@ void VL53L1X::set_ranging_mode(const RangingMode *mode) { optional VL53L1X::read_distance(ROI *roi, VL53L1_Error &status) { if (this->is_failed()) { - ESP_LOGE(TAG, "Cannot read distance while component is failed"); + ESP_LOGW(TAG, "Cannot read distance while component is failed"); return {}; } - ESP_LOGV(TAG, "Beginning distance read"); + ESP_LOGVV(TAG, "Beginning distance read"); - status = this->sensor.SetROI(roi->width, roi->height); - status += this->sensor.SetROICenter(roi->center); - if (status != VL53L1_ERROR_NONE) { - ESP_LOGE(TAG, "Could not set ROI, error code: %d", status); - return {}; + if (last_roi == nullptr || *roi != *last_roi) { + ESP_LOGVV(TAG, "Setting new ROI: { width: %d, height: %d, center: %d }", roi->width, roi->height, roi->center); + + status = this->sensor.SetROI(roi->width, roi->height); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Could not set ROI width/height, error code: %d", status); + return {}; + } + status = this->sensor.SetROICenter(roi->center); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Could not set ROI center, error code: %d", status); + return {}; + } + last_roi = roi; } status = this->sensor.StartRanging(); @@ -99,17 +180,18 @@ optional VL53L1X::read_distance(ROI *roi, VL53L1_Error &status) { // TODO use interrupt_pin, if given, to await data ready instead of polling uint8_t dataReady = false; while (!dataReady) { - status += this->sensor.CheckForDataReady(&dataReady); + status = this->sensor.CheckForDataReady(&dataReady); if (status != VL53L1_ERROR_NONE) { ESP_LOGE(TAG, "Failed to check if data is ready, error code: %d", status); return {}; } delay(1); + App.feed_wdt(); } // Get the results uint16_t distance; - status += this->sensor.GetDistanceInMm(&distance); + status = this->sensor.GetDistanceInMm(&distance); if (status != VL53L1_ERROR_NONE) { ESP_LOGE(TAG, "Could not get distance, error code: %d", status); return {}; @@ -117,13 +199,17 @@ optional VL53L1X::read_distance(ROI *roi, VL53L1_Error &status) { // After reading the results reset the interrupt to be able to take another measurement status = this->sensor.ClearInterrupt(); - status += this->sensor.StopRanging(); + if (status != VL53L1_ERROR_NONE) { + ESP_LOGE(TAG, "Could not clear interrupt, error code: %d", status); + return {}; + } + status = this->sensor.StopRanging(); if (status != VL53L1_ERROR_NONE) { ESP_LOGE(TAG, "Could not stop ranging, error code: %d", status); return {}; } - ESP_LOGV(TAG, "Finished distance read"); + ESP_LOGV(TAG, "Finished distance read: %d", distance); return {distance}; } diff --git a/components/vl53l1x/vl53l1x.h b/components/vl53l1x/vl53l1x.h index 70fb0ea8..ee0f7e18 100644 --- a/components/vl53l1x/vl53l1x.h +++ b/components/vl53l1x/vl53l1x.h @@ -3,6 +3,7 @@ #include "VL53L1X_ULD.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/gpio.h" #include "esphome/core/log.h" @@ -21,7 +22,7 @@ class VL53L1X : public i2c::I2CDevice, public Component { public: void setup() override; void dump_config() override; - // After GPIO, but before default...Is this needed? not sure. + /** This connects directly to a sensor */ float get_setup_priority() const override { return setup_priority::DATA; }; optional read_distance(ROI *roi, VL53L1_Error &error); @@ -33,6 +34,7 @@ class VL53L1X : public i2c::I2CDevice, public Component { void set_ranging_mode_override(const RangingMode *mode) { this->ranging_mode_override = {mode}; } void set_offset(int16_t val) { this->offset = val; } void set_xtalk(uint16_t val) { this->xtalk = val; } + void set_timeout(uint16_t val) { this->timeout = val; } protected: VL53L1X_ULD sensor; @@ -43,6 +45,12 @@ class VL53L1X : public i2c::I2CDevice, public Component { optional ranging_mode_override{}; optional offset{}; optional xtalk{}; + uint16_t timeout{}; + ROI *last_roi{}; + + VL53L1_Error init(); + VL53L1_Error wait_for_boot(); + VL53L1_Error get_device_state(uint8_t *device_state); }; } // namespace vl53l1x